mirror of
https://github.com/lingble/twenty.git
synced 2025-10-30 12:22:29 +00:00
validation on Select field (#8316)
fix #8204 I changed "API keys" to "API values". Stopped inputting special characters in Select field option keys. @lucasbordeau please check the changes and tell me if I need to do any other changes. :) --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -116,7 +116,7 @@ export const variables = {
|
|||||||
description: null,
|
description: null,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
label: 'fieldLabel',
|
label: 'fieldLabel',
|
||||||
name: 'fieldLabel',
|
name: 'fieldlabel',
|
||||||
options: undefined,
|
options: undefined,
|
||||||
settings: undefined,
|
settings: undefined,
|
||||||
objectMetadataId,
|
objectMetadataId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
export const formatFieldMetadataItemInput = (
|
export const formatFieldMetadataItemInput = (
|
||||||
input: Partial<
|
input: Partial<
|
||||||
@@ -22,7 +22,7 @@ export const formatFieldMetadataItemInput = (
|
|||||||
description: input.description?.trim() ?? null,
|
description: input.description?.trim() ?? null,
|
||||||
icon: input.icon,
|
icon: input.icon,
|
||||||
label,
|
label,
|
||||||
name: label ? computeMetadataNameFromLabelOrThrow(label) : undefined,
|
name: label ? computeMetadataNameFromLabel(label) : undefined,
|
||||||
options: input.options,
|
options: input.options,
|
||||||
settings: input.settings,
|
settings: input.settings,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { errors } from '@/settings/data-model/fields/forms/utils/errorMessages';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { METADATA_LABEL_VALID_PATTERN } from '~/pages/settings/data-model/constants/MetadataLabelValidPattern';
|
import { METADATA_LABEL_VALID_PATTERN } from '~/pages/settings/data-model/constants/MetadataLabelValidPattern';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
export const metadataLabelSchema = (existingLabels?: string[]) => {
|
export const metadataLabelSchema = (existingLabels?: string[]) => {
|
||||||
return z
|
return z
|
||||||
.string()
|
.string()
|
||||||
@@ -12,7 +12,7 @@ export const metadataLabelSchema = (existingLabels?: string[]) => {
|
|||||||
.refine(
|
.refine(
|
||||||
(label) => {
|
(label) => {
|
||||||
try {
|
try {
|
||||||
computeMetadataNameFromLabelOrThrow(label);
|
computeMetadataNameFromLabel(label);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
@@ -28,9 +28,7 @@ export const metadataLabelSchema = (existingLabels?: string[]) => {
|
|||||||
if (!existingLabels || !label?.length) {
|
if (!existingLabels || !label?.length) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return !existingLabels.includes(
|
return !existingLabels.includes(computeMetadataNameFromLabel(label));
|
||||||
computeMetadataNameFromLabelOrThrow(label),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { themeColorSchema } from 'twenty-ui';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { computeOptionValueFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
||||||
|
|
||||||
const selectOptionSchema = z
|
const selectOptionSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -15,7 +15,7 @@ const selectOptionSchema = z
|
|||||||
.refine(
|
.refine(
|
||||||
(option) => {
|
(option) => {
|
||||||
try {
|
try {
|
||||||
computeOptionValueFromLabelOrThrow(option.label);
|
computeOptionValueFromLabel(option.label);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
|||||||
color={MAIN_COLORS.yellow}
|
color={MAIN_COLORS.yellow}
|
||||||
/>
|
/>
|
||||||
</StyledIconContainer>
|
</StyledIconContainer>
|
||||||
<StyledApiKey>API keys</StyledApiKey>
|
<StyledApiKey>API values</StyledApiKey>
|
||||||
</StyledApiKeyContainer>
|
</StyledApiKeyContainer>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { v4 } from 'uuid';
|
|||||||
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { EXPANDED_WIDTH_ANIMATION_VARIANTS } from '@/settings/constants/ExpandedWidthAnimationVariants';
|
import { EXPANDED_WIDTH_ANIMATION_VARIANTS } from '@/settings/constants/ExpandedWidthAnimationVariants';
|
||||||
import { OPTION_VALUE_MAXIMUM_LENGTH } from '@/settings/data-model/constants/OptionValueMaximumLength';
|
import { OPTION_VALUE_MAXIMUM_LENGTH } from '@/settings/data-model/constants/OptionValueMaximumLength';
|
||||||
import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/select/utils/getOptionValueFromLabel';
|
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
@@ -27,6 +26,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
||||||
|
|
||||||
type SettingsDataModelFieldSelectFormOptionRowProps = {
|
type SettingsDataModelFieldSelectFormOptionRowProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -124,7 +124,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
onChange={(input) =>
|
onChange={(input) =>
|
||||||
onChange({
|
onChange({
|
||||||
...option,
|
...option,
|
||||||
value: getOptionValueFromLabel(input),
|
value: computeOptionValueFromLabel(input),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
RightIcon={isDefault ? IconCheck : undefined}
|
RightIcon={isDefault ? IconCheck : undefined}
|
||||||
@@ -162,14 +162,14 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(label) => {
|
onChange={(label) => {
|
||||||
const optionNameHasBeenEdited = !(
|
const optionNameHasBeenEdited = !(
|
||||||
option.value === getOptionValueFromLabel(option.label)
|
option.value === computeOptionValueFromLabel(option.label)
|
||||||
);
|
);
|
||||||
onChange({
|
onChange({
|
||||||
...option,
|
...option,
|
||||||
label,
|
label,
|
||||||
value: optionNameHasBeenEdited
|
value: optionNameHasBeenEdited
|
||||||
? option.value
|
? option.value
|
||||||
: getOptionValueFromLabel(label),
|
: computeOptionValueFromLabel(label),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
RightIcon={isDefault ? IconCheck : undefined}
|
RightIcon={isDefault ? IconCheck : undefined}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import {
|
|||||||
FieldMetadataItemOption,
|
FieldMetadataItemOption,
|
||||||
} from '@/object-metadata/types/FieldMetadataItem';
|
} from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
|
import { SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
|
||||||
import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/select/utils/getOptionValueFromLabel';
|
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
||||||
|
|
||||||
const DEFAULT_OPTION: FieldMetadataItemOption = {
|
const DEFAULT_OPTION: FieldMetadataItemOption = {
|
||||||
color: 'green',
|
color: 'green',
|
||||||
id: v4(),
|
id: v4(),
|
||||||
label: 'Option 1',
|
label: 'Option 1',
|
||||||
position: 0,
|
position: 0,
|
||||||
value: getOptionValueFromLabel('Option 1'),
|
value: computeOptionValueFromLabel('Option 1'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSelectSettingsFormInitialValues = ({
|
export const useSelectSettingsFormInitialValues = ({
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { getOptionValueFromLabel } from '../getOptionValueFromLabel';
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -3,7 +3,7 @@ import { v4 } from 'uuid';
|
|||||||
|
|
||||||
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { generateNewSelectOptionLabel } from '@/settings/data-model/fields/forms/select/utils/generateNewSelectOptionLabel';
|
import { generateNewSelectOptionLabel } from '@/settings/data-model/fields/forms/select/utils/generateNewSelectOptionLabel';
|
||||||
import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/select/utils/getOptionValueFromLabel';
|
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
||||||
|
|
||||||
export const generateNewSelectOption = (
|
export const generateNewSelectOption = (
|
||||||
options: FieldMetadataItemOption[],
|
options: FieldMetadataItemOption[],
|
||||||
@@ -15,6 +15,6 @@ export const generateNewSelectOption = (
|
|||||||
id: v4(),
|
id: v4(),
|
||||||
label: newOptionLabel,
|
label: newOptionLabel,
|
||||||
position: options.length,
|
position: options.length,
|
||||||
value: getOptionValueFromLabel(newOptionLabel),
|
value: computeOptionValueFromLabel(newOptionLabel),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import snakeCase from 'lodash.snakecase';
|
|
||||||
|
|
||||||
import { computeOptionValueFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
|
||||||
|
|
||||||
export const getOptionValueFromLabel = (label: string) => {
|
|
||||||
let transliteratedLabel = label;
|
|
||||||
try {
|
|
||||||
transliteratedLabel = computeOptionValueFromLabelOrThrow(label);
|
|
||||||
} catch (error) {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
return snakeCase(transliteratedLabel).toUpperCase();
|
|
||||||
};
|
|
||||||
@@ -29,7 +29,7 @@ import isEmpty from 'lodash.isempty';
|
|||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
|
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
const objectEditFormSchema = z
|
const objectEditFormSchema = z
|
||||||
.object({})
|
.object({})
|
||||||
@@ -93,16 +93,14 @@ export const ObjectSettings = ({ objectMetadataItem }: ObjectSettingsProps) => {
|
|||||||
...values,
|
...values,
|
||||||
...(values.labelSingular && dirtyFieldKeys.includes('labelSingular')
|
...(values.labelSingular && dirtyFieldKeys.includes('labelSingular')
|
||||||
? {
|
? {
|
||||||
nameSingular: computeMetadataNameFromLabelOrThrow(
|
nameSingular: computeMetadataNameFromLabel(
|
||||||
formValues.labelSingular,
|
formValues.labelSingular,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(values.labelPlural && dirtyFieldKeys.includes('labelPlural')
|
...(values.labelPlural && dirtyFieldKeys.includes('labelPlural')
|
||||||
? {
|
? {
|
||||||
namePlural: computeMetadataNameFromLabelOrThrow(
|
namePlural: computeMetadataNameFromLabel(formValues.labelPlural),
|
||||||
formValues.labelPlural,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
TooltipDelay,
|
TooltipDelay,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const settingsDataModelObjectAboutFormSchema = objectMetadataItemSchema
|
export const settingsDataModelObjectAboutFormSchema = objectMetadataItemSchema
|
||||||
@@ -144,16 +144,14 @@ export const SettingsDataModelObjectAboutForm = ({
|
|||||||
|
|
||||||
const fillNameSingularFromLabelSingular = (labelSingular: string) => {
|
const fillNameSingularFromLabelSingular = (labelSingular: string) => {
|
||||||
isDefined(labelSingular) &&
|
isDefined(labelSingular) &&
|
||||||
setValue(
|
setValue('nameSingular', computeMetadataNameFromLabel(labelSingular), {
|
||||||
'nameSingular',
|
shouldDirty: true,
|
||||||
computeMetadataNameFromLabelOrThrow(labelSingular),
|
});
|
||||||
{ shouldDirty: true },
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillNamePluralFromLabelPlural = (labelPlural: string) => {
|
const fillNamePluralFromLabelPlural = (labelPlural: string) => {
|
||||||
isDefined(labelPlural) &&
|
isDefined(labelPlural) &&
|
||||||
setValue('namePlural', computeMetadataNameFromLabelOrThrow(labelPlural), {
|
setValue('namePlural', computeMetadataNameFromLabel(labelPlural), {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
import { settingsDataModelObjectAboutFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
|
||||||
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
import { CreateObjectInput } from '~/generated-metadata/graphql';
|
||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
export const settingsCreateObjectInputSchema =
|
export const settingsCreateObjectInputSchema =
|
||||||
settingsDataModelObjectAboutFormSchema.transform<CreateObjectInput>(
|
settingsDataModelObjectAboutFormSchema.transform<CreateObjectInput>(
|
||||||
@@ -8,10 +8,9 @@ export const settingsCreateObjectInputSchema =
|
|||||||
...values,
|
...values,
|
||||||
nameSingular:
|
nameSingular:
|
||||||
values.nameSingular ??
|
values.nameSingular ??
|
||||||
computeMetadataNameFromLabelOrThrow(values.labelSingular),
|
computeMetadataNameFromLabel(values.labelSingular),
|
||||||
namePlural:
|
namePlural:
|
||||||
values.namePlural ??
|
values.namePlural ?? computeMetadataNameFromLabel(values.labelPlural),
|
||||||
computeMetadataNameFromLabelOrThrow(values.labelPlural),
|
|
||||||
isLabelSyncedWithName: values.isLabelSyncedWithName ?? true,
|
isLabelSyncedWithName: values.isLabelSyncedWithName ?? true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const METADATA_LABEL_VALID_PATTERN = /^[^0-9].*$/;
|
export const METADATA_LABEL_VALID_PATTERN = /^.*$/;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const OPTION_VALUE_VALID_PATTERN = /^[a-zA-Z0-9]+$/;
|
export const OPTION_VALUE_VALID_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
||||||
|
|||||||
@@ -1,27 +1,9 @@
|
|||||||
import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
import { computeMetadataNameFromLabel } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils';
|
||||||
|
|
||||||
describe('computeMetadataNameFromLabel', () => {
|
describe('computeMetadataNameFromLabel', () => {
|
||||||
it('throws if empty label', () => {
|
|
||||||
const label = ' ';
|
|
||||||
|
|
||||||
expect(() => computeMetadataNameFromLabelOrThrow(label)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('computes name for 1 char long label', () => {
|
|
||||||
const label = 'a';
|
|
||||||
|
|
||||||
expect(computeMetadataNameFromLabelOrThrow(label)).toEqual('a');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if label starts with digits', () => {
|
|
||||||
const label = '1string';
|
|
||||||
|
|
||||||
expect(() => computeMetadataNameFromLabelOrThrow(label)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('computes name for label with non-latin char', () => {
|
it('computes name for label with non-latin char', () => {
|
||||||
const label = 'λλλ!';
|
const label = 'λλλ!';
|
||||||
|
|
||||||
expect(computeMetadataNameFromLabelOrThrow(label)).toEqual('lll');
|
expect(computeMetadataNameFromLabel(label)).toEqual('lll');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { computeOptionValueFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
|
||||||
|
|
||||||
describe('computeOptionValueFromLabel', () => {
|
|
||||||
it('throws if empty label', () => {
|
|
||||||
const label = ' ';
|
|
||||||
|
|
||||||
expect(() => computeOptionValueFromLabelOrThrow(label)).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('computes name for 1 char long label', () => {
|
|
||||||
const label = 'a';
|
|
||||||
|
|
||||||
expect(computeOptionValueFromLabelOrThrow(label)).toEqual('a');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('compute name if starts with digits', () => {
|
|
||||||
const label = '1';
|
|
||||||
|
|
||||||
expect(computeOptionValueFromLabelOrThrow(label)).toEqual('1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('computes name for label with non-latin char', () => {
|
|
||||||
const label = 'λλλ';
|
|
||||||
|
|
||||||
expect(computeOptionValueFromLabelOrThrow(label)).toEqual('lll');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
import { METADATA_NAME_VALID_PATTERN } from '~/pages/settings/data-model/constants/MetadataNameValidPattern';
|
import camelCase from 'lodash.camelcase';
|
||||||
import { transliterateAndFormatOrThrow } from '~/pages/settings/data-model/utils/transliterate-and-format.utils';
|
import { slugify } from 'transliteration';
|
||||||
|
|
||||||
export const computeMetadataNameFromLabelOrThrow = (label: string): string => {
|
export const computeMetadataNameFromLabel = (label: string): string => {
|
||||||
if (label === '') {
|
const prefixedLabel = /^\d/.test(label) ? `n${label}` : label;
|
||||||
|
|
||||||
|
if (prefixedLabel === '') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return transliterateAndFormatOrThrow(label, METADATA_NAME_VALID_PATTERN);
|
|
||||||
|
const formattedString = slugify(prefixedLabel, {
|
||||||
|
trim: true,
|
||||||
|
separator: '_',
|
||||||
|
allowedChars: 'a-zA-Z0-9',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formattedString === '') {
|
||||||
|
throw new Error('Invalid label');
|
||||||
|
}
|
||||||
|
|
||||||
|
return camelCase(formattedString);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { OPTION_VALUE_VALID_PATTERN } from '~/pages/settings/data-model/constants/OptionValueValidPattern';
|
import { slugify } from 'transliteration';
|
||||||
import { transliterateAndFormatOrThrow } from '~/pages/settings/data-model/utils/transliterate-and-format.utils';
|
|
||||||
|
|
||||||
export const computeOptionValueFromLabelOrThrow = (label: string): string => {
|
export const computeOptionValueFromLabel = (label: string): string => {
|
||||||
return transliterateAndFormatOrThrow(label, OPTION_VALUE_VALID_PATTERN);
|
const prefixedLabel = /^\d/.test(label) ? `OPT${label}` : label;
|
||||||
|
|
||||||
|
const formattedString = slugify(prefixedLabel, {
|
||||||
|
trim: true,
|
||||||
|
separator: '_',
|
||||||
|
allowedChars: 'a-zA-Z0-9_',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formattedString === '') {
|
||||||
|
throw new Error('Invalid label');
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedString.toUpperCase();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import toCamelCase from 'lodash.camelcase';
|
|
||||||
import { slugify, transliterate } from 'transliteration';
|
|
||||||
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const transliterateAndFormatOrThrow = (
|
|
||||||
string: string,
|
|
||||||
validStringPattern: RegExp,
|
|
||||||
): string => {
|
|
||||||
let formattedString = string;
|
|
||||||
|
|
||||||
if (isDefined(formattedString.match(validStringPattern))) {
|
|
||||||
return toCamelCase(formattedString);
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedString = toCamelCase(
|
|
||||||
slugify(transliterate(formattedString, { trim: true })),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!formattedString.match(validStringPattern)) {
|
|
||||||
throw new Error(`"${string}" is not a valid name`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedString;
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import toCamelCase from 'lodash.camelcase';
|
import { slugify } from 'transliteration';
|
||||||
import { slugify, transliterate } from 'transliteration';
|
|
||||||
|
|
||||||
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
import { CreateObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/create-object.input';
|
||||||
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
import { UpdateObjectPayload } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input';
|
||||||
@@ -63,30 +62,6 @@ export const validateObjectMetadataInputOrThrow = <
|
|||||||
validateNameIsNotTooLongThrow(objectMetadataInput.namePlural);
|
validateNameIsNotTooLongThrow(objectMetadataInput.namePlural);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const transliterateAndFormatOrThrow = (string?: string): string => {
|
|
||||||
if (!string) {
|
|
||||||
throw new ObjectMetadataException(
|
|
||||||
'Name is required',
|
|
||||||
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let formattedString = string;
|
|
||||||
|
|
||||||
if (formattedString.match(METADATA_NAME_VALID_PATTERN) !== null) {
|
|
||||||
return toCamelCase(formattedString);
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedString = toCamelCase(
|
|
||||||
slugify(transliterate(formattedString, { trim: true })),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!formattedString.match(METADATA_NAME_VALID_PATTERN)) {
|
|
||||||
throw new Error(`"${string}" is not a valid name`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return formattedString;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateNameIsNotReservedKeywordOrThrow = (name?: string) => {
|
const validateNameIsNotReservedKeywordOrThrow = (name?: string) => {
|
||||||
if (name) {
|
if (name) {
|
||||||
if (reservedKeywords.includes(name)) {
|
if (reservedKeywords.includes(name)) {
|
||||||
@@ -133,17 +108,41 @@ const validateNameCharactersOrThrow = (name?: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const computeMetadataNameFromLabelOrThrow = (label: string): string => {
|
export const computeMetadataNameFromLabel = (label: string): string => {
|
||||||
const formattedString = transliterateAndFormatOrThrow(label);
|
if (!label) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'Label is required',
|
||||||
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return formattedString;
|
const prefixedLabel = /^\d/.test(label) ? `n${label}` : label;
|
||||||
|
|
||||||
|
if (prefixedLabel === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedString = slugify(prefixedLabel, {
|
||||||
|
trim: true,
|
||||||
|
separator: '_',
|
||||||
|
allowedChars: 'a-zA-Z0-9',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (formattedString === '') {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
`Invalid label: "${label}"`,
|
||||||
|
ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return camelCase(formattedString);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateNameAndLabelAreSyncOrThrow = (
|
export const validateNameAndLabelAreSyncOrThrow = (
|
||||||
label: string,
|
label: string,
|
||||||
name: string,
|
name: string,
|
||||||
) => {
|
) => {
|
||||||
const computedName = computeMetadataNameFromLabelOrThrow(label);
|
const computedName = computeMetadataNameFromLabel(label);
|
||||||
|
|
||||||
if (name !== computedName) {
|
if (name !== computedName) {
|
||||||
throw new ObjectMetadataException(
|
throw new ObjectMetadataException(
|
||||||
|
|||||||
@@ -43,12 +43,11 @@ const StyledTag = styled.h3<{
|
|||||||
border: ${({ variant, theme }) =>
|
border: ${({ variant, theme }) =>
|
||||||
variant === 'outline' || variant === 'border'
|
variant === 'outline' || variant === 'border'
|
||||||
? `1px ${variant === 'border' ? 'solid' : 'dashed'} ${theme.border.color.strong}`
|
? `1px ${variant === 'border' ? 'solid' : 'dashed'} ${theme.border.color.strong}`
|
||||||
: ''};
|
: 'none'};
|
||||||
|
|
||||||
gap: ${spacing1};
|
gap: ${spacing1};
|
||||||
|
|
||||||
min-width: ${({ preventShrink }) =>
|
min-width: ${({ preventShrink }) => (preventShrink ? 'fit-content' : 'none')};
|
||||||
preventShrink ? 'fit-content' : 'none;'};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledContent = styled.span`
|
const StyledContent = styled.span`
|
||||||
|
|||||||
Reference in New Issue
Block a user