Skip to content

Commit c0127f4

Browse files
authored
feat: simplify field preview logic in Settings (twentyhq#5541)
Closes twentyhq#5382 TODO: - [x] Test all field previews in app - [x] Fix tests - [x] Fix JSON preview
1 parent c772a79 commit c0127f4

33 files changed

+1182
-508
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
"uuid": "^9.0.0",
187187
"vite-tsconfig-paths": "^4.2.1",
188188
"xlsx-ugnis": "^0.19.3",
189-
"zod": "^3.22.2"
189+
"zod": "3.23.8"
190190
},
191191
"devDependencies": {
192192
"@babel/core": "^7.14.5",

packages/twenty-chrome-extension/src/manifest.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default defineManifest({
4646

4747
permissions: ['activeTab', 'storage', 'identity', 'sidePanel', 'cookies'],
4848

49-
// setting host permissions to all http connections will allow
49+
// setting host permissions to all http connections will allow
5050
// for people who host on their custom domain to get access to
5151
// extension instead of white listing individual urls
5252
host_permissions: ['https://*/*', 'http://*/*'],

packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { isString } from '@sniptt/guards';
2+
13
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
24
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
35
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
@@ -26,8 +28,11 @@ import { isFieldSelectValue } from '@/object-record/record-field/types/guards/is
2628
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
2729
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
2830
import { isDefined } from '~/utils/isDefined';
31+
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
2932

30-
const isValueEmpty = (value: unknown) => !isDefined(value) || value === '';
33+
const isValueEmpty = (value: unknown) =>
34+
!isDefined(value) ||
35+
(isString(value) && stripSimpleQuotesFromString(value) === '');
3136

3237
export const isFieldValueEmpty = ({
3338
fieldDefinition,
@@ -78,7 +83,8 @@ export const isFieldValueEmpty = ({
7883
if (isFieldFullName(fieldDefinition)) {
7984
return (
8085
!isFieldFullNameValue(fieldValue) ||
81-
isValueEmpty(fieldValue?.firstName + fieldValue?.lastName)
86+
(isValueEmpty(fieldValue?.firstName) &&
87+
isValueEmpty(fieldValue?.lastName))
8288
);
8389
}
8490

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { z } from 'zod';
2+
3+
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
4+
import { currencyCodeSchema } from '@/object-record/record-field/validation-schemas/currencyCodeSchema';
5+
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
6+
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';
7+
8+
export const currencyFieldDefaultValueSchema = z.object({
9+
amountMicros: z.number().nullable(),
10+
currencyCode: simpleQuotesStringSchema.refine(
11+
(value): value is `'${CurrencyCode}'` =>
12+
currencyCodeSchema.safeParse(stripSimpleQuotesFromString(value)).success,
13+
{ message: 'String is not a valid currencyCode' },
14+
),
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { z } from 'zod';
2+
3+
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
4+
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
5+
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';
6+
7+
export const multiSelectFieldDefaultValueSchema = (
8+
options?: FieldMetadataItemOption[],
9+
) => {
10+
if (!options?.length) return z.array(simpleQuotesStringSchema).nullable();
11+
12+
const optionValues = options.map(({ value }) => value);
13+
14+
return z
15+
.array(
16+
simpleQuotesStringSchema.refine(
17+
(value) => optionValues.includes(stripSimpleQuotesFromString(value)),
18+
{
19+
message: `String is not a valid multi-select option, available options are: ${options.join(
20+
', ',
21+
)}`,
22+
},
23+
),
24+
)
25+
.nullable();
26+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
2+
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
3+
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';
4+
5+
export const selectFieldDefaultValueSchema = (
6+
options?: FieldMetadataItemOption[],
7+
) => {
8+
if (!options?.length) return simpleQuotesStringSchema.nullable();
9+
10+
const optionValues = options.map(({ value }) => value);
11+
12+
return simpleQuotesStringSchema
13+
.refine(
14+
(value) => optionValues.includes(stripSimpleQuotesFromString(value)),
15+
{
16+
message: `String is not a valid select option, available options are: ${options.join(
17+
', ',
18+
)}`,
19+
},
20+
)
21+
.nullable();
22+
};

packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ export type SettingsFieldTypeConfig = {
3232
defaultValue?: unknown;
3333
};
3434

35-
export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
36-
SettingsSupportedFieldType,
37-
SettingsFieldTypeConfig
38-
> = {
35+
export const SETTINGS_FIELD_TYPE_CONFIGS = {
3936
[FieldMetadataType.Uuid]: {
4037
label: 'Unique ID',
4138
Icon: IconKey,
@@ -137,6 +134,9 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
137134
[FieldMetadataType.RawJson]: {
138135
label: 'JSON',
139136
Icon: IconJson,
140-
defaultValue: `{ "key": "value" }`,
137+
defaultValue: { key: 'value' },
141138
},
142-
};
139+
} as const satisfies Record<
140+
SettingsSupportedFieldType,
141+
SettingsFieldTypeConfig
142+
>;

packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,22 @@ const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
8080
`;
8181

8282
const previewableTypes = [
83+
FieldMetadataType.Address,
8384
FieldMetadataType.Boolean,
8485
FieldMetadataType.Currency,
85-
FieldMetadataType.DateTime,
8686
FieldMetadataType.Date,
87-
FieldMetadataType.Select,
88-
FieldMetadataType.MultiSelect,
87+
FieldMetadataType.DateTime,
88+
FieldMetadataType.FullName,
8989
FieldMetadataType.Link,
9090
FieldMetadataType.Links,
91+
FieldMetadataType.MultiSelect,
9192
FieldMetadataType.Number,
93+
FieldMetadataType.Phone,
9294
FieldMetadataType.Rating,
95+
FieldMetadataType.RawJson,
9396
FieldMetadataType.Relation,
97+
FieldMetadataType.Select,
9498
FieldMetadataType.Text,
95-
FieldMetadataType.Address,
96-
FieldMetadataType.RawJson,
97-
FieldMetadataType.Phone,
9899
];
99100

100101
export const SettingsDataModelFieldSettingsFormCard = ({

packages/twenty-front/src/modules/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencyForm.tsx

+2-12
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,15 @@ import { Controller, useFormContext } from 'react-hook-form';
22
import { z } from 'zod';
33

44
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
5-
import { currencyCodeSchema } from '@/object-record/record-field/validation-schemas/currencyCodeSchema';
5+
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema';
66
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
77
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
88
import { Select } from '@/ui/input/components/Select';
99
import { CardContent } from '@/ui/layout/card/components/CardContent';
1010
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
11-
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
12-
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';
1311

1412
export const settingsDataModelFieldCurrencyFormSchema = z.object({
15-
defaultValue: z.object({
16-
amountMicros: z.number().nullable(),
17-
currencyCode: simpleQuotesStringSchema.refine(
18-
(value) =>
19-
currencyCodeSchema.safeParse(stripSimpleQuotesFromString(value))
20-
.success,
21-
{ message: 'String is not a valid currencyCode' },
22-
),
23-
}),
13+
defaultValue: currencyFieldDefaultValueSchema,
2414
});
2515

2616
export type SettingsDataModelFieldCurrencyFormValues = z.infer<

packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
FieldMetadataItemOption,
1010
} from '@/object-metadata/types/FieldMetadataItem';
1111
import { selectOptionsSchema } from '@/object-metadata/validation-schemas/selectOptionsSchema';
12+
import { multiSelectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema';
13+
import { selectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema';
1214
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
1315
import { generateNewSelectOption } from '@/settings/data-model/fields/forms/select/utils/generateNewSelectOption';
1416
import { isSelectOptionDefaultValue } from '@/settings/data-model/utils/isSelectOptionDefaultValue';
@@ -21,17 +23,16 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
2123
import { moveArrayItem } from '~/utils/array/moveArrayItem';
2224
import { toSpliced } from '~/utils/array/toSpliced';
2325
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
24-
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';
2526

2627
import { SettingsDataModelFieldSelectFormOptionRow } from './SettingsDataModelFieldSelectFormOptionRow';
2728

2829
export const settingsDataModelFieldSelectFormSchema = z.object({
29-
defaultValue: simpleQuotesStringSchema.nullable(),
30+
defaultValue: selectFieldDefaultValueSchema(),
3031
options: selectOptionsSchema,
3132
});
3233

3334
export const settingsDataModelFieldMultiSelectFormSchema = z.object({
34-
defaultValue: z.array(simpleQuotesStringSchema).nullable(),
35+
defaultValue: multiSelectFieldDefaultValueSchema(),
3536
options: selectOptionsSchema,
3637
});
3738

packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview.tsx

+32-7
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { useIcons } from 'twenty-ui';
44

55
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
66
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
7+
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
78
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
89
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
910
import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput';
1011
import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput';
1112
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
1213
import { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
13-
import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview';
14+
import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue';
15+
import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord';
1416
import { FieldMetadataType } from '~/generated-metadata/graphql';
1517

1618
export type SettingsDataModelFieldPreviewProps = {
@@ -61,17 +63,40 @@ export const SettingsDataModelFieldPreview = ({
6163
const { getIcon } = useIcons();
6264
const FieldIcon = getIcon(fieldMetadataItem.icon);
6365

64-
const { entityId, fieldName, fieldPreviewValue, isLabelIdentifier, record } =
65-
useFieldPreview({
66-
fieldMetadataItem,
66+
// id and name are undefined in create mode (field does not exist yet)
67+
// and defined in edit mode.
68+
const isLabelIdentifier =
69+
!!fieldMetadataItem.id &&
70+
!!fieldMetadataItem.name &&
71+
isLabelIdentifierField({
72+
fieldMetadataItem: {
73+
id: fieldMetadataItem.id,
74+
name: fieldMetadataItem.name,
75+
},
6776
objectMetadataItem,
68-
relationObjectMetadataItem,
6977
});
7078

79+
const previewRecord = usePreviewRecord({
80+
objectMetadataItem,
81+
skip: !isLabelIdentifier,
82+
});
83+
84+
const fieldPreviewValue = useFieldPreviewValue({
85+
fieldMetadataItem,
86+
relationObjectMetadataItem,
87+
skip: isLabelIdentifier,
88+
});
89+
90+
const fieldName =
91+
fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`;
92+
const entityId =
93+
previewRecord?.id ??
94+
`${objectMetadataItem.nameSingular}-${fieldName}-preview`;
95+
7196
return (
7297
<>
73-
{record ? (
74-
<SettingsDataModelSetRecordEffect record={record} />
98+
{previewRecord ? (
99+
<SettingsDataModelSetRecordEffect record={previewRecord} />
75100
) : (
76101
<SettingsDataModelSetFieldValueEffect
77102
entityId={entityId}

0 commit comments

Comments
 (0)