chore: add docs and tests

chore: remove compo

chore: add docs and tests for useFilter

chore: add tests & docs for useCollator
This commit is contained in:
Josh 2023-04-17 11:24:21 +01:00
parent 7f1b43adf9
commit ef4f3b64fe
13 changed files with 11672 additions and 8017 deletions

View File

@ -0,0 +1,49 @@
---
title: useCollator
description: API reference for the useCollator hook in Strapi
tags:
- hooks
- helper-plugin
- i18n
---
A custom hook that returns the [`Intl.Collator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator) native API.
Useful for searching & sorting strings that are localised. The implementation method has a unique cache store to avoid creating a new instance of the
`Intl.Collator` for each call of the hook depending on the locale and options provided.
Stolen from [`react-aria`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/i18n/src/useCollator.ts).
## Usage
```js
import { useIntl } from 'react-intl';
import { useCollator } from '@strapi/helper-plugin';
const MyComponent = () => {
const { locale } = useIntl();
const fruits = ['banana', 'apple', 'orange', 'kiwi'];
const formatter = useCollator(locale, { sensitivity: 'base' });
/**
* This would render the list of fruits in the following order:
* apple
* banana
* kiwi
* orange
*/
return (
<div>
{fruits.sort(formatter.compare).map((fruit) => (
<h2 key={fruit}>{fruit}</h2>
))}
</div>
);
};
```
## Typescript
```ts
function useCollator(locale: string, options?: Intl.CollatorOptions): Intl.Collator;
```

View File

@ -0,0 +1,59 @@
---
title: useFilter
description: API reference for the useFilter hook in Strapi
tags:
- hooks
- helper-plugin
- i18n
---
Provides localized string search functionality that is useful for filtering or matching items in a list. Options can be
provided to adjust the sensitivity to case, diacritics, and other parameters.
Stolen from [`react-aria`](https://github.com/adobe/react-spectrum/blob/main/packages/%40react-aria/i18n/src/useFilter.ts).
## Usage
```js
import { useIntl } from 'react-intl';
import { useFilter } from '@strapi/helper-plugin';
const MyComponent = () => {
const { locale } = useIntl();
const fruits = ['orange', 'apple', 'kiwi', 'banana'];
const searchValue = 'an';
const { contains } = useFilter(locale, { sensitivity: 'base' });
/**
* This would render the list of fruits in the following order:
* orange
* banana
*/
return (
<div>
{fruits
.filter((fruit) => includes(fruit, searchValue))
.map((fruit) => (
<h2 key={fruit}>{fruit}</h2>
))}
</div>
);
};
```
## Typescript
```ts
interface Filter {
/** Returns whether a string starts with a given substring. */
startsWith(string: string, substring: string): boolean;
/** Returns whether a string ends with a given substring. */
endsWith(string: string, substring: string): boolean;
/** Returns whether a string contains a given substring. */
includes(string: string, substring: string): boolean;
}
function useFilter(locale: string, options?: Intl.CollatorOptions): Filter;
```

View File

@ -115,16 +115,25 @@ const sidebars = {
type: 'category',
label: 'Hooks',
items: [
{
type: 'doc',
label: 'useAPIErrorHandler',
id: 'core/helper-plugin/hooks/use-api-error-handler',
},
{
type: 'doc',
label: 'useCollator',
id: 'core/helper-plugin/hooks/use-collator',
},
{
type: 'doc',
label: 'useFetchClient',
id: 'core/helper-plugin/hooks/use-fetch-client',
},
{
type: 'doc',
label: 'useAPIErrorHandler',
id: 'core/helper-plugin/hooks/use-api-error-handler',
label: 'useFilter',
id: 'core/helper-plugin/hooks/use-filter',
},
],
},

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@ const LeftMenu = () => {
const modelLinksSelector = useMemo(makeSelectModelLinks, []);
const { collectionTypeLinks, singleTypeLinks } = useSelector(modelLinksSelector, shallowEqual);
const { contains } = useFilter(locale, {
const { includes } = useFilter(locale, {
sensitivity: 'base',
});
@ -65,7 +65,7 @@ const LeftMenu = () => {
/**
* Filter by the search value
*/
.filter((link) => contains(link.title, search))
.filter((link) => includes(link.title, search))
/**
* Sort correctly using the language
*/
@ -80,7 +80,7 @@ const LeftMenu = () => {
};
}),
})),
[collectionTypeLinks, search, singleTypeLinks, contains, formatMessage, formatter]
[collectionTypeLinks, search, singleTypeLinks, includes, formatMessage, formatter]
);
const handleClear = () => {

View File

@ -49,7 +49,7 @@ const useSortedRoles = () => {
const [{ query }] = useQueryParams();
const _q = query?._q || '';
const { contains } = useFilter(locale, {
const { includes } = useFilter(locale, {
sensitivity: 'base',
});
@ -61,7 +61,7 @@ const useSortedRoles = () => {
});
const sortedRoles = (roles || [])
.filter((role) => contains(role.name, _q) || contains(role.description, _q))
.filter((role) => includes(role.name, _q) || includes(role.description, _q))
.sort(
(a, b) => formatter.compare(a.name, b.name) || formatter.compare(a.description, b.description)
);

View File

@ -25,7 +25,7 @@ const useContentTypeBuilderMenu = () => {
const { onOpenModalCreateSchema, onOpenModalEditCategory } = useFormModalNavigation();
const { locale } = useIntl();
const { contains } = useFilter(locale, {
const { includes } = useFilter(locale, {
sensitivity: 'base',
});
@ -117,10 +117,10 @@ const useContentTypeBuilderMenu = () => {
}
},
links: components
.map((compo) => ({
name: compo.uid,
to: `/plugins/${pluginId}/component-categories/${category}/${compo.uid}`,
title: compo.schema.displayName,
.map((component) => ({
name: component.uid,
to: `/plugins/${pluginId}/component-categories/${category}/${component.uid}`,
title: component.schema.displayName,
}))
.sort((a, b) => formatter.compare(a.title, b.title)),
}))
@ -177,7 +177,7 @@ const useContentTypeBuilderMenu = () => {
links: section.links.map((link) => ({
...link,
links: link.links
.filter((link) => contains(link.title, search))
.filter((link) => includes(link.title, search))
.sort((a, b) => formatter.compare(a.title, b.title)),
})),
};
@ -186,7 +186,7 @@ const useContentTypeBuilderMenu = () => {
return {
...section,
links: section.links
.filter((link) => contains(link.title, search))
.filter((link) => includes(link.title, search))
.sort((a, b) => formatter.compare(a.title, b.title)),
};
});

View File

@ -34,7 +34,7 @@ const HomePage = () => {
const items = [{name: 'Paul', instrument: 'bass'}, {name: 'George', instrument: 'guitar'}]
const { contains } = useFilter(locale, {
const { includes } = useFilter(locale, {
sensitivity: 'base',
});
@ -44,7 +44,7 @@ const HomePage = () => {
const sortedList = items
.filter((item) => contains(item.name, _q) || contains(item.instrument, _q))
.filter((item) => includes(item.name, _q) || includes(item.instrument, _q))
.sort(
(a, b) => formatter.compare(a.name, b.name) || formatter.compare(a.instrument, b.instrument)
);
@ -83,14 +83,14 @@ const HomePage = () => {
{ name: 'George', instrument: 'guitar' },
];
const { locale } = useIntl();
const { contains } = useFilter(locale, {
const { includes } = useFilter(locale, {
sensitivity: 'base',
});
const formatter = useCollator(locale, {
sensitivity: 'base',
});
const sortedList = items
.filter((item) => contains(item.name, _q) || contains(item.instrument, _q))
.filter((item) => includes(item.name, _q) || includes(item.instrument, _q))
.sort(
(a, b) =>
formatter.compare(a.name, b.name) || formatter.compare(a.instrument, b.instrument)

View File

@ -0,0 +1,54 @@
import { renderHook } from '@testing-library/react-hooks';
import { useCollator } from '../useCollator';
describe('useCollator', () => {
it('should return the Intl.Collator class', () => {
const { result } = renderHook(() => useCollator('en'));
expect(result.current).toBeInstanceOf(Intl.Collator);
});
it('should pass options to the Intl.Collator class if I pass them to the hook', () => {
const { result } = renderHook(() => useCollator('en', { sensitivity: 'base' }));
expect(result.current.resolvedOptions().sensitivity).toBe('base');
});
it('should return me a new Intl.Collator if I pass a new locale or options object', () => {
const { result, rerender } = renderHook(({ locale, options }) => useCollator(locale, options), {
initialProps: {
locale: 'en',
options: { sensitivity: 'base' },
},
});
const first = result.current;
rerender({
locale: 'en',
options: { sensitivity: 'accent' },
});
const second = result.current;
expect(second).not.toBe(first);
rerender({
locale: 'fr',
options: { sensitivity: 'accent' },
});
const third = result.current;
expect(third).not.toBe(second);
expect(third).not.toBe(first);
rerender({
locale: 'en',
options: { sensitivity: 'base' },
});
expect(result.current).toBe(first);
});
});

View File

@ -0,0 +1,73 @@
import { renderHook } from '@testing-library/react-hooks';
import { useFilter } from '../useFilter';
describe('useFilter', () => {
it("should return an object with the properties 'startsWith', 'endsWith, & 'includes'", () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current).toHaveProperty('startsWith');
expect(result.current).toHaveProperty('endsWith');
expect(result.current).toHaveProperty('includes');
});
describe('startsWith', () => {
it('should return true if the substring is empty', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.startsWith('foo', '')).toBe(true);
});
it('should return true if the string starts with the substring', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.startsWith('foo', 'f')).toBe(true);
});
it('should return false if the string does not start with the substring', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.startsWith('foo', 'o')).toBe(false);
});
});
describe('endsWith', () => {
it('should return true if the substring is empty', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.endsWith('foo', '')).toBe(true);
});
it('should return true if the string ends with the substring', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.endsWith('foo', 'o')).toBe(true);
});
it('should return false if the string does not end with the substring', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.endsWith('foo', 'f')).toBe(false);
});
});
describe('includes', () => {
it('should return true if the substring is empty', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.includes('foo', '')).toBe(true);
});
it('should return true if the string includes the substring', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.includes('foo', 'o')).toBe(true);
});
it('should return false if the string does not include the substring', () => {
const { result } = renderHook(() => useFilter('en'));
expect(result.current.includes('foo', 'b')).toBe(false);
});
});
});

View File

@ -8,7 +8,7 @@ import { useCollator } from './useCollator';
* @typedef {Object} Filter
* @property {(string: string, substring: string) => boolean} startsWith Returns whether a string starts with a given substring.
* @property {(string: string, substring: string) => boolean} endsWith Returns whether a string ends with a given substring.
* @property {(string: string, substring: string) => boolean} contains Returns whether a string contains a given substring.
* @property {(string: string, substring: string) => boolean} includes Returns whether a string contains a given substring.
*/
/**
@ -45,7 +45,7 @@ export function useFilter(locale, options) {
return collator.compare(string.slice(-substring.length), substring) === 0;
},
contains(string, substring) {
includes(string, substring) {
if (substring.length === 0) {
return true;
}

View File

@ -79,7 +79,7 @@ const RoleListPage = () => {
enabled: canRead,
});
const { contains } = useFilter(locale, {
const { includes } = useFilter(locale, {
sensitivity: 'base',
});
@ -131,7 +131,7 @@ const RoleListPage = () => {
};
const sortedRoles = (roles || [])
.filter((role) => contains(role.name, _q) || contains(role.description, _q))
.filter((role) => includes(role.name, _q) || includes(role.description, _q))
.sort(
(a, b) => formatter.compare(a.name, b.name) || formatter.compare(a.description, b.description)
);

View File

@ -7046,7 +7046,6 @@ __metadata:
markdown-it-mark: ^3.0.1
markdown-it-sub: ^1.0.0
markdown-it-sup: 1.0.0
match-sorter: ^4.0.2
mini-css-extract-plugin: 2.7.2
msw: 1.0.1
node-schedule: 2.1.0
@ -7264,7 +7263,6 @@ __metadata:
history: ^4.9.0
immer: 9.0.19
lodash: 4.17.21
match-sorter: ^4.0.2
prop-types: ^15.7.2
qs: 6.11.1
react: ^17.0.2
@ -7383,7 +7381,6 @@ __metadata:
history: ^4.9.0
immer: 9.0.19
lodash: 4.17.21
match-sorter: ^4.0.2
pluralize: ^8.0.0
prop-types: ^15.7.2
qs: 6.11.1
@ -7635,7 +7632,6 @@ __metadata:
koa: ^2.13.4
koa2-ratelimit: ^1.1.2
lodash: 4.17.21
match-sorter: ^4.0.2
msw: 1.0.1
prop-types: ^15.7.2
purest: 4.0.2
@ -22250,16 +22246,6 @@ __metadata:
languageName: node
linkType: hard
"match-sorter@npm:^4.0.2":
version: 4.2.1
resolution: "match-sorter@npm:4.2.1"
dependencies:
"@babel/runtime": ^7.10.5
remove-accents: 0.4.2
checksum: 7f3cd8f84cdb4567b7a81f66a095e418044f318f41b6c8a1640730c99e370af9e5054f5a1ed2dfa1287a23101d75a105da63e4d95e8ac2f7061a8f8b32367c7d
languageName: node
linkType: hard
"match-sorter@npm:^6.0.2":
version: 6.3.1
resolution: "match-sorter@npm:6.3.1"