Skip to content

Commit 28f82ad

Browse files
committed
Normalize inputs
1 parent e99194f commit 28f82ad

File tree

5 files changed

+146
-98
lines changed

5 files changed

+146
-98
lines changed

frontend/packages/volto-light-theme/src/components/Widgets/ListFromList.stories.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,26 @@ export const Default: Story = {
2828
title: 'List from List',
2929
'aria-label': 'List from List',
3030
options: [
31-
{ id: '1', name: 'Adobe Photoshop' },
32-
{ id: '2', name: 'Adobe XD' },
33-
{ id: '3', name: 'Documents' },
34-
{ id: '4', name: 'Adobe InDesign' },
35-
{ id: '5', name: 'Utilities' },
36-
{ id: '6', name: 'Adobe AfterEffects' },
37-
{ id: '7', name: 'Pictures' },
38-
{ id: '8', name: 'Adobe Fresco' },
39-
{ id: '9', name: 'Apps' },
40-
{ id: '10', name: 'Adobe Illustrator' },
41-
{ id: '11', name: 'Adobe Lightroom' },
42-
{ id: '12', name: 'Adobe Dreamweaver' },
31+
'Adobe Photoshop',
32+
'Adobe XD',
33+
'Documents',
34+
'Adobe InDesign',
35+
'Utilities',
36+
'Adobe AfterEffects',
37+
'Pictures',
38+
'Adobe Fresco',
39+
'Apps',
40+
'Adobe Illustrator',
41+
'Adobe Lightroom',
42+
'Adobe Dreamweaver',
4343
],
4444
value: [
45-
{ id: '7', name: 'Pictures' },
46-
{ id: '8', name: 'Adobe Fresco' },
47-
{ id: '9', name: 'Apps' },
48-
{ id: '10', name: 'Adobe Illustrator' },
49-
{ id: '11', name: 'Adobe Lightroom' },
50-
{ id: '12', name: 'Adobe Dreamweaver' },
45+
'Pictures',
46+
'Adobe Fresco',
47+
'Apps',
48+
'Adobe Illustrator',
49+
'Adobe Lightroom',
50+
'Adobe Dreamweaver',
5151
],
5252
onChange: () => {},
5353
},

frontend/packages/volto-light-theme/src/components/Widgets/ListFromList.test.tsx

Lines changed: 0 additions & 41 deletions
This file was deleted.

frontend/packages/volto-light-theme/src/components/Widgets/ListFromList.tsx

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import {
99
ListBoxItem,
1010
} from 'react-aria-components';
1111
import { useEffect } from 'react';
12-
// import { isEqual } from 'lodash';
13-
// import { v4 as uuidv4 } from 'uuid';
12+
import {
13+
normalizeList,
14+
denormalizeList,
15+
} from '@kitconcept/volto-light-theme/helpers/listNormalizer';
1416

15-
interface FileItem {
16-
id: string;
17-
name: string;
18-
}
17+
type ListItem =
18+
| { title: string; token: string }
19+
| { label: string; value: string }
20+
| string;
21+
22+
type ListBoxItem = { label: string; id: string };
1923

2024
const messages = defineMessages({
2125
optionsLabel: {
@@ -31,7 +35,7 @@ const messages = defineMessages({
3135
interface DndListBoxProps {
3236
title: string;
3337
'aria-label': string;
34-
list: ListData<FileItem>;
38+
list: ListData<ListBoxItem>;
3539
}
3640

3741
function DndListBox(props: DndListBoxProps) {
@@ -44,7 +48,7 @@ function DndListBox(props: DndListBoxProps) {
4448
let item = list.getItem(key);
4549
return {
4650
'custom-app-type': JSON.stringify(item),
47-
'text/plain': item.name,
51+
'text/plain': item.label,
4852
};
4953
});
5054
},
@@ -111,7 +115,7 @@ function DndListBox(props: DndListBoxProps) {
111115
dragAndDropHooks={dragAndDropHooks}
112116
renderEmptyState={() => 'Drop items here'}
113117
>
114-
{(item) => <ListBoxItem>{item.name}</ListBoxItem>}
118+
{(item) => <ListBoxItem>{item.label}</ListBoxItem>}
115119
</ListBox>
116120
);
117121
}
@@ -121,52 +125,39 @@ type ListFromListProps = {
121125
title?: string;
122126
optionsTitle?: string;
123127
valuesTitle?: string;
124-
value?: FileItem[];
125-
options: FileItem[];
128+
value?: ListItem[];
129+
options: ListItem[];
126130
required?: boolean;
127-
default?: FileItem[];
131+
default?: ListItem[];
128132
'aria-label': string;
129-
onChange: (id: string, value: FileItem[]) => void;
133+
onChange: (id: string, value: ListItem[]) => void;
130134
};
131135

132136
// Returns items in `list` that do NOT exist in `sublist` (by name and type, ignoring id)
133137
function differenceByNameAndType(
134-
list: FileItem[],
135-
sublist: FileItem[],
136-
): FileItem[] {
137-
return list.filter((item) => !sublist.some((sub) => sub.name === item.name));
138+
list: ListBoxItem[],
139+
sublist: ListBoxItem[],
140+
): ListBoxItem[] {
141+
return list.filter((item) => !sublist.some((sub) => sub.id === item.id));
138142
}
139143

140-
// // Adds a unique "id" to each item if it doesn't have one, returns a new array (immutable)
141-
// function addIds<T extends { id?: string }>(
142-
// list: T[],
143-
// ): (Omit<T, 'id'> & { id: string })[] {
144-
// return list.map((item) =>
145-
// item.id
146-
// ? { ...(item as Omit<T, 'id'>), id: item.id }
147-
// : { ...(item as Omit<T, 'id'>), id: uuidv4() },
148-
// );
149-
// }
150-
151-
// // Removes the "id" property from each item, returns a new array (immutable)
152-
// function removeIds<T extends { id?: string }>(list: T[]): Omit<T, 'id'>[] {
153-
// return list.map(({ id, ...rest }) => ({ ...rest }));
154-
// }
155-
156144
function ListFromList(props: ListFromListProps) {
157145
const { options, value, onChange } = props;
158146
const intl = useIntl();
159147

160148
let optionsForDisplay = useListData({
161-
initialItems: differenceByNameAndType(options, value),
149+
initialItems: differenceByNameAndType(
150+
normalizeList(options),
151+
normalizeList(value),
152+
),
162153
});
163154

164155
let valueList = useListData({
165-
initialItems: value || [],
156+
initialItems: normalizeList(value) || [],
166157
});
167158

168159
useEffect(() => {
169-
onChange(props.id, valueList.items);
160+
onChange(props.id, denormalizeList(valueList.items));
170161
// eslint-disable-next-line react-hooks/exhaustive-deps
171162
}, [valueList.items]);
172163

@@ -176,19 +167,24 @@ function ListFromList(props: ListFromListProps) {
176167
const isSameByNameAndOrder =
177168
Array.isArray(value) &&
178169
value.length === valueList.items.length &&
179-
value.every((v, i) => v.name === valueList.items[i]?.name);
170+
normalizeList(value).every((v, i) => v.id === valueList.items[i]?.id);
180171

181172
if (!isSameByNameAndOrder) {
182173
valueList.setSelectedKeys(new Set()); // Clear selection if needed
183174
valueList.remove(...valueList.items.map((item) => item.id));
184175
if (value && value.length > 0) {
185-
valueList.append(...value);
176+
valueList.append(...normalizeList(value));
186177
}
187178
optionsForDisplay.setSelectedKeys(new Set()); // Clear selection if needed
188179
optionsForDisplay.remove(
189180
...optionsForDisplay.items.map((item) => item.id),
190181
);
191-
optionsForDisplay.append(...differenceByNameAndType(options, value));
182+
optionsForDisplay.append(
183+
...differenceByNameAndType(
184+
normalizeList(options),
185+
normalizeList(value),
186+
),
187+
);
192188
}
193189
// eslint-disable-next-line react-hooks/exhaustive-deps
194190
}, [value]);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { normalizeList, denormalizeList } from './listNormalizer';
3+
4+
describe('normalizeList', () => {
5+
it('normalizes an array of strings', () => {
6+
const input = ['foo', 'bar'];
7+
const output = normalizeList(input);
8+
expect(output).toEqual([
9+
{ label: 'foo', id: 'foo' },
10+
{ label: 'bar', id: 'bar' },
11+
]);
12+
});
13+
14+
it('normalizes an array of InputA objects', () => {
15+
const input = [
16+
{ title: 'Foo', token: 'foo' },
17+
{ title: 'Bar', token: 'bar' },
18+
];
19+
const output = normalizeList(input);
20+
expect(output).toEqual([
21+
{ label: 'Foo', id: 'foo' },
22+
{ label: 'Bar', id: 'bar' },
23+
]);
24+
});
25+
26+
it('normalizes an array of InputB objects', () => {
27+
const input = [
28+
{ label: 'Foo', value: 'foo' },
29+
{ label: 'Bar', value: 'bar' },
30+
];
31+
const output = normalizeList(input);
32+
expect(output).toEqual([
33+
{ label: 'Foo', id: 'foo' },
34+
{ label: 'Bar', id: 'bar' },
35+
]);
36+
});
37+
38+
it('normalizes a mixed array', () => {
39+
const input = [
40+
'baz',
41+
{ title: 'Foo', token: 'foo' },
42+
{ label: 'Bar', value: 'bar' },
43+
];
44+
const output = normalizeList(input);
45+
expect(output).toEqual([
46+
{ label: 'baz', id: 'baz' },
47+
{ label: 'Foo', id: 'foo' },
48+
{ label: 'Bar', id: 'bar' },
49+
]);
50+
});
51+
52+
it('throws on invalid item shape', () => {
53+
// @ts-expect-error
54+
expect(() => normalizeList([{ foo: 'bar' }])).toThrow('Invalid item shape');
55+
});
56+
});
57+
58+
describe('denormalizeList', () => {
59+
it('denormalizes a normalized array', () => {
60+
const input = [
61+
{ label: 'Foo', id: 'foo' },
62+
{ label: 'Bar', id: 'bar' },
63+
];
64+
const output = denormalizeList(input);
65+
expect(output).toEqual(['foo', 'bar']);
66+
});
67+
68+
it('denormalizes an empty array', () => {
69+
expect(denormalizeList([])).toEqual([]);
70+
});
71+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
type InputA = { title: string; token: string };
2+
type InputB = { label: string; value: string };
3+
type Normalized = { label: string; id: string };
4+
5+
export function normalizeList(
6+
arr: Array<InputA | InputB | string>,
7+
): Normalized[] {
8+
return arr.map((item) => {
9+
if (typeof item === 'string') {
10+
return { label: item, id: item };
11+
} else if ('title' in item && 'token' in item) {
12+
return { label: item.title, id: item.token };
13+
} else if ('label' in item && 'value' in item) {
14+
return { label: item.label, id: item.value };
15+
}
16+
throw new Error('Invalid item shape');
17+
});
18+
}
19+
20+
export function denormalizeList(arr: Normalized[]): string[] {
21+
return arr.map((item) => item.id);
22+
}

0 commit comments

Comments
 (0)