Skip to content

Commit

Permalink
feat: simplify field preview logic in Settings (twentyhq#5541)
Browse files Browse the repository at this point in the history
Closes twentyhq#5382

TODO:

- [x] Test all field previews in app
- [x] Fix tests
- [x] Fix JSON preview
  • Loading branch information
thaisguigon committed May 24, 2024
1 parent 1ae7fbe commit c7d61e1
Show file tree
Hide file tree
Showing 33 changed files with 1,182 additions and 508 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
"uuid": "^9.0.0",
"vite-tsconfig-paths": "^4.2.1",
"xlsx-ugnis": "^0.19.3",
"zod": "^3.22.2"
"zod": "3.23.8"
},
"devDependencies": {
"@babel/core": "^7.14.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/twenty-chrome-extension/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default defineManifest({

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

// setting host permissions to all http connections will allow
// setting host permissions to all http connections will allow
// for people who host on their custom domain to get access to
// extension instead of white listing individual urls
host_permissions: ['https://*/*', 'http://*/*'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isString } from '@sniptt/guards';

import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
Expand Down Expand Up @@ -26,8 +28,11 @@ import { isFieldSelectValue } from '@/object-record/record-field/types/guards/is
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
import { isDefined } from '~/utils/isDefined';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';

const isValueEmpty = (value: unknown) => !isDefined(value) || value === '';
const isValueEmpty = (value: unknown) =>
!isDefined(value) ||
(isString(value) && stripSimpleQuotesFromString(value) === '');

export const isFieldValueEmpty = ({
fieldDefinition,
Expand Down Expand Up @@ -78,7 +83,8 @@ export const isFieldValueEmpty = ({
if (isFieldFullName(fieldDefinition)) {
return (
!isFieldFullNameValue(fieldValue) ||
isValueEmpty(fieldValue?.firstName + fieldValue?.lastName)
(isValueEmpty(fieldValue?.firstName) &&
isValueEmpty(fieldValue?.lastName))
);
}

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

import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
import { currencyCodeSchema } from '@/object-record/record-field/validation-schemas/currencyCodeSchema';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';

export const currencyFieldDefaultValueSchema = z.object({
amountMicros: z.number().nullable(),
currencyCode: simpleQuotesStringSchema.refine(
(value): value is `'${CurrencyCode}'` =>
currencyCodeSchema.safeParse(stripSimpleQuotesFromString(value)).success,
{ message: 'String is not a valid currencyCode' },
),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from 'zod';

import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';

export const multiSelectFieldDefaultValueSchema = (
options?: FieldMetadataItemOption[],
) => {
if (!options?.length) return z.array(simpleQuotesStringSchema).nullable();

const optionValues = options.map(({ value }) => value);

return z
.array(
simpleQuotesStringSchema.refine(
(value) => optionValues.includes(stripSimpleQuotesFromString(value)),
{
message: `String is not a valid multi-select option, available options are: ${options.join(
', ',
)}`,
},
),
)
.nullable();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';

export const selectFieldDefaultValueSchema = (
options?: FieldMetadataItemOption[],
) => {
if (!options?.length) return simpleQuotesStringSchema.nullable();

const optionValues = options.map(({ value }) => value);

return simpleQuotesStringSchema
.refine(
(value) => optionValues.includes(stripSimpleQuotesFromString(value)),
{
message: `String is not a valid select option, available options are: ${options.join(
', ',
)}`,
},
)
.nullable();
};
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ export type SettingsFieldTypeConfig = {
defaultValue?: unknown;
};

export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
SettingsSupportedFieldType,
SettingsFieldTypeConfig
> = {
export const SETTINGS_FIELD_TYPE_CONFIGS = {
[FieldMetadataType.Uuid]: {
label: 'Unique ID',
Icon: IconKey,
Expand Down Expand Up @@ -137,6 +134,9 @@ export const SETTINGS_FIELD_TYPE_CONFIGS: Record<
[FieldMetadataType.RawJson]: {
label: 'JSON',
Icon: IconJson,
defaultValue: `{ "key": "value" }`,
defaultValue: { key: 'value' },
},
};
} as const satisfies Record<
SettingsSupportedFieldType,
SettingsFieldTypeConfig
>;
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,22 @@ const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
`;

const previewableTypes = [
FieldMetadataType.Address,
FieldMetadataType.Boolean,
FieldMetadataType.Currency,
FieldMetadataType.DateTime,
FieldMetadataType.Date,
FieldMetadataType.Select,
FieldMetadataType.MultiSelect,
FieldMetadataType.DateTime,
FieldMetadataType.FullName,
FieldMetadataType.Link,
FieldMetadataType.Links,
FieldMetadataType.MultiSelect,
FieldMetadataType.Number,
FieldMetadataType.Phone,
FieldMetadataType.Rating,
FieldMetadataType.RawJson,
FieldMetadataType.Relation,
FieldMetadataType.Select,
FieldMetadataType.Text,
FieldMetadataType.Address,
FieldMetadataType.RawJson,
FieldMetadataType.Phone,
];

export const SettingsDataModelFieldSettingsFormCard = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,15 @@ import { Controller, useFormContext } from 'react-hook-form';
import { z } from 'zod';

import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { currencyCodeSchema } from '@/object-record/record-field/validation-schemas/currencyCodeSchema';
import { currencyFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/currencyFieldDefaultValueSchema';
import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes';
import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues';
import { Select } from '@/ui/input/components/Select';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';

export const settingsDataModelFieldCurrencyFormSchema = z.object({
defaultValue: z.object({
amountMicros: z.number().nullable(),
currencyCode: simpleQuotesStringSchema.refine(
(value) =>
currencyCodeSchema.safeParse(stripSimpleQuotesFromString(value))
.success,
{ message: 'String is not a valid currencyCode' },
),
}),
defaultValue: currencyFieldDefaultValueSchema,
});

export type SettingsDataModelFieldCurrencyFormValues = z.infer<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
FieldMetadataItemOption,
} from '@/object-metadata/types/FieldMetadataItem';
import { selectOptionsSchema } from '@/object-metadata/validation-schemas/selectOptionsSchema';
import { multiSelectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/multiSelectFieldDefaultValueSchema';
import { selectFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/selectFieldDefaultValueSchema';
import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues';
import { generateNewSelectOption } from '@/settings/data-model/fields/forms/select/utils/generateNewSelectOption';
import { isSelectOptionDefaultValue } from '@/settings/data-model/utils/isSelectOptionDefaultValue';
Expand All @@ -21,17 +23,16 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { toSpliced } from '~/utils/array/toSpliced';
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
import { simpleQuotesStringSchema } from '~/utils/validation-schemas/simpleQuotesStringSchema';

import { SettingsDataModelFieldSelectFormOptionRow } from './SettingsDataModelFieldSelectFormOptionRow';

export const settingsDataModelFieldSelectFormSchema = z.object({
defaultValue: simpleQuotesStringSchema.nullable(),
defaultValue: selectFieldDefaultValueSchema(),
options: selectOptionsSchema,
});

export const settingsDataModelFieldMultiSelectFormSchema = z.object({
defaultValue: z.array(simpleQuotesStringSchema).nullable(),
defaultValue: multiSelectFieldDefaultValueSchema(),
options: selectOptionsSchema,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { useIcons } from 'twenty-ui';

import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { FieldDisplay } from '@/object-record/record-field/components/FieldDisplay';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { BooleanFieldInput } from '@/object-record/record-field/meta-types/input/components/BooleanFieldInput';
import { RatingFieldInput } from '@/object-record/record-field/meta-types/input/components/RatingFieldInput';
import { SettingsDataModelSetFieldValueEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetFieldValueEffect';
import { SettingsDataModelSetRecordEffect } from '@/settings/data-model/fields/preview/components/SettingsDataModelSetRecordEffect';
import { useFieldPreview } from '@/settings/data-model/fields/preview/hooks/useFieldPreview';
import { useFieldPreviewValue } from '@/settings/data-model/fields/preview/hooks/useFieldPreviewValue';
import { usePreviewRecord } from '@/settings/data-model/fields/preview/hooks/usePreviewRecord';
import { FieldMetadataType } from '~/generated-metadata/graphql';

export type SettingsDataModelFieldPreviewProps = {
Expand Down Expand Up @@ -61,17 +63,40 @@ export const SettingsDataModelFieldPreview = ({
const { getIcon } = useIcons();
const FieldIcon = getIcon(fieldMetadataItem.icon);

const { entityId, fieldName, fieldPreviewValue, isLabelIdentifier, record } =
useFieldPreview({
fieldMetadataItem,
// id and name are undefined in create mode (field does not exist yet)
// and defined in edit mode.
const isLabelIdentifier =
!!fieldMetadataItem.id &&
!!fieldMetadataItem.name &&
isLabelIdentifierField({
fieldMetadataItem: {
id: fieldMetadataItem.id,
name: fieldMetadataItem.name,
},
objectMetadataItem,
relationObjectMetadataItem,
});

const previewRecord = usePreviewRecord({
objectMetadataItem,
skip: !isLabelIdentifier,
});

const fieldPreviewValue = useFieldPreviewValue({
fieldMetadataItem,
relationObjectMetadataItem,
skip: isLabelIdentifier,
});

const fieldName =
fieldMetadataItem.name || `${fieldMetadataItem.type}-new-field`;
const entityId =
previewRecord?.id ??
`${objectMetadataItem.nameSingular}-${fieldName}-preview`;

return (
<>
{record ? (
<SettingsDataModelSetRecordEffect record={record} />
{previewRecord ? (
<SettingsDataModelSetRecordEffect record={previewRecord} />
) : (
<SettingsDataModelSetFieldValueEffect
entityId={entityId}
Expand Down
Loading

0 comments on commit c7d61e1

Please sign in to comment.