[App Search] Add configurable Filter and Sort modal to Documents View (#88066)
This commit is contained in:
parent
e2fc156bc5
commit
329a5a3f21
|
@ -4,15 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// Helper for calling the returned useEffect unmount handler
|
||||
let mockUnmountHandler: () => void;
|
||||
export const unmountHandler = () => mockUnmountHandler();
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...(jest.requireActual('react') as object),
|
||||
useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior
|
||||
useEffect: jest.fn((fn) => {
|
||||
mockUnmountHandler = fn();
|
||||
return mockUnmountHandler;
|
||||
}), // Calls on mount/every update - use mount for more complex behavior
|
||||
}));
|
||||
|
||||
// Helper for calling the returned useEffect unmount handler
|
||||
import { useEffect } from 'react';
|
||||
export const unmountHandler = () => (useEffect as jest.Mock).mock.calls[0][0]()();
|
||||
|
||||
/**
|
||||
* Example usage within a component test using shallow():
|
||||
*
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
import { CustomizationCallout } from './customization_callout';
|
||||
|
||||
describe('CustomizationCallout', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const onClick = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = shallow(<CustomizationCallout onClick={onClick} />);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.isEmptyRender()).toBe(false);
|
||||
});
|
||||
|
||||
it('calls onClick param when the Customize button is clicked', () => {
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
onClick(): void;
|
||||
}
|
||||
|
||||
export const CustomizationCallout: React.FC<Props> = ({ onClick }) => {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
className="customizationCallout"
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiIcon type="iInCircle" color="primary" size="xxl" />
|
||||
<EuiSpacer />
|
||||
<EuiText size="s" textAlign="center">
|
||||
<strong>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationCallout.message',
|
||||
{
|
||||
defaultMessage:
|
||||
'Did you know that you can customize your document search experience? Click "Customize" below to get started.',
|
||||
}
|
||||
)}
|
||||
</strong>
|
||||
</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiButton fill color="primary" iconType="gear" onClick={onClick}>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationCallout.button',
|
||||
{
|
||||
defaultMessage: 'Customize',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { setMockValues, setMockActions } from '../../../../__mocks__';
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { EuiButton, EuiButtonEmpty } from '@elastic/eui';
|
||||
|
||||
import { CustomizationModal } from './customization_modal';
|
||||
|
||||
describe('CustomizationModal', () => {
|
||||
const props = {
|
||||
filterFields: ['field1', 'field2'],
|
||||
sortFields: ['sortField1', 'sortField2'],
|
||||
onClose: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
};
|
||||
|
||||
const values = {
|
||||
engine: {
|
||||
name: 'some-engine',
|
||||
apiKey: '1234',
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
setEngine: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockValues(values);
|
||||
setMockActions(actions);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<CustomizationModal {...props} />);
|
||||
expect(wrapper.isEmptyRender()).toBe(false);
|
||||
});
|
||||
|
||||
it('when save is clicked, it calls onSave prop with selected filter and sort fields', () => {
|
||||
const wrapper = shallow(<CustomizationModal {...props} />);
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
expect(props.onSave).toHaveBeenCalledWith({
|
||||
filterFields: ['field1', 'field2'],
|
||||
sortFields: ['sortField1', 'sortField2'],
|
||||
});
|
||||
});
|
||||
|
||||
it('when save is clicked, it calls onSave prop when with updated selections', () => {
|
||||
const wrapper = shallow(<CustomizationModal {...props} />);
|
||||
|
||||
const sortFieldsDropdown = wrapper.find('[data-test-subj="sortFieldsDropdown"]');
|
||||
sortFieldsDropdown.simulate('change', [{ label: 'newSort1' }]);
|
||||
|
||||
const filterFieldsDropdown = wrapper.find('[data-test-subj="filterFieldsDropdown"]');
|
||||
filterFieldsDropdown.simulate('change', [{ label: 'newField1' }]);
|
||||
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
expect(props.onSave).toHaveBeenCalledWith({
|
||||
filterFields: ['newField1'],
|
||||
sortFields: ['newSort1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onClose when cancel is clicked', () => {
|
||||
const wrapper = shallow(<CustomizationModal {...props} />);
|
||||
wrapper.find(EuiButtonEmpty).simulate('click');
|
||||
expect(props.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiComboBox,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiOverlayMask,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { EngineLogic } from '../../engine';
|
||||
|
||||
interface Props {
|
||||
filterFields: string[];
|
||||
sortFields: string[];
|
||||
onClose(): void;
|
||||
onSave({ sortFields, filterFields }: { sortFields: string[]; filterFields: string[] }): void;
|
||||
}
|
||||
|
||||
const fieldNameToComboBoxOption = (fieldName: string) => ({ label: fieldName });
|
||||
const comboBoxOptionToFieldName = ({ label }: { label: string }) => label;
|
||||
|
||||
export const CustomizationModal: React.FC<Props> = ({
|
||||
filterFields,
|
||||
onClose,
|
||||
onSave,
|
||||
sortFields,
|
||||
}) => {
|
||||
const { engine } = useValues(EngineLogic);
|
||||
|
||||
const [selectedFilterFields, setSelectedFilterFields] = useState(
|
||||
filterFields.map(fieldNameToComboBoxOption)
|
||||
);
|
||||
const [selectedSortFields, setSelectedSortFields] = useState(
|
||||
sortFields.map(fieldNameToComboBoxOption)
|
||||
);
|
||||
|
||||
const engineSchema = engine.schema || {};
|
||||
const selectableFilterFields = useMemo(
|
||||
() => Object.keys(engineSchema).map(fieldNameToComboBoxOption),
|
||||
[engineSchema]
|
||||
);
|
||||
const selectableSortFields = useMemo(
|
||||
() => Object.keys(engineSchema).map(fieldNameToComboBoxOption),
|
||||
[engineSchema]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={onClose}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.title',
|
||||
{
|
||||
defaultMessage: 'Customize document search',
|
||||
}
|
||||
)}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.filterFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Filter fields',
|
||||
}
|
||||
)}
|
||||
fullWidth={true}
|
||||
helpText={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.filterFields',
|
||||
{
|
||||
defaultMessage:
|
||||
'Faceted values rendered as filters and available as query refinement',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="filterFieldsDropdown"
|
||||
fullWidth={true}
|
||||
options={selectableFilterFields}
|
||||
selectedOptions={selectedFilterFields}
|
||||
onChange={setSelectedFilterFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.sortFieldsLabel',
|
||||
{
|
||||
defaultMessage: 'Sort fields',
|
||||
}
|
||||
)}
|
||||
fullWidth={true}
|
||||
helpText={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.sortFields',
|
||||
{
|
||||
defaultMessage:
|
||||
'Used to display result sorting options, ascending and descending',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="sortFieldsDropdown"
|
||||
fullWidth={true}
|
||||
options={selectableSortFields}
|
||||
selectedOptions={selectedSortFields}
|
||||
onChange={setSelectedSortFields}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.cancel',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={() => {
|
||||
onSave({
|
||||
filterFields: selectedFilterFields.map(comboBoxOptionToFieldName),
|
||||
sortFields: selectedSortFields.map(comboBoxOptionToFieldName),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.save',
|
||||
{
|
||||
defaultMessage: 'Save',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -20,4 +20,9 @@
|
|||
.documentsSearchExperience__pagingInfo {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.customizationCallout {
|
||||
background-color: $euiPageBackgroundColor;
|
||||
padding: $euiSizeL;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,26 @@ import '../../../../__mocks__/kea.mock';
|
|||
import { setMockValues } from '../../../../__mocks__';
|
||||
import '../../../../__mocks__/enterprise_search_url.mock';
|
||||
|
||||
const mockSetFields = jest.fn();
|
||||
|
||||
jest.mock('../../../../shared/use_local_storage', () => ({
|
||||
useLocalStorage: jest.fn(() => [
|
||||
{
|
||||
filterFields: ['a', 'b', 'c'],
|
||||
sortFields: ['d', 'c'],
|
||||
},
|
||||
mockSetFields,
|
||||
]),
|
||||
}));
|
||||
|
||||
import React from 'react';
|
||||
// @ts-expect-error types are not available for this package yet
|
||||
import { SearchProvider } from '@elastic/react-search-ui';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { CustomizationCallout } from './customization_callout';
|
||||
import { CustomizationModal } from './customization_modal';
|
||||
|
||||
import { SearchExperience } from './search_experience';
|
||||
|
||||
describe('SearchExperience', () => {
|
||||
|
@ -31,4 +46,40 @@ describe('SearchExperience', () => {
|
|||
const wrapper = shallow(<SearchExperience />);
|
||||
expect(wrapper.find(SearchProvider).length).toBe(1);
|
||||
});
|
||||
|
||||
describe('customization modal', () => {
|
||||
it('has a customization modal which can be opened and closed', () => {
|
||||
const wrapper = shallow(<SearchExperience />);
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(false);
|
||||
|
||||
wrapper.find(CustomizationCallout).simulate('click');
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(true);
|
||||
|
||||
wrapper.find(CustomizationModal).prop('onClose')();
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('passes values from localStorage to the customization modal', () => {
|
||||
const wrapper = shallow(<SearchExperience />);
|
||||
wrapper.find(CustomizationCallout).simulate('click');
|
||||
expect(wrapper.find(CustomizationModal).prop('filterFields')).toEqual(['a', 'b', 'c']);
|
||||
expect(wrapper.find(CustomizationModal).prop('sortFields')).toEqual(['d', 'c']);
|
||||
});
|
||||
|
||||
it('updates selected fields in localStorage and closes modal on save', () => {
|
||||
const wrapper = shallow(<SearchExperience />);
|
||||
wrapper.find(CustomizationCallout).simulate('click');
|
||||
wrapper.find(CustomizationModal).prop('onSave')({
|
||||
filterFields: ['new', 'filters'],
|
||||
sortFields: ['new', 'sorts'],
|
||||
});
|
||||
|
||||
expect(mockSetFields).toHaveBeenCalledWith({
|
||||
filterFields: ['new', 'filters'],
|
||||
sortFields: ['new', 'sorts'],
|
||||
});
|
||||
|
||||
expect(wrapper.find(CustomizationModal).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useValues } from 'kea';
|
||||
|
@ -17,10 +17,13 @@ import './search_experience.scss';
|
|||
|
||||
import { EngineLogic } from '../../engine';
|
||||
import { externalUrl } from '../../../../shared/enterprise_search_url';
|
||||
import { useLocalStorage } from '../../../../shared/use_local_storage';
|
||||
|
||||
import { SearchBoxView, SortingView } from './views';
|
||||
import { SearchExperienceContent } from './search_experience_content';
|
||||
import { buildSearchUIConfig } from './build_search_ui_config';
|
||||
import { CustomizationCallout } from './customization_callout';
|
||||
import { CustomizationModal } from './customization_modal';
|
||||
|
||||
const DEFAULT_SORT_OPTIONS = [
|
||||
{
|
||||
|
@ -43,6 +46,18 @@ export const SearchExperience: React.FC = () => {
|
|||
const { engine } = useValues(EngineLogic);
|
||||
const endpointBase = externalUrl.enterpriseSearchUrl;
|
||||
|
||||
const [showCustomizationModal, setShowCustomizationModal] = useState(false);
|
||||
const openCustomizationModal = () => setShowCustomizationModal(true);
|
||||
const closeCustomizationModal = () => setShowCustomizationModal(false);
|
||||
|
||||
const [fields, setFields] = useLocalStorage(
|
||||
`documents-search-experience-customization--${engine.name}`,
|
||||
{
|
||||
filterFields: [] as string[],
|
||||
sortFields: [] as string[],
|
||||
}
|
||||
);
|
||||
|
||||
// TODO const sortFieldsOptions = _flatten(fields.sortFields.map(fieldNameToSortOptions)) // we need to flatten this array since fieldNameToSortOptions returns an array of two sorting options
|
||||
const sortingOptions = [...DEFAULT_SORT_OPTIONS /* TODO ...sortFieldsOptions*/];
|
||||
|
||||
|
@ -85,12 +100,25 @@ export const SearchExperience: React.FC = () => {
|
|||
sortOptions={sortingOptions}
|
||||
view={SortingView}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<CustomizationCallout onClick={openCustomizationModal} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="documentsSearchExperience__content">
|
||||
<SearchExperienceContent />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</SearchProvider>
|
||||
{showCustomizationModal && (
|
||||
<CustomizationModal
|
||||
filterFields={fields.filterFields}
|
||||
sortFields={fields.sortFields}
|
||||
onClose={closeCustomizationModal}
|
||||
onSave={({ filterFields, sortFields }) => {
|
||||
setFields({ filterFields, sortFields });
|
||||
closeCustomizationModal();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { useLocalStorage } from './use_local_storage';
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { useLocalStorage } from './use_local_storage';
|
||||
|
||||
describe('useLocalStorage', () => {
|
||||
const KEY = 'fields';
|
||||
|
||||
const TestComponent = () => {
|
||||
const [fields, setFields] = useLocalStorage(KEY, {
|
||||
options: ['foo', 'bar', 'baz'],
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
id="change"
|
||||
onClick={() => {
|
||||
setFields({
|
||||
options: ['big', 'new', 'values'],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{fields?.options?.join(', ')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
global.localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('will read state from localStorage on init if values already exist', () => {
|
||||
global.localStorage.setItem(
|
||||
KEY,
|
||||
JSON.stringify({
|
||||
options: ['some', 'old', 'values'],
|
||||
})
|
||||
);
|
||||
const wrapper = shallow(<TestComponent />);
|
||||
expect(wrapper.text()).toBe('some, old, values');
|
||||
});
|
||||
|
||||
it('will ignore non-JSON values in localStorage', () => {
|
||||
global.localStorage.setItem(KEY, 'blah blah blah');
|
||||
const wrapper = shallow(<TestComponent />);
|
||||
expect(wrapper.text()).toBe('foo, bar, baz');
|
||||
expect(global.localStorage.getItem(KEY)).toBe('{"options":["foo","bar","baz"]}');
|
||||
});
|
||||
|
||||
it('if will use provided default values if state does not already exist in localStorage', () => {
|
||||
const wrapper = shallow(<TestComponent />);
|
||||
expect(wrapper.text()).toBe('foo, bar, baz');
|
||||
expect(global.localStorage.getItem(KEY)).toBe('{"options":["foo","bar","baz"]}');
|
||||
});
|
||||
|
||||
it('state can be updated with new values', () => {
|
||||
const wrapper = shallow(<TestComponent />);
|
||||
wrapper.find('#change').simulate('click');
|
||||
expect(wrapper.text()).toBe('big, new, values');
|
||||
expect(global.localStorage.getItem(KEY)).toBe('{"options":["big","new","values"]}');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* A hook that works like `useState`, but persisted to localStorage.
|
||||
*
|
||||
* example:
|
||||
*
|
||||
* const [foo, setFoo] = useLocalStorage("foo", "bar");
|
||||
*
|
||||
* console.log(foo) // "bar"
|
||||
* setFoo("baz")
|
||||
* console.log(foo) // "baz"
|
||||
*
|
||||
* // Navigate away from page and return
|
||||
*
|
||||
* const [foo, setFoo] = useLocalStorage("foo", "bar");
|
||||
* console.log(foo) // "baz"
|
||||
*/
|
||||
export const useLocalStorage = <Value>(key: string, defaultValue: Value): [Value, Function] => {
|
||||
const saveToStorage = (value: Value) => window.localStorage.setItem(key, JSON.stringify(value));
|
||||
const removeFromStorage = () => window.localStorage.removeItem(key);
|
||||
const getFromStorage = (): Value | undefined => {
|
||||
const storedItem = window.localStorage.getItem(key);
|
||||
if (!storedItem) return;
|
||||
|
||||
let parsedItem;
|
||||
try {
|
||||
return JSON.parse(storedItem) as Value;
|
||||
} catch (e) {
|
||||
removeFromStorage();
|
||||
}
|
||||
|
||||
return parsedItem;
|
||||
};
|
||||
|
||||
const storedItem = getFromStorage();
|
||||
if (!storedItem) {
|
||||
saveToStorage(defaultValue);
|
||||
}
|
||||
const toStore = storedItem || defaultValue;
|
||||
|
||||
const [item, setItem] = useState<Value>(toStore);
|
||||
|
||||
const saveItem = (value: Value) => {
|
||||
saveToStorage(value);
|
||||
setItem(value);
|
||||
};
|
||||
|
||||
return [item, saveItem];
|
||||
};
|
Loading…
Reference in a new issue