feat: save Select field options (#2869)

Closes #2704
This commit is contained in:
Thaïs
2023-12-08 11:15:52 +01:00
committed by GitHub
parent 1f40c45140
commit 56a93d2ead
29 changed files with 211 additions and 105 deletions

View File

@ -20,7 +20,7 @@ export const CREATE_ONE_OBJECT_METADATA_ITEM = gql`
`;
export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
mutation CreateOneFieldMetadataItem($input: CreateOneFieldInput!) {
mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {
createOneField(input: $input) {
id
type
@ -28,12 +28,13 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
label
description
icon
placeholder
isCustom
isActive
isNullable
createdAt
updatedAt
defaultValue
options
}
}
`;
@ -65,7 +66,6 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
label
description
icon
placeholder
isCustom
isActive
isNullable
@ -125,7 +125,6 @@ export const DELETE_ONE_FIELD_METADATA_ITEM = gql`
label
description
icon
placeholder
isCustom
isActive
isNullable

View File

@ -58,6 +58,8 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
}
fromFieldMetadataId
}
defaultValue
options
}
}
pageInfo {

View File

@ -1,8 +1,11 @@
import { v4 } from 'uuid';
import { FieldType } from '@/object-record/field/types/FieldType';
import { Field } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldMetadataItem } from '../types/FieldMetadataItem';
import { FieldMetadataOption } from '../types/FieldMetadataOption';
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
import { useCreateOneFieldMetadataItem } from './useCreateOneFieldMetadataItem';
@ -17,6 +20,7 @@ export const useFieldMetadataItem = () => {
const createMetadataField = (
input: Pick<Field, 'label' | 'icon' | 'description'> & {
objectMetadataId: string;
options?: Omit<FieldMetadataOption, 'id'>[];
type: FieldMetadataType;
},
) =>
@ -27,11 +31,20 @@ export const useFieldMetadataItem = () => {
});
const editMetadataField = (
input: Pick<Field, 'id' | 'label' | 'icon' | 'description'>,
input: Pick<Field, 'id' | 'label' | 'icon' | 'description'> & {
options?: FieldMetadataOption[];
},
) =>
updateOneFieldMetadataItem({
fieldMetadataIdToUpdate: input.id,
updatePayload: formatFieldMetadataItemInput(input),
updatePayload: formatFieldMetadataItemInput({
...input,
// In Edit mode, all options need an id,
// so we generate an id for newly created options.
options: input.options?.map((option) =>
option.id ? option : { ...option, id: v4() },
),
}),
});
const activateMetadataField = (metadataField: FieldMetadataItem) =>

View File

@ -1,8 +1,9 @@
import { ThemeColor } from '@/ui/theme/constants/colors';
import { Field, Relation } from '~/generated-metadata/graphql';
export type FieldMetadataItem = Omit<
Field,
'fromRelationMetadata' | 'toRelationMetadata'
'fromRelationMetadata' | 'toRelationMetadata' | 'defaultValue' | 'options'
> & {
fromRelationMetadata?:
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
@ -20,4 +21,12 @@ export type FieldMetadataItem = Omit<
>;
})
| null;
defaultValue?: string;
options?: {
color: ThemeColor;
id: string;
label: string;
position: number;
value: string;
}[];
};

View File

@ -0,0 +1,8 @@
import { ThemeColor } from '@/ui/theme/constants/colors';
export type FieldMetadataOption = {
color?: ThemeColor;
id?: string;
isDefault?: boolean;
label: string;
};

View File

@ -1,12 +1,34 @@
import toCamelCase from 'lodash.camelcase';
import toSnakeCase from 'lodash.snakecase';
import { Field } from '~/generated-metadata/graphql';
import { FieldMetadataOption } from '../types/FieldMetadataOption';
const getOptionValueFromLabel = (label: string) =>
toSnakeCase(label.trim()).toUpperCase();
export const formatFieldMetadataItemInput = (
input: Pick<Field, 'label' | 'icon' | 'description'>,
) => ({
description: input.description?.trim() ?? null,
icon: input.icon,
label: input.label.trim(),
name: toCamelCase(input.label.trim()),
});
input: Pick<Field, 'label' | 'icon' | 'description' | 'defaultValue'> & {
options?: FieldMetadataOption[];
},
) => {
const defaultOption = input.options?.find((option) => option.isDefault);
return {
defaultValue: defaultOption
? getOptionValueFromLabel(defaultOption.label)
: undefined,
description: input.description?.trim() ?? null,
icon: input.icon,
label: input.label.trim(),
name: toCamelCase(input.label.trim()),
options: input.options?.map((option, index) => ({
color: option.color,
id: option.id,
label: option.label.trim(),
position: index,
value: getOptionValueFromLabel(option.label),
})),
};
};

View File

@ -12,11 +12,7 @@ import { isFieldRating } from '../../types/guards/isFieldRating';
export const useRatingField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata(
FieldMetadataType.Probability,
isFieldRating,
fieldDefinition,
);
assertFieldMetadata(FieldMetadataType.Rating, isFieldRating, fieldDefinition);
const fieldName = fieldDefinition.metadata.fieldName;

View File

@ -2,6 +2,7 @@ import { useContext } from 'react';
import { useRecoilState } from 'recoil';
import { ThemeColor } from '@/ui/theme/constants/colors';
import { FieldMetadataType } from '~/generated/graphql';
import { FieldContext } from '../../contexts/FieldContext';
import { useFieldInitialValue } from '../../hooks/useFieldInitialValue';
@ -14,7 +15,7 @@ import { isFieldSelectValue } from '../../types/guards/isFieldSelectValue';
export const useSelectField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
assertFieldMetadata('ENUM', isFieldSelect, fieldDefinition);
assertFieldMetadata(FieldMetadataType.Select, isFieldSelect, fieldDefinition);
const { fieldName } = fieldDefinition.metadata;

View File

@ -46,7 +46,7 @@ const RatingFieldInputWithContext = ({
fieldDefinition={{
fieldMetadataId: 'rating',
label: 'Rating',
type: FieldMetadataType.Probability,
type: FieldMetadataType.Rating,
iconName: 'Icon123',
metadata: {
fieldName: 'Rating',

View File

@ -2,6 +2,7 @@ import { selectorFamily } from 'recoil';
import { isFieldFullName } from '@/object-record/field/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/field/types/guards/isFieldFullNameValue';
import { isFieldSelect } from '@/object-record/field/types/guards/isFieldSelect';
import { isFieldUuid } from '@/object-record/field/types/guards/isFieldUuid';
import { assertNotNull } from '~/utils/assert';
@ -42,7 +43,8 @@ export const isEntityFieldEmptyFamilySelector = selectorFamily({
isFieldNumber(fieldDefinition) ||
isFieldRating(fieldDefinition) ||
isFieldEmail(fieldDefinition) ||
isFieldBoolean(fieldDefinition)
isFieldBoolean(fieldDefinition) ||
isFieldSelect(fieldDefinition)
//|| isFieldPhone(fieldDefinition)
) {
const fieldValue = get(entityFieldsFamilyState(entityId))?.[

View File

@ -1,18 +1,18 @@
export type FieldType =
| 'BOOLEAN'
| 'UUID'
| 'TEXT'
| 'RELATION'
| 'CHIP'
| 'CURRENCY'
| 'DATE_TIME'
| 'DOUBLE_TEXT_CHIP'
| 'DOUBLE_TEXT'
| 'EMAIL'
| 'ENUM'
| 'FULL_NAME'
| 'LINK'
| 'NUMBER'
| 'PHONE'
| 'PROBABILITY'
| 'RATING'
| 'RELATION'
| 'SELECT'
| 'TEXT'
| 'URL'
| 'LINK'
| 'CURRENCY'
| 'FULL_NAME';
| 'UUID';

View File

@ -29,7 +29,7 @@ type AssertFieldMetadataFunction = <
? FieldDateTimeMetadata
: E extends 'EMAIL'
? FieldEmailMetadata
: E extends 'ENUM'
: E extends 'SELECT'
? FieldSelectMetadata
: E extends 'LINK'
? FieldLinkMetadata
@ -37,7 +37,7 @@ type AssertFieldMetadataFunction = <
? FieldNumberMetadata
: E extends 'PHONE'
? FieldPhoneMetadata
: E extends 'PROBABILITY'
: E extends 'RATING'
? FieldRatingMetadata
: E extends 'RELATION'
? FieldRelationMetadata

View File

@ -6,4 +6,4 @@ import { FieldMetadata, FieldRatingMetadata } from '../FieldMetadata';
export const isFieldRating = (
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldRatingMetadata> =>
field.type === FieldMetadataType.Probability;
field.type === FieldMetadataType.Rating;

View File

@ -1,6 +1,9 @@
import { FieldMetadataType } from '~/generated/graphql';
import { FieldDefinition } from '../FieldDefinition';
import { FieldMetadata, FieldSelectMetadata } from '../FieldMetadata';
export const isFieldSelect = (
field: FieldDefinition<FieldMetadata>,
): field is FieldDefinition<FieldSelectMetadata> => field.type === 'ENUM';
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
): field is FieldDefinition<FieldSelectMetadata> =>
field.type === FieldMetadataType.Select;

View File

@ -148,7 +148,7 @@ export const SettingsObjectFieldPreview = ({
>
{fieldMetadata.type === FieldMetadataType.Boolean ? (
<BooleanFieldInput readonly />
) : fieldMetadata.type === FieldMetadataType.Probability ? (
) : fieldMetadata.type === FieldMetadataType.Rating ? (
<RatingFieldInput readonly />
) : (
<FieldDisplay />

View File

@ -88,10 +88,10 @@ export const SettingsObjectFieldTypeSelectSection = ({
FieldMetadataType.Boolean,
FieldMetadataType.Currency,
FieldMetadataType.DateTime,
FieldMetadataType.Enum,
FieldMetadataType.Select,
FieldMetadataType.Link,
FieldMetadataType.Number,
FieldMetadataType.Probability,
FieldMetadataType.Rating,
FieldMetadataType.Relation,
FieldMetadataType.Text,
].includes(values.type) && (
@ -151,7 +151,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
})
}
/>
) : values.type === FieldMetadataType.Enum ? (
) : values.type === FieldMetadataType.Select ? (
<SettingsObjectFieldSelectForm
values={selectFormConfig}
onChange={(nextValues) => onChange({ select: nextValues })}

View File

@ -95,7 +95,7 @@ export const Rating: Story = {
fieldMetadata: {
icon: 'IconHandClick',
label: 'Engagement',
type: FieldMetadataType.Probability,
type: FieldMetadataType.Rating,
},
},
};

View File

@ -108,7 +108,7 @@ export const WithSelectForm: Story = {
fieldMetadata: { label: 'Industry', icon: 'IconBuildingFactory2' },
values: {
...fieldMetadataFormDefaultValues,
type: FieldMetadataType.Enum,
type: FieldMetadataType.Select,
select: [
{
color: 'pink',

View File

@ -59,10 +59,14 @@ export const settingsFieldMetadataTypes: Record<
Icon: IconCalendarEvent,
defaultValue: defaultDateValue.toISOString(),
},
[FieldMetadataType.Enum]: {
[FieldMetadataType.Select]: {
label: 'Select',
Icon: IconTag,
},
[FieldMetadataType.MultiSelect]: {
label: 'Multi-Select',
Icon: IconTag,
},
[FieldMetadataType.Currency]: {
label: 'Currency',
Icon: IconCoins,
@ -79,5 +83,10 @@ export const settingsFieldMetadataTypes: Record<
Icon: IconTwentyStar,
defaultValue: '3',
},
[FieldMetadataType.Rating]: {
label: 'Rating',
Icon: IconTwentyStar,
defaultValue: '3',
},
[FieldMetadataType.FullName]: { label: 'Full Name', Icon: IconUser },
};

View File

@ -56,11 +56,12 @@ const relationSchema = fieldSchema.merge(
const selectSchema = fieldSchema.merge(
z.object({
type: z.literal(FieldMetadataType.Enum),
type: z.literal(FieldMetadataType.Select),
select: z
.array(
z.object({
color: themeColorSchema,
id: z.string().optional(),
isDefault: z.boolean().optional(),
label: z.string().min(1),
}),
@ -70,18 +71,20 @@ const selectSchema = fieldSchema.merge(
);
const {
Enum: _Enum,
Select: _Select,
Relation: _Relation,
...otherFieldTypes
} = FieldMetadataType;
type OtherFieldType = Exclude<
FieldMetadataType,
FieldMetadataType.Relation | FieldMetadataType.Select
>;
const otherFieldTypesSchema = fieldSchema.merge(
z.object({
type: z.enum(
Object.values(otherFieldTypes) as [
Exclude<FieldMetadataType, FieldMetadataType.Relation>,
...Exclude<FieldMetadataType, FieldMetadataType.Relation>[],
],
Object.values(otherFieldTypes) as [OtherFieldType, ...OtherFieldType[]],
),
}),
);
@ -165,7 +168,7 @@ export const useFieldMetadataForm = () => {
!isDeeplyEqual(initialRelationFormValues, nextRelationFormValues),
);
setHasSelectFormChanged(
nextFieldFormValues.type === FieldMetadataType.Enum &&
nextFieldFormValues.type === FieldMetadataType.Select &&
!isDeeplyEqual(initialSelectFormValues, nextSelectFormValues),
);
};
@ -177,6 +180,7 @@ export const useFieldMetadataForm = () => {
hasFormChanged:
hasFieldFormChanged || hasRelationFormChanged || hasSelectFormChanged,
hasRelationFormChanged,
hasSelectFormChanged,
initForm,
isInitialized,
isValid: validationResult.success,

View File

@ -48,7 +48,7 @@ export const useFieldPreview = ({
const defaultSelectValue = selectOptions?.[0];
const selectValue =
fieldMetadata.type === FieldMetadataType.Enum &&
fieldMetadata.type === FieldMetadataType.Select &&
typeof firstRecordFieldValue === 'string'
? selectOptions?.find(
(selectOption) => selectOption.value === firstRecordFieldValue,
@ -65,7 +65,7 @@ export const useFieldPreview = ({
value:
fieldMetadata.type === FieldMetadataType.Relation
? relationValue
: fieldMetadata.type === FieldMetadataType.Enum
: fieldMetadata.type === FieldMetadataType.Select
? selectValue || defaultSelectValue
: firstRecordFieldValue || defaultValue,
};