Skip to content

Commit

Permalink
Small Refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
widoz committed Oct 29, 2023
1 parent 453d47a commit 1ba6dad
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 56 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text eol=lf
16 changes: 9 additions & 7 deletions @types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ declare namespace EntitiesSearch {
: PostType<'edit'>[K];
}>;

type Record<V extends any> = Readonly<{
type ControlOption<V extends any> = Readonly<{
value: V;
label: string;
}>;

type ComponentStateAware<V> = {
value: Set<V>;
value: V;
setValue(value: ComponentStateAware['value']): void;
};

Expand All @@ -40,19 +40,21 @@ declare namespace EntitiesSearch {
* Components
*/
interface PostTypeSelect<V> {
readonly value: Record<V> | null;
readonly options: Set<NonNullable<PostTypeSelect<V>['value']>>;
readonly value: V | null;
readonly options: Set<ControlOption<V>>;
readonly onChange: (value: PostTypeSelect<V>['value']) => void;
}

interface PostsSelect<V> {
readonly value: Set<Record<V>> | null;
readonly options: NonNullable<PostsSelect<V>['value']>;
readonly value: Set<V> | null;
readonly options: Set<ControlOption<V>>;
readonly onChange: (values: PostsSelect<V>['value']) => void;
}

interface PostsController<P, T> {
readonly postsComponent: React.ComponentType<ComponentStateAware<P>>;
readonly postsComponent: React.ComponentType<
ComponentStateAware<Set<P>>
>;
readonly typesComponent: React.ComponentType<ComponentStateAware<T>>;
}
}
31 changes: 28 additions & 3 deletions sources/js/src/components/post-type-select.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
import EntitiesSearch from '@types';
import { Set } from 'immutable';
import React, { JSX } from 'react';
import Select from 'react-select';
import Select, { SingleValue } from 'react-select';

import { matchOptionValues } from '../utils/match-option-values';
import { onChangeControlOptionsHandle } from '../utils/on-change-control-options-handle';

export function PostTypeSelect<V>(
props: EntitiesSearch.PostTypeSelect<V>
): JSX.Element | null {
const matchedValues = matchOptionValues(Set([props.value]), props.options);

const onChange = (values: Set<V> | null) =>
props.onChange(values?.first() ?? null);

return (
<Select
isMulti={false}
value={props.value}
value={matchedValues?.first() ?? null}
options={props.options.toArray()}
onChange={props.onChange}
onChange={(options) => {
if (isNonNullableValue(options)) {
onChange(null);
return;
}

onChangeControlOptionsHandle(
onChange,
Set(options ? [options] : [])
);
}}
/>
);
}

function isNonNullableValue<V>(
value: SingleValue<EntitiesSearch.ControlOption<V>>
): value is NonNullable<EntitiesSearch.ControlOption<V>> {
return value !== null && value.value !== null;
}
4 changes: 2 additions & 2 deletions sources/js/src/components/posts-post-types-controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const PostsPostTypesController = <P, T>(

const Types = () => (
<props.typesComponent
value={state.types}
setValue={(types) => setState({ ...state, types })}
value={state.types.first()}
setValue={(type) => setState({ ...state, types: Set([type]) })}
/>
);

Expand Down
13 changes: 8 additions & 5 deletions sources/js/src/components/posts-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ import { Set } from 'immutable';
import React, { JSX } from 'react';
import Select from 'react-select';

import { isControlOption } from '../utils/is-control-option';
import { matchOptionValues } from '../utils/match-option-values';
import { onChangeControlOptionsHandle } from '../utils/on-change-control-options-handle';

export function PostsSelect<V>(
props: EntitiesSearch.PostsSelect<V>
): JSX.Element | null {
const values = matchOptionValues(props.value, props.options);

return (
<Select
isMulti={true}
value={props.value?.toArray()}
value={values?.toArray() ?? []}
options={props.options.toArray()}
onChange={(options) => {
props.onChange(Set(options.filter(isControlOption)));
}}
onChange={(options) =>
onChangeControlOptionsHandle(props.onChange, Set(options))
}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Set } from 'immutable';

export function convertPostTypesToControlOptions(
postTypes: Set<EntitiesSearch.PostType>
): Set<EntitiesSearch.Record<EntitiesSearch.PostType['slug']>> {
): Set<EntitiesSearch.ControlOption<EntitiesSearch.PostType['slug']>> {
return postTypes.map((postType) => ({
label: postType.name,
value: postType.slug,
Expand Down
6 changes: 3 additions & 3 deletions sources/js/src/utils/is-control-option.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import EntitiesSearch from '@types';

export function isControlOption(
export const isControlOption = (
value: unknown
): value is EntitiesSearch.Record<any> {
): value is EntitiesSearch.ControlOption<any> => {
if (!value) {
return false;
}
Expand All @@ -11,4 +11,4 @@ export function isControlOption(
}

return value.hasOwnProperty('label') && value.hasOwnProperty('value');
}
};
10 changes: 10 additions & 0 deletions sources/js/src/utils/match-option-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import EntitiesSearch from '@types';
import { Set } from 'immutable';

export const matchOptionValues = <V>(
value: Set<V> | null,
options: Set<EntitiesSearch.ControlOption<V>>
): Set<EntitiesSearch.ControlOption<V>> | null => {
if (!value) return null;
return options.filter((option) => value?.has(option.value));
};
19 changes: 19 additions & 0 deletions sources/js/src/utils/on-change-control-options-handle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import EntitiesSearch from '@types';
import { Set } from 'immutable';

import { isControlOption } from './is-control-option';

/**
* @internal
* @param handler
* @param options
*/
export const onChangeControlOptionsHandle = <V>(
handler: (values: Set<V> | null) => void,
options: Set<EntitiesSearch.ControlOption<V>> | null
): void => {
if (options === null) return;
const controlOptions = Set(options).filter(isControlOption);
const values = controlOptions.map((option) => option.value);
handler(values.size > 0 ? values : null);
};
13 changes: 4 additions & 9 deletions tests/js/unit/components/post-types-select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ jest.mock('react-select', () => (props: ReactSelect) => (
<select
id="post-type-select"
data-testid="post-type-select"
onChange={() =>
props.onChange({
label: faker.random.word(),
value: faker.word.noun(),
})
}
onChange={() => props.onChange(faker.word.noun())}
className="react-select"
>
{props.options.map((option: any) => (
Expand All @@ -41,18 +36,18 @@ describe('Post Types Select', () => {
*/
it('call the given onChange handler', (done) => {
let expectedCalled: boolean = false;
const option: EntitiesSearch.Record<string> = {
const option: EntitiesSearch.ControlOption<string> = {
label: faker.random.word(),
value: faker.word.noun(),
};
const options = Set<EntitiesSearch.Record<string>>([])
const options = Set<EntitiesSearch.ControlOption<string>>([])
.add(option)
.merge(buildOptions());

render(
<PostTypeSelect
options={options}
value={option}
value={option.value}
onChange={() => (expectedCalled = true)}
/>
);
Expand Down
16 changes: 6 additions & 10 deletions tests/js/unit/components/posts-post-types-controller.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,7 @@ describe('Posts Post Types Controller', () => {
);

const PostsComponent = (
props: EntitiesSearch.ComponentStateAware<
EntitiesSearch.ControlOption<string>
>
props: EntitiesSearch.ComponentStateAware<Set<string>>
) => {
return (
<PostsSelect
Expand All @@ -73,7 +71,7 @@ describe('Posts Post Types Controller', () => {
const postComponentElement = screen.getByTestId('posts-component');
userEvent.click(postComponentElement).then(() => {
// @ts-ignore
expect(STATE['posts'].toArray()).toEqual(['post-one', 'post-two']);
expect(STATE.posts.toArray()).toEqual(['post-one', 'post-two']);
done();
});
});
Expand All @@ -85,20 +83,18 @@ describe('Posts Post Types Controller', () => {
) => (
<button
data-testid="post-type-component"
onClick={() => props.onChange(Set(['post-type']))}
onClick={() => props.onChange('post-type')}
>
Update Component State
</button>
);

const PostTypeComponent = (
props: EntitiesSearch.ComponentStateAware<
EntitiesSearch.ControlOption<string>
>
props: EntitiesSearch.ComponentStateAware<string>
) => {
return (
<PostTypeSelect
value={props.value.first()}
value={props.value}
onChange={props.setValue}
options={Set([])}
/>
Expand All @@ -117,7 +113,7 @@ describe('Posts Post Types Controller', () => {
);
userEvent.click(postTypeComponentElement).then(() => {
// @ts-ignore
expect(STATE['types'].toArray()).toEqual(['post-type']);
expect(STATE.types.toArray()).toEqual(['post-type']);
done();
});
});
Expand Down
19 changes: 5 additions & 14 deletions tests/js/unit/components/posts-select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,15 @@ import { PostsSelect } from '../../../../sources/js/src/components/posts-select'
import { buildOptions } from '../utils';

type ReactSelect = EntitiesSearch.PostsSelect<string> & {
value: Array<EntitiesSearch.Record<string>>;
value: Array<EntitiesSearch.ControlOption<string>>;
};

jest.mock('react-select', () => (props: ReactSelect) => (
<select
multiple={true}
id="posts-select"
data-testid="posts-select"
onChange={() =>
props.onChange(
Set([
{
label: faker.random.word(),
value: faker.word.noun(),
},
])
)
}
onChange={() => props.onChange(Set([faker.word.noun()]))}
className="react-select"
>
{props.options.map((option: any) => (
Expand All @@ -44,18 +35,18 @@ jest.mock('react-select', () => (props: ReactSelect) => (
describe('Posts Select', () => {
it('call the given onChange handler', () => {
let expectedCalled: boolean = false;
const option: EntitiesSearch.Record<string> = {
const option: EntitiesSearch.ControlOption<string> = {
label: faker.random.word(),
value: faker.word.noun(),
};
const options = Set<EntitiesSearch.Record<string>>([])
const options = Set<EntitiesSearch.ControlOption<string>>([])
.add(option)
.merge(buildOptions());

render(
<PostsSelect
options={options}
value={Set([option])}
value={Set([option.value])}
onChange={() => (expectedCalled = true)}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions tests/js/unit/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Set } from 'immutable';

import { faker } from '@faker-js/faker';

export function buildOptions(): Set<EntitiesSearch.Record<string>> {
let options = Set<EntitiesSearch.Record<string>>([]);
export function buildOptions(): Set<EntitiesSearch.ControlOption<string>> {
let options = Set<EntitiesSearch.ControlOption<string>>([]);

for (let count = 0; count < 9; ++count) {
options = options.add({
Expand Down

0 comments on commit 1ba6dad

Please sign in to comment.