Skip to content

Commit 7dda10f

Browse files
feat: add description and placeholder to additional fields (#582)
* feat: add description and placeholder to the additional fields * feat: refresh lockfile * chore: test coverage
1 parent 8012de2 commit 7dda10f

File tree

16 files changed

+1080
-1099
lines changed

16 files changed

+1080
-1099
lines changed

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,33 @@ Config for this plugin is stored as a part of the `config/plugins.{js|ts}` or `c
176176

177177
It is advised to configure additional fields through the plugin's Settings Page. There you can find the table of custom fields and toggle input for the audience field. When enabled, the audience field can be customized through the content manager. Custom fields can be added, edited, toggled, and removed with the use of the table provided on the Settings Page. When removing custom fields be advised that their values in navigation items will be lost. Disabling the custom fields will not affect the data and can be done with no consequence of loosing information.
178178

179-
Creating configuration for additional fields with the `config.(js|ts)` file should be done with caution. Config object contains the `additionalFields` property of type `Array<CustomField | 'audience'>`, where CustomField is of type `{ type: 'string' | 'boolean' | 'media', name: string, label: string, enabled?: boolean }`. When creating custom fields be advised that the `name` property has to be unique. When editing a custom field it is advised not to edit its `name` and `type` properties. After config has been restored the custom fields that are not present in `config.js` file will be deleted and their values in navigation items will be lost.
179+
Creating configuration for additional fields with the `config.(js|ts)` file should be done with caution. Config object contains the `additionalFields` property of type `Array<CustomField | 'audience'>`, where CustomField is of type
180+
181+
```ts
182+
type CustomField =
183+
| {
184+
type: 'string' | 'boolean' | 'media';
185+
name: string;
186+
label: string;
187+
description?: string;
188+
placeholder?: string;
189+
required?: boolean;
190+
enabled?: boolean;
191+
}
192+
| {
193+
type: 'select';
194+
name: string;
195+
label: string;
196+
description?: string;
197+
placeholder?: string;
198+
multi: boolean;
199+
options: string[];
200+
required?: boolean;
201+
enabled?: boolean;
202+
};
203+
```
204+
205+
When creating custom fields be advised that the `name` property has to be unique. When editing a custom field it is advised not to edit its `name` and `type` properties. After config has been restored the custom fields that are not present in `config.js` file will be deleted and their values in navigation items will be lost.
180206

181207
## 🔧 GQL Configuration
182208

admin/src/api/validators.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,13 @@ export const navigationSchema = z.object({
5959
});
6060

6161
const navigationCustomFieldBase = z.object({
62-
// TODO: Proper message translation
63-
name: z.string().refine((current) => !current.includes(' '), { message: 'No space allowed' }),
64-
label: z.string(),
62+
name: z
63+
.string({ required_error: 'requiredError' })
64+
.nonempty('requiredError')
65+
.refine((current) => !current.includes(' '), { message: 'noSpaceError' }),
66+
label: z.string({ required_error: 'requiredError' }).nonempty('requiredError'),
67+
description: z.string().optional(),
68+
placeholder: z.string().optional(),
6569
required: z.boolean().optional(),
6670
enabled: z.boolean().optional(),
6771
});
@@ -70,7 +74,9 @@ export type NavigationItemCustomFieldSelect = z.infer<typeof navigationItemCusto
7074
const navigationItemCustomFieldSelect = navigationCustomFieldBase.extend({
7175
type: z.literal('select'),
7276
multi: z.boolean(),
73-
options: z.array(z.string()),
77+
options: z
78+
.array(z.string(), { required_error: 'requiredError' })
79+
.min(1, { message: 'requiredError' }),
7480
});
7581

7682
export type NavigationItemCustomFieldPrimitive = z.infer<typeof navigationItemCustomFieldPrimitive>;
@@ -88,9 +94,11 @@ const navigationItemCustomFieldMedia = navigationCustomFieldBase.extend({
8894
});
8995

9096
export type NavigationItemCustomField = z.infer<typeof navigationItemCustomField>;
91-
export const navigationItemCustomField = navigationItemCustomFieldPrimitive
92-
.or(navigationItemCustomFieldMedia)
93-
.or(navigationItemCustomFieldSelect);
97+
export const navigationItemCustomField = z.discriminatedUnion('type', [
98+
navigationItemCustomFieldPrimitive,
99+
navigationItemCustomFieldMedia,
100+
navigationItemCustomFieldSelect,
101+
]);
94102

95103
export type NavigationItemAdditionalField = z.infer<typeof navigationItemAdditionalField>;
96104
export const navigationItemAdditionalField = z.union([

admin/src/pages/HomePage/components/AdditionalFieldInput/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const AdditionalFieldInput: React.FC<AdditionalFieldInputProps> = ({
6363
id: field.name,
6464
name: name || field.name,
6565
disabled: isLoading || disabled,
66+
placeholder: field.placeholder,
6667
}),
6768
[field, isLoading]
6869
);

admin/src/pages/HomePage/components/NavigationItemForm/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,8 @@ export const NavigationItemForm: React.FC<NavigationItemFormProps> = ({
833833
<Field
834834
name={`additionalFields.${additionalField.name}`}
835835
label={additionalField.label}
836+
hint={additionalField.description}
837+
required={additionalField.required}
836838
error={renderError(`additionalFields.${additionalField.name}`)}
837839
>
838840
<AdditionalFieldInput

admin/src/pages/SettingsPage/components/CustomFieldForm/index.tsx

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { get, isNil, isObject, isString, set } from 'lodash';
2828
const tradPrefix = 'pages.settings.form.customFields.popup.';
2929

3030
interface ICustomFieldFormProps {
31-
customField: Partial<NavigationItemCustomField>;
31+
customField: NavigationItemCustomField | null;
3232
isEditForm: boolean;
3333
onSubmit: Effect<NavigationItemCustomField>;
3434
onClose: VoidEffect;
@@ -59,9 +59,14 @@ const CustomFieldForm: React.FC<ICustomFieldFormProps> = ({
5959

6060
const { formatMessage } = useIntl();
6161

62-
const [formValue, setFormValue] = useState<NavigationItemCustomField>(
63-
{} as NavigationItemCustomField
64-
);
62+
const [formValue, setFormValue] = useState<NavigationItemCustomField>({
63+
name: '',
64+
label: '',
65+
type: 'string',
66+
required: false,
67+
multi: false,
68+
enabled: true,
69+
});
6570
const [formError, setFormError] = useState<FormItemErrorSchema<NavigationItemCustomField>>();
6671

6772
const { type } = formValue;
@@ -70,7 +75,7 @@ const CustomFieldForm: React.FC<ICustomFieldFormProps> = ({
7075
if (customField) {
7176
setFormValue({
7277
...customField,
73-
} as NavigationItemCustomField);
78+
});
7479
}
7580
}, [customField]);
7681

@@ -111,16 +116,13 @@ const CustomFieldForm: React.FC<ICustomFieldFormProps> = ({
111116
const renderError = (error: string): string | undefined => {
112117
const errorOccurence = get(formError, error);
113118
if (errorOccurence) {
114-
return formatMessage(getTrad(error));
119+
return formatMessage(getTrad(`${tradPrefix}${error}.${errorOccurence}`));
115120
}
116121
return undefined;
117122
};
118123

119124
const submit = (e: React.MouseEvent, values: NavigationItemCustomField) => {
120-
const { success, data, error } = navigationItemCustomField.safeParse({
121-
...values,
122-
enabled: true,
123-
});
125+
const { success, data, error } = navigationItemCustomField.safeParse(values);
124126
if (success) {
125127
onSubmit(data);
126128
} else if (error) {
@@ -148,6 +150,7 @@ const CustomFieldForm: React.FC<ICustomFieldFormProps> = ({
148150
label={formatMessage(getTrad(`${tradPrefix}name.label`))}
149151
hint={formatMessage(getTrad(`${tradPrefix}name.description`))}
150152
error={renderError('name')}
153+
required
151154
>
152155
<TextInput
153156
name="name"
@@ -168,6 +171,7 @@ const CustomFieldForm: React.FC<ICustomFieldFormProps> = ({
168171
label={formatMessage(getTrad(`${tradPrefix}label.label`))}
169172
hint={formatMessage(getTrad(`${tradPrefix}label.description`))}
170173
error={renderError('label')}
174+
required
171175
>
172176
<TextInput
173177
name="label"
@@ -181,11 +185,50 @@ const CustomFieldForm: React.FC<ICustomFieldFormProps> = ({
181185
/>
182186
</Field>
183187
</Grid.Item>
188+
<Grid.Item key="description" col={12}>
189+
<Field
190+
name="description"
191+
label={formatMessage(getTrad(`${tradPrefix}description.label`))}
192+
hint={formatMessage(getTrad(`${tradPrefix}description.description`))}
193+
error={renderError('description')}
194+
>
195+
<TextInput
196+
name="description"
197+
value={values.description}
198+
onChange={(eventOrPath: FormChangeEvent, value?: any) =>
199+
handleChange(eventOrPath, value, onChange)
200+
}
201+
placeholder={formatMessage(getTrad(`${tradPrefix}description.placeholder`))}
202+
type="string"
203+
width="100%"
204+
/>
205+
</Field>
206+
</Grid.Item>
207+
<Grid.Item key="placeholder" col={12}>
208+
<Field
209+
name="placeholder"
210+
label={formatMessage(getTrad(`${tradPrefix}placeholder.label`))}
211+
hint={formatMessage(getTrad(`${tradPrefix}placeholder.description`))}
212+
error={renderError('placeholder')}
213+
>
214+
<TextInput
215+
name="placeholder"
216+
value={values.placeholder}
217+
onChange={(eventOrPath: FormChangeEvent, value?: any) =>
218+
handleChange(eventOrPath, value, onChange)
219+
}
220+
placeholder={formatMessage(getTrad(`${tradPrefix}placeholder.placeholder`))}
221+
type="string"
222+
width="100%"
223+
/>
224+
</Field>
225+
</Grid.Item>
184226
<Grid.Item key="type" col={12}>
185227
<Field
186228
name="type"
187229
label={formatMessage(getTrad(`${tradPrefix}type.label`))}
188230
hint={formatMessage(getTrad(`${tradPrefix}type.description`))}
231+
required
189232
>
190233
<SingleSelect
191234
name="type"

admin/src/pages/SettingsPage/components/CustomFieldModal/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Modal, Typography } from '@strapi/design-system';
22
import React from 'react';
33

4-
import { pick } from 'lodash';
54
import { useIntl } from 'react-intl';
65
import { NavigationItemCustomField } from '../../../../schemas';
76
import { getTrad } from '../../../../translations';
@@ -53,7 +52,7 @@ const CustomFieldModal: React.FC<ICustomFieldModalProps> = ({
5352
</Modal.Header>
5453
<CustomFieldForm
5554
isEditForm={isEditMode}
56-
customField={pick(data, 'name', 'label', 'type', 'required', 'options', 'multi')}
55+
customField={data}
5756
onSubmit={onSubmit}
5857
onClose={onClose}
5958
/>

admin/src/schemas/config.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { z } from 'zod';
22

33
export type NavigationItemCustomFieldBase = z.infer<typeof navigationCustomFieldBase>;
44
const navigationCustomFieldBase = z.object({
5-
// TODO: Proper message translation
6-
name: z.string().refine((current) => !current.includes(' '), { message: 'No space allowed' }),
7-
label: z.string(),
5+
name: z
6+
.string({ required_error: 'requiredError' })
7+
.nonempty('requiredError')
8+
.refine((current) => !current.includes(' '), { message: 'noSpaceError' }),
9+
label: z.string({ required_error: 'requiredError' }).nonempty('requiredError'),
10+
description: z.string().optional(),
11+
placeholder: z.string().optional(),
812
required: z.boolean().optional(),
913
enabled: z.boolean().optional(),
1014
});
@@ -13,7 +17,9 @@ export type NavigationItemCustomFieldSelect = z.infer<typeof navigationItemCusto
1317
const navigationItemCustomFieldSelect = navigationCustomFieldBase.extend({
1418
type: z.literal('select'),
1519
multi: z.boolean(),
16-
options: z.array(z.string()),
20+
options: z
21+
.array(z.string(), { required_error: 'requiredError' })
22+
.min(1, { message: 'requiredError' }),
1723
});
1824

1925
export type NavigationItemCustomFieldPrimitive = z.infer<typeof navigationItemCustomFieldPrimitive>;
@@ -31,7 +37,7 @@ const navigationItemCustomFieldMedia = navigationCustomFieldBase.extend({
3137
});
3238

3339
export type NavigationItemCustomField = z.infer<typeof navigationItemCustomField>;
34-
export const navigationItemCustomField = z.union([
40+
export const navigationItemCustomField = z.discriminatedUnion('type', [
3541
navigationItemCustomFieldPrimitive,
3642
navigationItemCustomFieldMedia,
3743
navigationItemCustomFieldSelect,

admin/src/translations/ca.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,12 +354,26 @@ const ca = {
354354
label: 'Nom du champ personnalisé',
355355
placeholder: 'exemple_nom',
356356
description: 'Le nom du champ personnalisé doit être unique',
357+
requiredError: 'El nom és obligatori',
358+
noSpaceError: "No s'admeten espais",
357359
},
358360
label: {
359361
label: 'Étiquette du champ personnalisé',
360362
placeholder: "Exemple d'étiquette",
361363
description:
362364
"Cette étiquette sera affichée sur le formulaire de l'élément de navigation",
365+
requiredError: "L'etiqueta és obligatòria",
366+
},
367+
description: {
368+
label: 'Descripció del camp personalitzat',
369+
placeholder: 'Exemple de descripció',
370+
description: 'Aquesta descripció es mostrarà sota el camp com a pista o explicació',
371+
},
372+
placeholder: {
373+
label: 'Marcador de posició del camp personalitzat',
374+
placeholder: 'Exemple de marcador de posició',
375+
description:
376+
'Aquest text de marcador de posició apareixerà dins del camp abans de la interacció',
363377
},
364378
type: {
365379
label: 'Type de champ personnalisé',
@@ -373,6 +387,7 @@ const ca = {
373387
label: "Options pour l'entrée de sélection",
374388
description:
375389
'Activer ce champ ne changera pas les éléments de navigation déjà existants',
390+
requiredError: 'Cal almenys una opció',
376391
},
377392
multi: {
378393
label: "Activer l'entrée de plusieurs options",

admin/src/translations/en.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,11 +355,26 @@ const en = {
355355
label: 'Custom field name',
356356
placeholder: 'example_name',
357357
description: 'Name of the custom field must be unique',
358+
requiredError: 'Name is required',
359+
noSpaceError: 'No space allowed',
358360
},
359361
label: {
360362
label: 'Custom field label',
361363
placeholder: 'Example label',
362364
description: 'This label will be displayed on navigation item form',
365+
requiredError: 'Label is required',
366+
},
367+
description: {
368+
label: 'Custom field description',
369+
placeholder: 'Example description',
370+
description:
371+
'This description will be displayed under the field as a hint or explenation',
372+
},
373+
placeholder: {
374+
label: 'Custom field placeholder',
375+
placeholder: 'Example placeholder',
376+
description:
377+
'This placeholder text will appear inside the field before the interaction',
363378
},
364379
type: {
365380
label: 'Custom field type',
@@ -372,6 +387,7 @@ const en = {
372387
options: {
373388
label: 'Options for select input',
374389
description: 'Provide options separated by ";"',
390+
requiredError: 'At least one option is required',
375391
},
376392
multi: {
377393
label: 'Enable multiple options input',

admin/src/translations/es.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,24 @@ const en = {
358358
label: 'Nombre del campo personalizado',
359359
placeholder: 'ejemplo_nombre',
360360
description: 'El nombre del campo personalizado debe ser único',
361+
requiredError: 'El nombre es obligatorio',
362+
noSpaceError: 'No se permiten espacios',
361363
},
362364
label: {
363365
label: 'Etiqueta del campo personalizado',
364366
placeholder: 'Ejemplo de etiqueta',
365367
description: 'Esta etiqueta se mostrará en el formulario del elemento de navegación',
368+
requiredError: 'La etiqueta es obligatoria',
369+
},
370+
description: {
371+
label: 'Descripción del campo personalizado',
372+
placeholder: 'Ejemplo de descripción',
373+
description: 'Esta descripción se mostrará debajo del campo como pista o explicación',
374+
},
375+
placeholder: {
376+
label: 'Marcador de posición del campo personalizado',
377+
placeholder: 'Ejemplo de marcador de posición',
378+
description: 'Este texto aparecerá dentro del campo antes de la interacción',
366379
},
367380
type: {
368381
label: 'Tipo de campo personalizado',
@@ -376,6 +389,7 @@ const en = {
376389
options: {
377390
label: 'Opciones para la entrada de selección',
378391
description: 'Proporciona opciones separadas por ";"',
392+
requiredError: 'Se requiere al menos una opción',
379393
},
380394
multi: {
381395
label: 'Habilitar entrada de múltiples opciones',

0 commit comments

Comments
 (0)