Skip to content

Commit ec8e983

Browse files
authored
Field Populator: Dialog UI and validations [MAPS-104] (#10296)
* adding sidebar UI + tests * adding first version of the Dialog location + tests * adding validations * removing unused imports * fix after rebase * fixing pr comments
1 parent d79f5ca commit ec8e983

File tree

10 files changed

+476
-11
lines changed

10 files changed

+476
-11
lines changed

apps/field-populator/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"dependencies": {
66
"@contentful/app-sdk": "4.25.0",
77
"@contentful/f36-components": "5.6.0",
8+
"@contentful/f36-multiselect": "^5.6.0",
89
"@contentful/f36-tokens": "5.1.0",
910
"@contentful/react-apps-toolkit": "1.2.16",
1011
"@testing-library/jest-dom": "^6.9.1",
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { css } from 'emotion';
2+
import tokens from '@contentful/f36-tokens';
3+
4+
export const styles = {
5+
invalid: css`
6+
button {
7+
border-color: ${tokens.red600};
8+
}
9+
`,
10+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React, { useState } from 'react';
2+
import { Box, Stack, Pill } from '@contentful/f36-components';
3+
import { Multiselect } from '@contentful/f36-multiselect';
4+
import { normalizeLocaleCode, SimplifiedLocale } from '../utils/locales';
5+
import { styles } from './LocaleMultiSelect.styles';
6+
7+
interface LocaleMultiSelectProps {
8+
availableLocales: SimplifiedLocale[];
9+
selectedLocales: SimplifiedLocale[];
10+
onSelectionChange: (locales: SimplifiedLocale[]) => void;
11+
isDisabled?: boolean;
12+
isInvalid?: boolean;
13+
}
14+
15+
const LocaleMultiSelect: React.FC<LocaleMultiSelectProps> = ({
16+
availableLocales,
17+
selectedLocales,
18+
onSelectionChange,
19+
isDisabled = false,
20+
isInvalid = false,
21+
}) => {
22+
const [filteredLocales, setFilteredLocales] = useState<SimplifiedLocale[]>(availableLocales);
23+
24+
const handleSearchValueChange = (event: { target: { value: string } }) => {
25+
const value = event.target.value;
26+
const newFilteredLocales = availableLocales.filter((locale) =>
27+
locale.name.toLowerCase().includes(value.toLowerCase())
28+
);
29+
setFilteredLocales(newFilteredLocales);
30+
};
31+
32+
const handleLocaleToggle = (locale: SimplifiedLocale, checked: boolean) => {
33+
if (checked) {
34+
onSelectionChange([...selectedLocales, locale]);
35+
} else {
36+
onSelectionChange(selectedLocales.filter((l) => l.code !== locale.code));
37+
}
38+
};
39+
40+
const handleLocaleRemove = (localeCode: string) => {
41+
onSelectionChange(selectedLocales.filter((l) => l.code !== localeCode));
42+
};
43+
44+
return (
45+
<Stack marginTop="spacingXs" flexDirection="column" alignItems="start">
46+
<Multiselect
47+
className={isInvalid ? styles.invalid : undefined}
48+
searchProps={{
49+
searchPlaceholder: 'Search locales',
50+
onSearchValueChange: handleSearchValueChange,
51+
}}
52+
placeholder="Select one or more"
53+
popoverProps={{ isFullWidth: true, listMaxHeight: 110 }}
54+
currentSelection={selectedLocales.map((l) => l.name)}
55+
triggerButtonProps={{ isDisabled }}>
56+
{filteredLocales.map((locale) => (
57+
<Multiselect.Option
58+
key={`multiselect-locale-${normalizeLocaleCode(locale.code)}`}
59+
itemId={`multiselect-locale-${normalizeLocaleCode(locale.code)}`}
60+
value={locale.code}
61+
isChecked={selectedLocales.some((l) => l.code === locale.code)}
62+
onSelectItem={(e) => handleLocaleToggle(locale, e.target.checked)}>
63+
{locale.name}
64+
</Multiselect.Option>
65+
))}
66+
</Multiselect>
67+
68+
{selectedLocales.length > 0 && (
69+
<Box width="full" overflow="auto">
70+
<Stack flexDirection="row" spacing="spacing2Xs" flexWrap="wrap">
71+
{selectedLocales.map((locale) => (
72+
<Pill
73+
key={locale.code}
74+
testId={`pill-locale-${normalizeLocaleCode(locale.code)}`}
75+
label={locale.name}
76+
isDraggable={false}
77+
onClose={() => handleLocaleRemove(locale.code)}
78+
/>
79+
))}
80+
</Stack>
81+
</Box>
82+
)}
83+
</Stack>
84+
);
85+
};
86+
87+
export default LocaleMultiSelect;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { css } from 'emotion';
2+
3+
export const styles = {
4+
container: css({
5+
minHeight: '340px',
6+
}),
7+
};
Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,101 @@
11
import { DialogAppSDK } from '@contentful/app-sdk';
2-
import { Paragraph } from '@contentful/f36-components';
3-
import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit';
2+
import { Button, Flex, Form, FormControl, Select } from '@contentful/f36-components';
3+
import { useAutoResizer, useSDK } from '@contentful/react-apps-toolkit';
4+
import { useState } from 'react';
5+
import LocaleMultiSelect from '../components/LocaleMultiSelect';
6+
import {
7+
SimplifiedLocale,
8+
mapLocaleNamesToSimplifiedLocales,
9+
normalizeLocaleCode,
10+
} from '../utils/locales';
11+
import { styles } from './Dialog.styles';
412

513
const Dialog = () => {
614
const sdk = useSDK<DialogAppSDK>();
15+
const [selectedSourceLocale, setSelectedSourceLocale] = useState<string | null>(null);
16+
const [selectedTargetLocales, setSelectedTargetLocales] = useState<SimplifiedLocale[]>([]);
17+
const [missingSourceLocale, setMissingSourceLocale] = useState(false);
18+
const [missingTargetLocales, setMissingTargetLocales] = useState(false);
719

8-
return <Paragraph>Hello Dialog Component (AppId: {sdk.ids.app})</Paragraph>;
20+
const mappedLocales = mapLocaleNamesToSimplifiedLocales(sdk.locales.names);
21+
22+
useAutoResizer();
23+
24+
const handlePopulateFields = () => {
25+
if (!selectedSourceLocale || selectedTargetLocales.length === 0) {
26+
setMissingSourceLocale(!selectedSourceLocale);
27+
setMissingTargetLocales(selectedTargetLocales.length === 0);
28+
return;
29+
}
30+
31+
// todo : copy and paste logic to populate fields
32+
33+
sdk.close();
34+
};
35+
36+
return (
37+
<Form>
38+
<Flex
39+
flexDirection="column"
40+
justifyContent="space-between"
41+
marginTop="spacingM"
42+
marginRight="spacingL"
43+
marginLeft="spacingL"
44+
marginBottom="spacingM"
45+
className={styles.container}>
46+
<Flex flexDirection="column">
47+
<FormControl isRequired isInvalid={missingSourceLocale}>
48+
<FormControl.Label>Select source locale</FormControl.Label>
49+
<Select
50+
id="source-locale"
51+
name="source-locale"
52+
testId="source-locale-select"
53+
onChange={(event) => setSelectedSourceLocale(event.target.value)}>
54+
{!selectedSourceLocale && (
55+
<Select.Option key={`select-locale-empty`} value={undefined}>
56+
Select one
57+
</Select.Option>
58+
)}
59+
{mappedLocales.map((locale) => (
60+
<Select.Option
61+
key={`select-locale-${normalizeLocaleCode(locale.code)}`}
62+
data-test-id={`select-locale-${normalizeLocaleCode(locale.code)}`}
63+
value={locale.code}>
64+
{locale.name}
65+
</Select.Option>
66+
))}
67+
</Select>
68+
{missingSourceLocale && (
69+
<FormControl.ValidationMessage>Select source locale</FormControl.ValidationMessage>
70+
)}
71+
</FormControl>
72+
<FormControl isRequired isInvalid={missingTargetLocales}>
73+
<FormControl.Label>Select target locales to populate</FormControl.Label>
74+
<LocaleMultiSelect
75+
availableLocales={mappedLocales}
76+
selectedLocales={selectedTargetLocales}
77+
onSelectionChange={setSelectedTargetLocales}
78+
isInvalid={missingTargetLocales}
79+
/>
80+
{missingTargetLocales && (
81+
<FormControl.ValidationMessage>Select target locales</FormControl.ValidationMessage>
82+
)}
83+
</FormControl>
84+
</Flex>
85+
<Flex justifyContent="flex-end" gap="spacingM">
86+
<Button
87+
onClick={() => {
88+
sdk.close();
89+
}}>
90+
Cancel
91+
</Button>
92+
<Button variant="primary" onClick={() => handlePopulateFields()}>
93+
Populate fields
94+
</Button>
95+
</Flex>
96+
</Flex>
97+
</Form>
98+
);
999
};
10100

11101
export default Dialog;

apps/field-populator/src/locations/Sidebar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const Sidebar = () => {
1111
const openDialog = async () => {
1212
return sdk.dialogs.openCurrentApp({
1313
title: APP_NAME,
14+
width: 'large',
15+
minHeight: '340px',
1416
});
1517
};
1618

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export type SimplifiedLocale = { code: string; name: string };
2+
3+
export const mapLocaleNamesToSimplifiedLocales = (
4+
localeNames: Record<string, string>
5+
): SimplifiedLocale[] => {
6+
return Object.keys(localeNames).map((key) => ({
7+
code: key,
8+
name: localeNames[key],
9+
}));
10+
};
11+
12+
export const normalizeLocaleCode = (code: string) => {
13+
return code.toLowerCase().replace(/\s/g, '-');
14+
};

0 commit comments

Comments
 (0)