Skip to content

Commit

Permalink
Refactor default value for select (twentyhq#5343)
Browse files Browse the repository at this point in the history
In this PR, we are refactoring two things:
- leverage field.defaultValue for Select and MultiSelect settings form
(instead of option.isDefault)
- use quoted string (ex: "'USD'") for string default values to embrace
backend format

---------

Co-authored-by: Thaïs Guigon <guigon.thais@gmail.com>
  • Loading branch information
charlesBochet and thaisguigon committed May 10, 2024
1 parent 7728c09 commit 8590bd7
Show file tree
Hide file tree
Showing 40 changed files with 843 additions and 559 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,82 +1,41 @@
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from '~/generated-metadata/graphql';

import {
formatFieldMetadataItemInput,
getOptionValueFromLabel,
} from '../formatFieldMetadataItemInput';

describe('getOptionValueFromLabel', () => {
it('should return the option value from the label', () => {
const label = 'Example Label';
const expected = 'EXAMPLE_LABEL';

const result = getOptionValueFromLabel(label);

expect(result).toEqual(expected);
});

it('should handle labels with accents', () => {
const label = 'Éxàmplè Làbèl';
const expected = 'EXAMPLE_LABEL';

const result = getOptionValueFromLabel(label);

expect(result).toEqual(expected);
});

it('should handle labels with special characters', () => {
const label = 'Example!@#$%^&*() Label';
const expected = 'EXAMPLE_LABEL';

const result = getOptionValueFromLabel(label);

expect(result).toEqual(expected);
});

it('should handle labels with emojis', () => {
const label = '📱 Example Label';
const expected = 'EXAMPLE_LABEL';

const result = getOptionValueFromLabel(label);

expect(result).toEqual(expected);
});
});
import { formatFieldMetadataItemInput } from '../formatFieldMetadataItemInput';

describe('formatFieldMetadataItemInput', () => {
it('should format the field metadata item input correctly', () => {
const options: FieldMetadataItemOption[] = [
{
id: '1',
label: 'Option 1',
color: 'red' as const,
position: 0,
value: 'OPTION_1',
},
{
id: '2',
label: 'Option 2',
color: 'blue' as const,
position: 1,
value: 'OPTION_2',
},
];
const input = {
defaultValue: "'OPTION_1'",
label: 'Example Label',
icon: 'example-icon',
type: FieldMetadataType.Select,
description: 'Example description',
options: [
{ id: '1', label: 'Option 1', color: 'red' as const, isDefault: true },
{ id: '2', label: 'Option 2', color: 'blue' as const },
],
options,
};

const expected = {
description: 'Example description',
icon: 'example-icon',
label: 'Example Label',
name: 'exampleLabel',
options: [
{
id: '1',
label: 'Option 1',
color: 'red',
position: 0,
value: 'OPTION_1',
},
{
id: '2',
label: 'Option 2',
color: 'blue',
position: 1,
value: 'OPTION_2',
},
],
options,
defaultValue: "'OPTION_1'",
};

Expand Down Expand Up @@ -108,38 +67,37 @@ describe('formatFieldMetadataItemInput', () => {
});

it('should format the field metadata item multi select input correctly', () => {
const options: FieldMetadataItemOption[] = [
{
id: '1',
label: 'Option 1',
color: 'red' as const,
position: 0,
value: 'OPTION_1',
},
{
id: '2',
label: 'Option 2',
color: 'blue' as const,
position: 1,
value: 'OPTION_2',
},
];
const input = {
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
label: 'Example Label',
icon: 'example-icon',
type: FieldMetadataType.MultiSelect,
description: 'Example description',
options: [
{ id: '1', label: 'Option 1', color: 'red' as const, isDefault: true },
{ id: '2', label: 'Option 2', color: 'blue' as const, isDefault: true },
],
options,
};

const expected = {
description: 'Example description',
icon: 'example-icon',
label: 'Example Label',
name: 'exampleLabel',
options: [
{
id: '1',
label: 'Option 1',
color: 'red',
position: 0,
value: 'OPTION_1',
},
{
id: '2',
label: 'Option 2',
color: 'blue',
position: 1,
value: 'OPTION_2',
},
],
options,
defaultValue: ["'OPTION_1'", "'OPTION_2'"],
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,82 +1,22 @@
import toSnakeCase from 'lodash.snakecase';

import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getDefaultValueForBackend } from '@/object-metadata/utils/getDefaultValueForBackend';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { formatMetadataLabelToMetadataNameOrThrows } from '~/pages/settings/data-model/utils/format-metadata-label-to-name.util';
import { isDefined } from '~/utils/isDefined';

import { FieldMetadataOption } from '../types/FieldMetadataOption';

export const getOptionValueFromLabel = (label: string) => {
// Remove accents
const unaccentedLabel = label
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
// Remove special characters
const noSpecialCharactersLabel = unaccentedLabel.replace(
/[^a-zA-Z0-9 ]/g,
'',
);

return toSnakeCase(noSpecialCharactersLabel).toUpperCase();
};

export const formatFieldMetadataItemInput = (
input: Partial<
Pick<
FieldMetadataItem,
'type' | 'label' | 'defaultValue' | 'icon' | 'description'
'type' | 'label' | 'defaultValue' | 'icon' | 'description' | 'options'
>
> & { options?: FieldMetadataOption[] },
>,
) => {
const options = input.options as FieldMetadataOption[] | undefined;
let defaultValue = input.defaultValue;
if (input.type === FieldMetadataType.MultiSelect) {
defaultValue = options
?.filter((option) => option.isDefault)
?.map((defaultOption) => getOptionValueFromLabel(defaultOption.label));
}
if (input.type === FieldMetadataType.Select) {
const defaultOption = options?.find((option) => option.isDefault);
defaultValue = isDefined(defaultOption)
? getOptionValueFromLabel(defaultOption.label)
: undefined;
}

// Check if options has unique values
if (options !== undefined) {
// Compute the values based on the label
const values = options.map((option) =>
getOptionValueFromLabel(option.label),
);

if (new Set(values).size !== options.length) {
throw new Error(
`Options must have unique values, but contains the following duplicates ${values.join(
', ',
)}`,
);
}
}

const label = input.label?.trim();

return {
defaultValue:
isDefined(defaultValue) && input.type
? getDefaultValueForBackend(defaultValue, input.type)
: undefined,
defaultValue: input.defaultValue,
description: input.description?.trim() ?? null,
icon: input.icon,
label,
name: label ? formatMetadataLabelToMetadataNameOrThrows(label) : undefined,
options: options?.map((option, index) => ({
color: option.color,
id: option.id,
label: option.label.trim(),
position: index,
value: getOptionValueFromLabel(option.label),
})),
options: input.options,
};
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { z } from 'zod';

import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/utils/getOptionValueFromLabel';
import { themeColorSchema } from '@/ui/theme/utils/themeColorSchema';

const selectOptionSchema = z
.object({
color: themeColorSchema,
id: z.string(),
label: z.string().trim().min(1),
position: z.number(),
value: z.string(),
})
.refine((option) => option.value === getOptionValueFromLabel(option.label), {
message: 'Value does not match label',
}) satisfies z.ZodType<FieldMetadataItemOption>;

export const selectOptionsSchema = z
.array(selectOptionSchema)
.min(1)
.refine(
(options) => {
const optionIds = options.map(({ id }) => id);
return new Set(optionIds).size === options.length;
},
{
message: 'Options must have unique ids',
},
)
.refine(
(options) => {
const optionValues = options.map(({ value }) => value);
return new Set(optionValues).size === options.length;
},
{
message: 'Options must have unique values',
},
)
.refine(
(options) =>
[...options].sort().every((option, index) => option.position === index),
{
message: 'Options positions must be sequential',
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { z } from 'zod';

import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';

export const currencyCodeSchema = z.nativeEnum(CurrencyCode);

0 comments on commit 8590bd7

Please sign in to comment.