diff --git a/front/src/generated-metadata/graphql.ts b/front/src/generated-metadata/graphql.ts index 1c6f7db37..08c7365db 100644 --- a/front/src/generated-metadata/graphql.ts +++ b/front/src/generated-metadata/graphql.ts @@ -1014,6 +1014,26 @@ export type UserV2Edge = { node: UserV2; }; +export type WorkspaceV2 = { + __typename?: 'workspaceV2'; + createdAt: Scalars['DateTime']['output']; + deletedAt?: Maybe; + displayName?: Maybe; + domainName?: Maybe; + id: Scalars['ID']['output']; + inviteHash?: Maybe; + logo?: Maybe; + updatedAt: Scalars['DateTime']['output']; +}; + +export type WorkspaceV2Edge = { + __typename?: 'workspaceV2Edge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']['output']; + /** The node containing the workspaceV2 */ + node: WorkspaceV2; +}; + export type CreateOneObjectMetadataItemMutationVariables = Exact<{ input: CreateOneObjectInput; }>; diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 0096dd770..cd6980802 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1462,6 +1462,7 @@ export type Mutation = { updateOnePerson?: Maybe; updateOnePipelineProgress?: Maybe; updateOnePipelineStage?: Maybe; + updateOneWorkspaceV2: WorkspaceV2; updateUser: User; updateWorkspace: Workspace; uploadAttachment: Scalars['String']; @@ -1668,6 +1669,11 @@ export type MutationUpdateOnePipelineStageArgs = { }; +export type MutationUpdateOneWorkspaceV2Args = { + input: UpdateOneWorkspaceV2Input; +}; + + export type MutationUpdateUserArgs = { data: UserUpdateInput; where: UserWhereUniqueInput; @@ -2486,6 +2492,8 @@ export type Query = { objects: ObjectConnection; relation: Relation; relations: RelationConnection; + workspaceV2: WorkspaceV2; + workspaceV2s: WorkspaceV2Connection; }; @@ -2613,6 +2621,18 @@ export type QueryFindWorkspaceFromInviteHashArgs = { inviteHash: Scalars['String']; }; + +export type QueryWorkspaceV2Args = { + id: Scalars['ID']; +}; + + +export type QueryWorkspaceV2sArgs = { + filter?: WorkspaceV2Filter; + paging?: CursorPaging; + sorting?: Array; +}; + export enum QueryMode { Default = 'default', Insensitive = 'insensitive' @@ -2647,6 +2667,18 @@ export enum RelationMetadataType { OneToOne = 'ONE_TO_ONE' } +/** Sort Directions */ +export enum SortDirection { + Asc = 'ASC', + Desc = 'DESC' +} + +/** Sort Nulls Options */ +export enum SortNulls { + NullsFirst = 'NULLS_FIRST', + NullsLast = 'NULLS_LAST' +} + export enum SortOrder { Asc = 'asc', Desc = 'desc' @@ -2694,6 +2726,20 @@ export type Telemetry = { enabled: Scalars['Boolean']; }; +export type UpdateOneWorkspaceV2Input = { + /** The id of the record to update */ + id: Scalars['ID']; + /** The update to apply. */ + update: UpdateWorkspaceInput; +}; + +export type UpdateWorkspaceInput = { + displayName: Scalars['String']; + domainName: Scalars['String']; + inviteHash: Scalars['String']; + logo: Scalars['String']; +}; + export type User = { __typename?: 'User'; assignedActivities?: Maybe>; @@ -3150,6 +3196,16 @@ export type WorkspaceUpdateInput = { workspaceMember?: InputMaybe; }; +export type WorkspaceV2Connection = { + __typename?: 'WorkspaceV2Connection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; + /** Fetch total count of records */ + totalCount: Scalars['Int']; +}; + export type Field = { __typename?: 'field'; createdAt: Scalars['DateTime']; @@ -3287,6 +3343,42 @@ export type UserV2Edge = { node: UserV2; }; +export type WorkspaceV2 = { + __typename?: 'workspaceV2'; + createdAt: Scalars['DateTime']; + deletedAt?: Maybe; + displayName?: Maybe; + domainName?: Maybe; + id: Scalars['ID']; + inviteHash?: Maybe; + logo?: Maybe; + updatedAt: Scalars['DateTime']; +}; + +export type WorkspaceV2Edge = { + __typename?: 'workspaceV2Edge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']; + /** The node containing the workspaceV2 */ + node: WorkspaceV2; +}; + +export type WorkspaceV2Filter = { + and?: InputMaybe>; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type WorkspaceV2Sort = { + direction: SortDirection; + field: WorkspaceV2SortFields; + nulls?: InputMaybe; +}; + +export enum WorkspaceV2SortFields { + Id = 'id' +} + export type CreateEventMutationVariables = Exact<{ type: Scalars['String']; data: Scalars['JSON']; diff --git a/front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts b/front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts index 75ed8d406..c736a142b 100644 --- a/front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts +++ b/front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts @@ -35,6 +35,11 @@ export const useObjectMetadataItemForSettings = () => { (objectMetadataItem) => objectMetadataItem.id === id, ); + const findObjectMetadataItemByNamePlural = (namePlural: string) => + objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.namePlural === namePlural, + ); + const { createOneObjectMetadataItem } = useCreateOneObjectRecordMetadataItem(); const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem(); @@ -88,6 +93,8 @@ export const useObjectMetadataItemForSettings = () => { eraseObjectMetadataItem, findActiveObjectMetadataItemBySlug, findObjectMetadataItemById, + findObjectMetadataItemByNamePlural, loading, + objectMetadataItems, }; }; diff --git a/front/src/modules/object-metadata/utils/parseFieldRelationType.ts b/front/src/modules/object-metadata/utils/parseFieldRelationType.ts index fa5b9465e..14a526e38 100644 --- a/front/src/modules/object-metadata/utils/parseFieldRelationType.ts +++ b/front/src/modules/object-metadata/utils/parseFieldRelationType.ts @@ -1,39 +1,51 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldDefinitionRelationType } from '@/ui/object/field/types/FieldDefinition'; +import { + FieldMetadataType, + RelationMetadataType, +} from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; export const parseFieldRelationType = ( field: FieldMetadataItem | undefined, ): FieldDefinitionRelationType | undefined => { - if (field && field.type === 'RELATION') { - if ( - isDefined(field.fromRelationMetadata) && - field.fromRelationMetadata.relationType === 'ONE_TO_MANY' - ) { - return 'FROM_NAMY_OBJECTS'; - } else if ( - isDefined(field.toRelationMetadata) && - field.toRelationMetadata.relationType === 'ONE_TO_MANY' - ) { - return 'TO_ONE_OBJECT'; - } else if ( - isDefined(field.fromRelationMetadata) && - field.fromRelationMetadata.relationType === 'MANY_TO_MANY' - ) { - return 'FROM_NAMY_OBJECTS'; - } else if ( - isDefined(field.toRelationMetadata) && - field.toRelationMetadata.relationType === 'MANY_TO_MANY' - ) { - return 'TO_MANY_OBJECTS'; - } + if (!field || field.type !== FieldMetadataType.Relation) return; - throw new Error( - `Cannot determine field relation type for field : ${JSON.stringify( - field, - )}.`, - ); - } else { - return undefined; + const config: Record< + RelationMetadataType, + { from: FieldDefinitionRelationType; to: FieldDefinitionRelationType } + > = { + [RelationMetadataType.ManyToMany]: { + from: 'FROM_MANY_OBJECTS', + to: 'TO_MANY_OBJECTS', + }, + [RelationMetadataType.OneToMany]: { + from: 'FROM_MANY_OBJECTS', + to: 'TO_ONE_OBJECT', + }, + [RelationMetadataType.OneToOne]: { + from: 'FROM_ONE_OBJECT', + to: 'TO_ONE_OBJECT', + }, + }; + + if ( + isDefined(field.fromRelationMetadata) && + field.fromRelationMetadata.relationType in config + ) { + return config[field.fromRelationMetadata.relationType].from; } + + if ( + isDefined(field.toRelationMetadata) && + field.toRelationMetadata.relationType in config + ) { + return config[field.toRelationMetadata.relationType].to; + } + + throw new Error( + `Cannot determine field relation type for field : ${JSON.stringify( + field, + )}.`, + ); }; diff --git a/front/src/modules/settings/data-model/assets/OneToMany.svg b/front/src/modules/settings/data-model/assets/OneToMany.svg new file mode 100644 index 000000000..4720d885d --- /dev/null +++ b/front/src/modules/settings/data-model/assets/OneToMany.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/front/src/modules/settings/data-model/assets/OneToOne.svg b/front/src/modules/settings/data-model/assets/OneToOne.svg new file mode 100644 index 000000000..3c9920997 --- /dev/null +++ b/front/src/modules/settings/data-model/assets/OneToOne.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx index 7c80b6ed2..d3d667480 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreview.tsx @@ -1,30 +1,25 @@ -import { useEffect } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; import { parseFieldType } from '@/object-metadata/utils/parseFieldType'; -import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; import { Tag } from '@/ui/display/tag/components/Tag'; -import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon'; import { FieldDisplay } from '@/ui/object/field/components/FieldDisplay'; import { FieldContext } from '@/ui/object/field/contexts/FieldContext'; import { BooleanFieldInput } from '@/ui/object/field/meta-types/input/components/BooleanFieldInput'; -import { entityFieldsFamilySelector } from '@/ui/object/field/states/selectors/entityFieldsFamilySelector'; -import { FieldMetadataType } from '~/generated/graphql'; -import { assertNotNull } from '~/utils/assert'; +import { Field } from '~/generated/graphql'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { SettingsObjectFieldPreviewValueEffect } from '../components/SettingsObjectFieldPreviewValueEffect'; import { dataTypes } from '../constants/dataTypes'; +import { useFieldPreview } from '../hooks/useFieldPreview'; +import { useRelationFieldPreview } from '../hooks/useRelationFieldPreview'; export type SettingsObjectFieldPreviewProps = { - fieldIconKey?: string | null; - fieldLabel: string; - fieldName?: string; - fieldType: FieldMetadataType; - isObjectCustom: boolean; - objectIconKey?: string | null; - objectLabelPlural: string; - objectNamePlural: string; + className?: string; + fieldMetadata: Pick & { id?: string }; + objectMetadataId: string; + relationObjectMetadataId?: string; + shrink?: boolean; }; const StyledContainer = styled.div` @@ -52,7 +47,7 @@ const StyledObjectName = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; -const StyledFieldPreview = styled.div` +const StyledFieldPreview = styled.div<{ shrink?: boolean }>` align-items: center; background-color: ${({ theme }) => theme.background.primary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -61,7 +56,8 @@ const StyledFieldPreview = styled.div` gap: ${({ theme }) => theme.spacing(2)}; height: ${({ theme }) => theme.spacing(8)}; overflow: hidden; - padding: 0 ${({ theme }) => theme.spacing(2)}; + padding: 0 + ${({ shrink, theme }) => (shrink ? theme.spacing(1) : theme.spacing(2))}; white-space: nowrap; `; @@ -73,41 +69,41 @@ const StyledFieldLabel = styled.div` `; export const SettingsObjectFieldPreview = ({ - fieldIconKey, - fieldLabel, - fieldName, - fieldType, - isObjectCustom, - objectIconKey, - objectLabelPlural, - objectNamePlural, + className, + fieldMetadata, + objectMetadataId, + relationObjectMetadataId, + shrink, }: SettingsObjectFieldPreviewProps) => { const theme = useTheme(); - const { Icon: ObjectIcon } = useLazyLoadIcon(objectIconKey ?? ''); - const { Icon: FieldIcon } = useLazyLoadIcon(fieldIconKey ?? ''); - const { objects } = useFindManyObjectRecords({ - objectNamePlural, - skip: !fieldName, + const { + entityId, + FieldIcon, + fieldName, + hasValue, + ObjectIcon, + objectMetadataItem, + value, + } = useFieldPreview({ + fieldMetadata, + objectMetadataId, }); - const [fieldValue, setFieldValue] = useRecoilState( - entityFieldsFamilySelector({ - entityId: objects[0]?.id ?? objectNamePlural, - fieldName: fieldName || 'new-field', - }), - ); + const { defaultValue: relationDefaultValue, entityChipDisplayMapper } = + useRelationFieldPreview({ + relationObjectMetadataId, + skipDefaultValue: + fieldMetadata.type !== FieldMetadataType.Relation || hasValue, + }); - useEffect(() => { - setFieldValue( - fieldName && assertNotNull(objects[0]?.[fieldName]) - ? objects[0][fieldName] - : dataTypes[fieldType].defaultValue, - ); - }, [fieldName, fieldType, fieldValue, objects, setFieldValue]); + const defaultValue = + fieldMetadata.type === FieldMetadataType.Relation + ? relationDefaultValue + : dataTypes[fieldMetadata.type].defaultValue; return ( - + {!!ObjectIcon && ( @@ -116,15 +112,20 @@ export const SettingsObjectFieldPreview = ({ stroke={theme.icon.stroke.sm} /> )} - {objectLabelPlural} + {objectMetadataItem?.labelPlural} - {isObjectCustom ? ( + {objectMetadataItem?.isCustom ? ( ) : ( )} - + + {!!FieldIcon && ( )} - {fieldLabel}: + {fieldMetadata.label}: - {fieldType === 'BOOLEAN' ? ( + {fieldMetadata.type === FieldMetadataType.Boolean ? ( ) : ( diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldPreviewValueEffect.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreviewValueEffect.tsx new file mode 100644 index 000000000..96b008e16 --- /dev/null +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldPreviewValueEffect.tsx @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; +import { useRecoilState } from 'recoil'; + +import { entityFieldsFamilySelector } from '@/ui/object/field/states/selectors/entityFieldsFamilySelector'; + +type SettingsObjectFieldPreviewValueEffectProps = { + entityId: string; + fieldName: string; + value: unknown; +}; + +export const SettingsObjectFieldPreviewValueEffect = ({ + entityId, + fieldName, + value, +}: SettingsObjectFieldPreviewValueEffectProps) => { + const [, setFieldValue] = useRecoilState( + entityFieldsFamilySelector({ + entityId, + fieldName, + }), + ); + + useEffect(() => { + setFieldValue(value); + }, [value, setFieldValue]); + + return null; +}; diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldRelationForm.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldRelationForm.tsx new file mode 100644 index 000000000..19ab4a044 --- /dev/null +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldRelationForm.tsx @@ -0,0 +1,123 @@ +import styled from '@emotion/styled'; + +import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { validateMetadataLabel } from '@/object-metadata/utils/validateMetadataLabel'; +import { IconPicker } from '@/ui/input/components/IconPicker'; +import { Select } from '@/ui/input/components/Select'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons'; +import { Field } from '~/generated-metadata/graphql'; + +import { relationTypes } from '../constants/relationTypes'; +import { RelationType } from '../types/RelationType'; + +export type SettingsObjectFieldRelationFormValues = Partial<{ + field: Partial>; + objectMetadataId: string; + type: RelationType; +}>; + +type SettingsObjectFieldRelationFormProps = { + disableRelationEdition?: boolean; + onChange: (values: SettingsObjectFieldRelationFormValues) => void; + values?: SettingsObjectFieldRelationFormValues; +}; + +const StyledSelectsContainer = styled.div` + display: grid; + gap: ${({ theme }) => theme.spacing(4)}; + grid-template-columns: 1fr 1fr; + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledInputsLabel = styled.span` + color: ${({ theme }) => theme.font.color.light}; + display: block; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; + text-transform: uppercase; +`; + +const StyledInputsContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +export const SettingsObjectFieldRelationForm = ({ + disableRelationEdition, + onChange, + values, +}: SettingsObjectFieldRelationFormProps) => { + const { icons } = useLazyLoadIcons(); + const { objectMetadataItems, findObjectMetadataItemById } = + useObjectMetadataItemForSettings(); + + const selectedObjectMetadataItem = + (values?.objectMetadataId + ? findObjectMetadataItemById(values.objectMetadataId) + : undefined) || objectMetadataItems[0]; + + return ( +
+ + ({ + label: objectMetadataItem.labelPlural, + value: objectMetadataItem.id, + Icon: objectMetadataItem.icon + ? icons[objectMetadataItem.icon] + : undefined, + }))} + onChange={(value) => onChange({ objectMetadataId: value })} + /> + + + Field on {selectedObjectMetadataItem?.labelPlural} + + + + onChange({ + field: { ...values?.field, icon: value.iconKey }, + }) + } + variant="primary" + /> + { + if (!value || validateMetadataLabel(value)) { + onChange({ + field: { ...values?.field, label: value }, + }); + } + }} + fullWidth + /> + +
+ ); +}; diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeCard.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeCard.tsx index 137349614..27c549bad 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeCard.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeCard.tsx @@ -27,6 +27,11 @@ const StyledTitle = styled.h3` margin-bottom: ${({ theme }) => theme.spacing(4)}; `; +const StyledPreviewContent = styled.div` + display: flex; + gap: 6px; +`; + const StyledFormContainer = styled.div` background-color: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -46,7 +51,7 @@ export const SettingsObjectFieldTypeCard = ({
Preview - {preview} + {preview} {!!form && {form}}
diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx index 801582e49..3a53e368d 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx @@ -3,82 +3,142 @@ import styled from '@emotion/styled'; import { H2Title } from '@/ui/display/typography/components/H2Title'; import { Select } from '@/ui/input/components/Select'; import { Section } from '@/ui/layout/section/components/Section'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { Field, FieldMetadataType } from '~/generated-metadata/graphql'; import { dataTypes } from '../constants/dataTypes'; +import { relationTypes } from '../constants/relationTypes'; import { SettingsObjectFieldPreview, SettingsObjectFieldPreviewProps, } from './SettingsObjectFieldPreview'; +import { + SettingsObjectFieldRelationForm, + SettingsObjectFieldRelationFormValues, +} from './SettingsObjectFieldRelationForm'; import { SettingsObjectFieldTypeCard } from './SettingsObjectFieldTypeCard'; +export type SettingsObjectFieldTypeSelectSectionFormValues = Partial<{ + type: FieldMetadataType; + relation: SettingsObjectFieldRelationFormValues; +}>; + type SettingsObjectFieldTypeSelectSectionProps = { - disabled?: boolean; - onChange?: (value: FieldMetadataType) => void; -} & Pick< - SettingsObjectFieldPreviewProps, - | 'fieldIconKey' - | 'fieldLabel' - | 'fieldName' - | 'fieldType' - | 'isObjectCustom' - | 'objectIconKey' - | 'objectLabelPlural' - | 'objectNamePlural' ->; + fieldMetadata: Pick & { id?: string }; + relationFieldMetadataId?: string; + onChange: (values: SettingsObjectFieldTypeSelectSectionFormValues) => void; + values?: SettingsObjectFieldTypeSelectSectionFormValues; +} & Pick; const StyledSettingsObjectFieldTypeCard = styled(SettingsObjectFieldTypeCard)` margin-top: ${({ theme }) => theme.spacing(4)}; `; -// TODO: remove "enum" and "relation" types for now, add them back when the backend is ready. -const { ENUM: _ENUM, RELATION: _RELATION, ...allowedDataTypes } = dataTypes; +const StyledSettingsObjectFieldPreview = styled(SettingsObjectFieldPreview)` + display: grid; + flex: 1 1 100%; +`; + +const StyledRelationImage = styled.img<{ flip?: boolean }>` + transform: ${({ flip }) => (flip ? 'scaleX(-1)' : 'none')}; + width: 54px; +`; export const SettingsObjectFieldTypeSelectSection = ({ - disabled, - fieldIconKey, - fieldLabel, - fieldName, - fieldType, - isObjectCustom, - objectIconKey, - objectLabelPlural, - objectNamePlural, + fieldMetadata, + relationFieldMetadataId, + objectMetadataId, onChange, -}: SettingsObjectFieldTypeSelectSectionProps) => ( -
- - onChange({ type: value })} + options={allowedFieldTypes.map(([key, dataType]) => ({ + value: key as FieldMetadataType, + ...dataType, + }))} + /> + {!!values?.type && + [ + FieldMetadataType.Boolean, + FieldMetadataType.Currency, + FieldMetadataType.Date, + FieldMetadataType.Link, + FieldMetadataType.Number, + FieldMetadataType.Relation, + FieldMetadataType.Text, + ].includes(values.type) && ( + + + {values.type === FieldMetadataType.Relation && + !!relationFormConfig?.type && + !!relationFormConfig.objectMetadataId && ( + <> + + + + )} + + } + form={ + values.type === FieldMetadataType.Relation && ( + + onChange({ + relation: { ...relationFormConfig, ...nextValues }, + }) + } + /> + ) + } + /> + )} +
+ ); +}; diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx deleted file mode 100644 index 4a75619cd..000000000 --- a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldPreview.stories.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { MemoryRouter } from 'react-router-dom'; -import { Meta, StoryObj } from '@storybook/react'; - -import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; - -import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview'; - -const meta: Meta = { - title: 'Modules/Settings/DataModel/SettingsObjectFieldPreview', - component: SettingsObjectFieldPreview, - decorators: [ComponentDecorator], - args: { - fieldIconKey: 'IconNotes', - fieldLabel: 'Description', - fieldType: FieldMetadataType.Text, - isObjectCustom: false, - objectIconKey: 'IconBuildingSkyscraper', - objectLabelPlural: 'Companies', - objectNamePlural: 'companies', - }, -}; - -export default meta; -type Story = StoryObj; - -export const Text: Story = {}; - -export const Boolean: Story = { - args: { - fieldIconKey: 'IconHeadphones', - fieldLabel: 'Priority Support', - fieldType: FieldMetadataType.Boolean, - }, -}; - -export const Currency: Story = { - args: { - fieldIconKey: 'IconCurrencyDollar', - fieldLabel: 'Amount', - fieldType: FieldMetadataType.Currency, - }, -}; - -export const Date: Story = { - args: { - fieldIconKey: 'IconCalendarEvent', - fieldLabel: 'Registration Date', - fieldType: FieldMetadataType.Date, - }, -}; - -export const Link: Story = { - decorators: [ - (Story) => ( - - - - ), - ], - args: { - fieldIconKey: 'IconWorldWww', - fieldLabel: 'Website', - fieldType: FieldMetadataType.Link, - }, -}; - -export const Number: Story = { - args: { - fieldIconKey: 'IconUsers', - fieldLabel: 'Employees', - fieldType: FieldMetadataType.Number, - }, -}; - -export const Select: Story = { - args: { - fieldIconKey: 'IconBuildingFactory2', - fieldLabel: 'Industry', - fieldType: FieldMetadataType.Enum, - }, -}; - -export const CustomObject: Story = { - args: { - isObjectCustom: true, - objectIconKey: 'IconApps', - objectLabelPlural: 'Workspaces', - objectNamePlural: 'workspaces', - }, -}; diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeCard.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeCard.stories.tsx deleted file mode 100644 index 7d363e6ab..000000000 --- a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeCard.stories.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; - -import { TextInput } from '@/ui/input/components/TextInput'; -import { FieldMetadataType } from '~/generated/graphql'; -import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; - -import { SettingsObjectFieldPreview } from '../SettingsObjectFieldPreview'; -import { SettingsObjectFieldTypeCard } from '../SettingsObjectFieldTypeCard'; - -const meta: Meta = { - title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeCard', - component: SettingsObjectFieldTypeCard, - decorators: [ComponentDecorator], - args: { - preview: ( - - ), - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; - -export const WithForm: Story = { - args: { form: }, -}; diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx index f4e01f1b4..db47209b0 100644 --- a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx +++ b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx @@ -1,7 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/testing-library'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { SettingsObjectFieldTypeSelectSection } from '../SettingsObjectFieldTypeSelectSection'; @@ -10,16 +9,7 @@ const meta: Meta = { title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeSelectSection', component: SettingsObjectFieldTypeSelectSection, decorators: [ComponentDecorator], - args: { - fieldType: FieldMetadataType.Number, - fieldIconKey: 'IconUsers', - fieldLabel: 'Employees', - fieldName: 'employees', - isObjectCustom: false, - objectIconKey: 'IconUser', - objectLabelPlural: 'People', - objectNamePlural: 'people', - }, + args: {}, }; export default meta; @@ -28,7 +18,7 @@ type Story = StoryObj; export const Default: Story = {}; export const Disabled: Story = { - args: { disabled: true }, + args: {}, }; export const WithOpenSelect: Story = { diff --git a/front/src/modules/settings/data-model/constants/dataTypes.ts b/front/src/modules/settings/data-model/constants/dataTypes.ts index d675c04c9..445ac3b8e 100644 --- a/front/src/modules/settings/data-model/constants/dataTypes.ts +++ b/front/src/modules/settings/data-model/constants/dataTypes.ts @@ -7,7 +7,7 @@ import { IconMail, IconNumbers, IconPhone, - IconPlug, + IconRelationManyToMany, IconTag, IconTextSize, IconUser, @@ -61,9 +61,12 @@ export const dataTypes: Record< [FieldMetadataType.Currency]: { label: 'Currency', Icon: IconCoins, - defaultValue: { amount: 2000, currency: CurrencyCode.Usd }, + defaultValue: { amountMicros: 2000000000, currencyCode: CurrencyCode.Usd }, + }, + [FieldMetadataType.Relation]: { + label: 'Relation', + Icon: IconRelationManyToMany, }, - [FieldMetadataType.Relation]: { label: 'Relation', Icon: IconPlug }, [FieldMetadataType.Email]: { label: 'Email', Icon: IconMail }, [FieldMetadataType.Phone]: { label: 'Phone', Icon: IconPhone }, [FieldMetadataType.Probability]: { diff --git a/front/src/modules/settings/data-model/constants/relationTypes.ts b/front/src/modules/settings/data-model/constants/relationTypes.ts new file mode 100644 index 000000000..f7e5df873 --- /dev/null +++ b/front/src/modules/settings/data-model/constants/relationTypes.ts @@ -0,0 +1,34 @@ +import { IconRelationOneToMany, IconRelationOneToOne } from '@/ui/display/icon'; +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { RelationMetadataType } from '~/generated-metadata/graphql'; + +import OneToManySvg from '../assets/OneToMany.svg'; +import OneToOneSvg from '../assets/OneToOne.svg'; +import { RelationType } from '../types/RelationType'; + +export const relationTypes: Record< + RelationType, + { + label: string; + Icon: IconComponent; + imageSrc: string; + isImageFlipped?: boolean; + } +> = { + [RelationMetadataType.OneToMany]: { + label: 'Has many', + Icon: IconRelationOneToMany, + imageSrc: OneToManySvg, + }, + [RelationMetadataType.OneToOne]: { + label: 'Has one', + Icon: IconRelationOneToOne, + imageSrc: OneToOneSvg, + }, + MANY_TO_ONE: { + label: 'Belongs to one', + Icon: IconRelationOneToMany, + imageSrc: OneToManySvg, + isImageFlipped: true, + }, +}; diff --git a/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts b/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts new file mode 100644 index 000000000..6cbf49f79 --- /dev/null +++ b/front/src/modules/settings/data-model/hooks/useFieldMetadataForm.ts @@ -0,0 +1,142 @@ +import { useState } from 'react'; +import { DeepPartial } from 'react-hook-form'; +import { z } from 'zod'; + +import { + FieldMetadataType, + RelationMetadataType, +} from '~/generated-metadata/graphql'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; + +import { SettingsObjectFieldTypeSelectSectionFormValues } from '../components/SettingsObjectFieldTypeSelectSection'; + +type FormValues = { + description?: string; + icon: string; + label: string; + type: FieldMetadataType; + relation: SettingsObjectFieldTypeSelectSectionFormValues['relation']; +}; + +const defaultValues: FormValues = { + icon: 'IconUsers', + label: '', + type: FieldMetadataType.Text, + relation: { + type: RelationMetadataType.OneToMany, + }, +}; + +const fieldSchema = z.object({ + description: z.string().optional(), + icon: z.string().startsWith('Icon'), + label: z.string().min(1), +}); + +const relationSchema = fieldSchema.merge( + z.object({ + type: z.literal(FieldMetadataType.Relation), + relation: z.object({ + field: fieldSchema, + objectMetadataId: z.string().uuid(), + type: z.enum([ + RelationMetadataType.OneToMany, + RelationMetadataType.OneToOne, + 'MANY_TO_ONE', + ]), + }), + }), +); + +const { Relation: _, ...otherFieldTypes } = FieldMetadataType; + +const otherFieldTypesSchema = fieldSchema.merge( + z.object({ + type: z.enum( + Object.values(otherFieldTypes) as [ + Exclude, + ...Exclude[], + ], + ), + }), +); + +const schema = z.discriminatedUnion('type', [ + relationSchema, + otherFieldTypesSchema, +]); + +export const useFieldMetadataForm = () => { + const [isInitialized, setIsInitialized] = useState(false); + const [initialFormValues, setInitialFormValues] = + useState(defaultValues); + const [formValues, setFormValues] = useState(defaultValues); + const [hasFieldFormChanged, setHasFieldFormChanged] = useState(false); + const [hasRelationFormChanged, setHasRelationFormChanged] = useState(false); + const [validationResult, setValidationResult] = useState( + schema.safeParse(formValues), + ); + + const mergePartialValues = ( + previousValues: FormValues, + nextValues: DeepPartial, + ) => ({ + ...previousValues, + ...nextValues, + relation: { + ...previousValues.relation, + ...nextValues.relation, + field: { + ...previousValues.relation?.field, + ...nextValues.relation?.field, + }, + }, + }); + + const initForm = (lazyInitialFormValues: DeepPartial) => { + if (isInitialized) return; + + const mergedFormValues = mergePartialValues( + initialFormValues, + lazyInitialFormValues, + ); + + setInitialFormValues(mergedFormValues); + setFormValues(mergedFormValues); + setValidationResult(schema.safeParse(mergedFormValues)); + setIsInitialized(true); + }; + + const handleFormChange = (values: DeepPartial) => { + const nextFormValues = mergePartialValues(formValues, values); + + setFormValues(nextFormValues); + setValidationResult(schema.safeParse(nextFormValues)); + + const { relation: initialRelationFormValues, ...initialFieldFormValues } = + initialFormValues; + const { relation: nextRelationFormValues, ...nextFieldFormValues } = + nextFormValues; + + setHasFieldFormChanged( + !isDeeplyEqual(initialFieldFormValues, nextFieldFormValues), + ); + setHasRelationFormChanged( + nextFieldFormValues.type === FieldMetadataType.Relation && + !isDeeplyEqual(initialRelationFormValues, nextRelationFormValues), + ); + }; + + return { + formValues, + handleFormChange, + hasFieldFormChanged, + hasFormChanged: hasFieldFormChanged || hasRelationFormChanged, + hasRelationFormChanged, + initForm, + isValid: validationResult.success, + validatedFormValues: validationResult.success + ? validationResult.data + : undefined, + }; +}; diff --git a/front/src/modules/settings/data-model/hooks/useFieldPreview.ts b/front/src/modules/settings/data-model/hooks/useFieldPreview.ts new file mode 100644 index 000000000..4ff7a7248 --- /dev/null +++ b/front/src/modules/settings/data-model/hooks/useFieldPreview.ts @@ -0,0 +1,40 @@ +import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; +import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon'; +import { Field } from '~/generated-metadata/graphql'; +import { assertNotNull } from '~/utils/assert'; + +export const useFieldPreview = ({ + fieldMetadata, + objectMetadataId, +}: { + fieldMetadata: Partial>; + objectMetadataId: string; +}) => { + const { findObjectMetadataItemById } = useObjectMetadataItemForSettings(); + const objectMetadataItem = findObjectMetadataItemById(objectMetadataId); + + const { objects } = useFindManyObjectRecords({ + objectNamePlural: objectMetadataItem?.namePlural, + skip: !objectMetadataItem || !fieldMetadata.id, + }); + + const { Icon: ObjectIcon } = useLazyLoadIcon(objectMetadataItem?.icon ?? ''); + const { Icon: FieldIcon } = useLazyLoadIcon(fieldMetadata.icon ?? ''); + + const [firstRecord] = objects; + const fieldName = fieldMetadata.id + ? objectMetadataItem?.fields.find(({ id }) => id === fieldMetadata.id)?.name + : undefined; + const value = fieldName ? firstRecord?.[fieldName] : undefined; + + return { + entityId: firstRecord?.id || `${objectMetadataId}-no-records`, + FieldIcon, + fieldName: fieldName || `${fieldMetadata.type}-new-field`, + hasValue: assertNotNull(value), + ObjectIcon, + objectMetadataItem, + value, + }; +}; diff --git a/front/src/modules/settings/data-model/hooks/useRelationFieldPreview.ts b/front/src/modules/settings/data-model/hooks/useRelationFieldPreview.ts new file mode 100644 index 000000000..d5853136c --- /dev/null +++ b/front/src/modules/settings/data-model/hooks/useRelationFieldPreview.ts @@ -0,0 +1,29 @@ +import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useFindManyObjectRecords } from '@/object-record/hooks/useFindManyObjectRecords'; + +export const useRelationFieldPreview = ({ + relationObjectMetadataId, + skipDefaultValue, +}: { + relationObjectMetadataId?: string; + skipDefaultValue: boolean; +}) => { + const { findObjectMetadataItemById } = useObjectMetadataItemForSettings(); + + const relationObjectMetadataItem = relationObjectMetadataId + ? findObjectMetadataItemById(relationObjectMetadataId) + : undefined; + + const { objects: relationObjects } = useFindManyObjectRecords({ + objectNamePlural: relationObjectMetadataItem?.namePlural, + skip: skipDefaultValue || !relationObjectMetadataItem, + }); + + return { + defaultValue: relationObjects?.[0], + entityChipDisplayMapper: (fieldValue?: { id: string }) => ({ + name: fieldValue?.id || relationObjectMetadataItem?.labelSingular || '', + avatarType: 'squared' as const, + }), + }; +}; diff --git a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx index 7640d28a9..2942cef57 100644 --- a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx +++ b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx @@ -16,7 +16,7 @@ const StyledDataType = styled.div<{ value: FieldMetadataType }>` padding: 0 ${({ theme }) => theme.spacing(2)}; ${({ theme, value }) => - value === 'RELATION' + value === FieldMetadataType.Relation ? css` border-color: ${theme.color.purple20}; color: ${theme.color.purple}; diff --git a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index d0f33ff59..d4009cb77 100644 --- a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -6,7 +6,6 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; import { dataTypes } from '../../constants/dataTypes'; @@ -31,9 +30,6 @@ const StyledIconTableCell = styled(TableCell)` padding-right: ${({ theme }) => theme.spacing(1)}; `; -// TODO: remove "relation" type for now, add it back when the backend is ready. -const { RELATION: _, ...dataTypesWithoutRelation } = dataTypes; - export const SettingsObjectFieldItemTableRow = ({ ActionIcon, fieldItem, @@ -42,13 +38,11 @@ export const SettingsObjectFieldItemTableRow = ({ const { Icon } = useLazyLoadIcon(fieldItem.icon ?? ''); // TODO: parse with zod and merge types with FieldType (create a subset of FieldType for example) - const fieldDataTypeIsSupported = Object.keys( - dataTypesWithoutRelation, - ).includes(fieldItem.type); + const fieldDataTypeIsSupported = Object.keys(dataTypes).includes( + fieldItem.type, + ); - if (!fieldDataTypeIsSupported) { - return null; - } + if (!fieldDataTypeIsSupported) return null; return ( @@ -58,9 +52,7 @@ export const SettingsObjectFieldItemTableRow = ({ {fieldItem.isCustom ? 'Custom' : 'Standard'} - + {ActionIcon} diff --git a/front/src/modules/ui/display/chip/components/Chip.tsx b/front/src/modules/ui/display/chip/components/Chip.tsx index 92fb211f7..a9974dea5 100644 --- a/front/src/modules/ui/display/chip/components/Chip.tsx +++ b/front/src/modules/ui/display/chip/components/Chip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import { MouseEvent, ReactNode } from 'react'; import styled from '@emotion/styled'; import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip'; @@ -28,9 +28,10 @@ type ChipProps = { maxWidth?: string; variant?: ChipVariant; accent?: ChipAccent; - leftComponent?: React.ReactNode; - rightComponent?: React.ReactNode; + leftComponent?: ReactNode; + rightComponent?: ReactNode; className?: string; + onClick?: (event: MouseEvent) => void; }; const StyledContainer = styled.div>` @@ -125,6 +126,7 @@ export const Chip = ({ accent = ChipAccent.TextPrimary, maxWidth, className, + onClick, }: ChipProps) => ( {leftComponent} diff --git a/front/src/modules/ui/display/chip/components/EntityChip.tsx b/front/src/modules/ui/display/chip/components/EntityChip.tsx index 3eee413b3..f9d5bbda9 100644 --- a/front/src/modules/ui/display/chip/components/EntityChip.tsx +++ b/front/src/modules/ui/display/chip/components/EntityChip.tsx @@ -45,31 +45,31 @@ export const EntityChip = ({ }; return isNonEmptyString(name) ? ( -
- - ) : ( - - ) - } - /> -
+ + ) : ( + + ) + } + clickable={!!linkToEntity} + onClick={handleLinkClick} + /> ) : ( <> ); diff --git a/front/src/modules/ui/display/icon/index.ts b/front/src/modules/ui/display/icon/index.ts index 0e5399ed8..dd2252357 100644 --- a/front/src/modules/ui/display/icon/index.ts +++ b/front/src/modules/ui/display/icon/index.ts @@ -69,6 +69,9 @@ export { IconPlug, IconPlus, IconProgressCheck, + IconRelationManyToMany, + IconRelationOneToMany, + IconRelationOneToOne, IconRepeat, IconRobot, IconSearch, diff --git a/front/src/modules/ui/input/components/IconPicker.tsx b/front/src/modules/ui/input/components/IconPicker.tsx index 0f11065e5..587c5c80d 100644 --- a/front/src/modules/ui/input/components/IconPicker.tsx +++ b/front/src/modules/ui/input/components/IconPicker.tsx @@ -19,6 +19,7 @@ import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope'; type IconPickerProps = { disabled?: boolean; + dropdownScopeId?: string; onChange: (params: { iconKey: string; Icon: IconComponent }) => void; selectedIconKey?: string; onClickOutside?: () => void; @@ -44,6 +45,7 @@ const convertIconKeyToLabel = (iconKey: string) => export const IconPicker = ({ disabled, + dropdownScopeId = 'icon-picker', onChange, selectedIconKey, onClickOutside, @@ -53,7 +55,7 @@ export const IconPicker = ({ }: IconPickerProps) => { const [searchString, setSearchString] = useState(''); - const { closeDropdown } = useDropdown({ dropdownScopeId: 'icon-picker' }); + const { closeDropdown } = useDropdown({ dropdownScopeId }); const { icons, isLoadingIcons: isLoading } = useLazyLoadIcons(); @@ -75,7 +77,7 @@ export const IconPicker = ({ }, [icons, searchString, selectedIconKey]); return ( - + = { + className?: string; disabled?: boolean; dropdownScopeId: string; + label?: string; onChange?: (value: Value) => void; options: { value: Value; label: string; Icon?: IconComponent }[]; value?: Value; }; -const StyledContainer = styled.div<{ disabled?: boolean }>` +const StyledControlContainer = styled.div<{ disabled?: boolean }>` align-items: center; background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -34,7 +36,16 @@ const StyledContainer = styled.div<{ disabled?: boolean }>` padding: 0 ${({ theme }) => theme.spacing(2)}; `; -const StyledLabel = styled.div` +const StyledLabel = styled.span` + color: ${({ theme }) => theme.font.color.light}; + display: block; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; + text-transform: uppercase; +`; + +const StyledControlLabel = styled.div` align-items: center; display: flex; gap: ${({ theme }) => theme.spacing(1)}; @@ -46,8 +57,10 @@ const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` `; export const Select = ({ + className, disabled, dropdownScopeId, + label, onChange, options, value, @@ -59,46 +72,49 @@ export const Select = ({ const { closeDropdown } = useDropdown({ dropdownScopeId }); const selectControl = ( - - - {!!selectedOption.Icon && ( + + + {!!selectedOption?.Icon && ( )} - {selectedOption.label} - + {selectedOption?.label} + - + ); return disabled ? ( selectControl ) : ( - - {options.map((option) => ( - { - onChange?.(option.value); - closeDropdown(); - }} - /> - ))} - - } - dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }} - /> +
+ {!!label && {label}} + + {options.map((option) => ( + { + onChange?.(option.value); + closeDropdown(); + }} + /> + ))} + + } + dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }} + /> +
); }; diff --git a/front/src/modules/ui/layout/page/PagePanel.tsx b/front/src/modules/ui/layout/page/PagePanel.tsx index 433e67671..92a917a71 100644 --- a/front/src/modules/ui/layout/page/PagePanel.tsx +++ b/front/src/modules/ui/layout/page/PagePanel.tsx @@ -5,8 +5,6 @@ const StyledPanel = styled.div` background: ${({ theme }) => theme.background.primary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.md}; - display: flex; - flex-direction: row; height: 100%; overflow: auto; width: 100%; diff --git a/front/src/modules/ui/object/field/meta-types/display/utils/getEntityChipFromFieldMetadata.ts b/front/src/modules/ui/object/field/meta-types/display/utils/getEntityChipFromFieldMetadata.ts index a26c753a0..11ef68df9 100644 --- a/front/src/modules/ui/object/field/meta-types/display/utils/getEntityChipFromFieldMetadata.ts +++ b/front/src/modules/ui/object/field/meta-types/display/utils/getEntityChipFromFieldMetadata.ts @@ -7,9 +7,10 @@ export const getEntityChipFromFieldMetadata = ( fieldDefinition: FieldDefinition, fieldValue: any, ) => { + const { entityChipDisplayMapper } = fieldDefinition; const { fieldName } = fieldDefinition.metadata; - const chipValue: Pick< + const defaultChipValue: Pick< EntityChipProps, 'name' | 'pictureUrl' | 'avatarType' | 'entityId' > = { @@ -19,15 +20,23 @@ export const getEntityChipFromFieldMetadata = ( entityId: fieldValue?.id, }; - // TODO: use every - if (fieldName === 'accountOwner' && fieldValue) { - chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.lastName; - } else if (fieldName === 'company' && fieldValue) { - chipValue.name = fieldValue.name; - chipValue.pictureUrl = getLogoUrlFromDomainName(fieldValue.domainName); - } else if (fieldName === 'person' && fieldValue) { - chipValue.name = fieldValue.name.firstName + ' ' + fieldValue.name.lastName; + if (['accountOwner', 'person'].includes(fieldName) && fieldValue) { + return { + ...defaultChipValue, + name: `${fieldValue.firstName} ${fieldValue.lastName}`, + }; } - return chipValue; + if (fieldName === 'company' && fieldValue) { + return { + ...defaultChipValue, + name: fieldValue.name, + pictureUrl: getLogoUrlFromDomainName(fieldValue.domainName), + }; + } + + return { + ...defaultChipValue, + ...entityChipDisplayMapper?.(fieldValue), + }; }; diff --git a/front/src/modules/ui/object/field/types/FieldDefinition.ts b/front/src/modules/ui/object/field/types/FieldDefinition.ts index ba41ebfb6..357a03ba3 100644 --- a/front/src/modules/ui/object/field/types/FieldDefinition.ts +++ b/front/src/modules/ui/object/field/types/FieldDefinition.ts @@ -5,9 +5,10 @@ import { FieldMetadata } from './FieldMetadata'; import { FieldType } from './FieldType'; export type FieldDefinitionRelationType = - | 'TO_ONE_OBJECT' - | 'FROM_NAMY_OBJECTS' - | 'TO_MANY_OBJECTS'; + | 'FROM_MANY_OBJECTS' + | 'FROM_ONE_OBJECT' + | 'TO_MANY_OBJECTS' + | 'TO_ONE_OBJECT'; export type FieldDefinition = { fieldMetadataId: string; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldDateValue.ts b/front/src/modules/ui/object/field/types/guards/isFieldDateValue.ts index 0611a5fc7..4ea3a44b1 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldDateValue.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldDateValue.ts @@ -1,8 +1,20 @@ import { isNull, isString } from '@sniptt/guards'; +import { formatToHumanReadableDate } from '~/utils'; + import { FieldDateValue } from '../FieldMetadata'; // TODO: add zod export const isFieldDateValue = ( fieldValue: unknown, -): fieldValue is FieldDateValue => isNull(fieldValue) || isString(fieldValue); +): fieldValue is FieldDateValue => { + try { + if (isNull(fieldValue)) return true; + if (isString(fieldValue)) { + formatToHumanReadableDate(fieldValue); + return true; + } + } catch {} + + return false; +}; diff --git a/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index c985e885e..d090df5aa 100644 --- a/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -1,14 +1,16 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; +import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata'; import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection'; import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection'; +import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm'; import { AppPath } from '@/types/AppPath'; import { IconArchive, IconSettings } from '@/ui/display/icon'; import { H2Title } from '@/ui/display/typography/components/H2Title'; @@ -34,13 +36,22 @@ export const SettingsObjectFieldEdit = () => { metadataField.isActive && getFieldSlug(metadataField) === fieldSlug, ); - const [formValues, setFormValues] = useState< - Partial<{ - icon: string; - label: string; - description: string; - }> - >({}); + const { + relationFieldMetadataItem, + relationObjectMetadataItem, + relationType, + } = useRelationMetadata({ fieldMetadataItem: activeMetadataField }); + + const { + formValues, + handleFormChange, + hasFieldFormChanged, + hasFormChanged, + hasRelationFormChanged, + initForm, + isValid, + validatedFormValues, + } = useFieldMetadataForm(); useEffect(() => { if (loading) return; @@ -50,36 +61,59 @@ export const SettingsObjectFieldEdit = () => { return; } - if (!Object.keys(formValues).length) { - setFormValues({ - icon: activeMetadataField.icon ?? undefined, - label: activeMetadataField.label, - description: activeMetadataField.description ?? undefined, - }); - } + initForm({ + icon: activeMetadataField.icon ?? undefined, + label: activeMetadataField.label, + description: activeMetadataField.description ?? undefined, + type: activeMetadataField.type, + relation: { + field: { + icon: relationFieldMetadataItem?.icon, + label: relationFieldMetadataItem?.label, + }, + objectMetadataId: relationObjectMetadataItem?.id, + type: relationType, + }, + }); }, [ activeMetadataField, activeObjectMetadataItem, - formValues, + initForm, loading, navigate, + relationFieldMetadataItem?.icon, + relationFieldMetadataItem?.label, + relationObjectMetadataItem?.id, + relationType, ]); if (!activeObjectMetadataItem || !activeMetadataField) return null; - const areRequiredFieldsFilled = !!formValues.label; - - const hasChanges = - formValues.description !== activeMetadataField.description || - formValues.icon !== activeMetadataField.icon || - formValues.label !== activeMetadataField.label; - - const canSave = areRequiredFieldsFilled && hasChanges; + const canSave = isValid && hasFormChanged; const handleSave = async () => { - const editedField = { ...activeMetadataField, ...formValues }; + if (!validatedFormValues) return; - await editMetadataField(editedField); + if ( + validatedFormValues.type === FieldMetadataType.Relation && + relationFieldMetadataItem?.id && + hasRelationFormChanged + ) { + await editMetadataField({ + icon: validatedFormValues.relation.field.icon, + id: relationFieldMetadataItem.id, + label: validatedFormValues.relation.field.label, + }); + } + + if (hasFieldFormChanged) { + await editMetadataField({ + description: validatedFormValues.description, + icon: validatedFormValues.icon, + id: activeMetadataField.id, + label: validatedFormValues.label, + }); + } navigate(`/settings/objects/${objectSlug}`); }; @@ -116,23 +150,21 @@ export const SettingsObjectFieldEdit = () => { name={formValues.label} description={formValues.description} iconKey={formValues.icon} - onChange={(values) => - setFormValues((previousFormValues) => ({ - ...previousFormValues, - ...values, - })) - } + onChange={handleFormChange} />
diff --git a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index 6859282e5..50aecad7f 100644 --- a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { useCreateOneRelationMetadata } from '@/object-metadata/hooks/useCreateOneRelationMetadata'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useCreateOneObjectRecord } from '@/object-record/hooks/useCreateOneObjectRecord'; @@ -11,6 +12,7 @@ import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderCon import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection'; import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection'; +import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm'; import { AppPath } from '@/types/AppPath'; import { IconSettings } from '@/ui/display/icon'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; @@ -23,26 +25,48 @@ export const SettingsObjectNewFieldStep2 = () => { const navigate = useNavigate(); const { objectSlug = '' } = useParams(); - const { findActiveObjectMetadataItemBySlug, loading } = - useObjectMetadataItemForSettings(); + const { + findActiveObjectMetadataItemBySlug, + findObjectMetadataItemById, + findObjectMetadataItemByNamePlural, + loading, + } = useObjectMetadataItemForSettings(); const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); const { createMetadataField } = useFieldMetadataItem(); + const { + formValues, + handleFormChange, + initForm, + isValid: canSave, + validatedFormValues, + } = useFieldMetadataForm(); + useEffect(() => { if (loading) return; - if (!activeObjectMetadataItem) navigate(AppPath.NotFound); - }, [activeObjectMetadataItem, loading, navigate]); + if (!activeObjectMetadataItem) { + navigate(AppPath.NotFound); + return; + } - const [formValues, setFormValues] = useState<{ - description?: string; - icon: string; - label: string; - type: FieldMetadataType; - }>({ icon: 'IconUsers', label: '', type: FieldMetadataType.Number }); + initForm({ + relation: { + field: { icon: activeObjectMetadataItem.icon }, + objectMetadataId: findObjectMetadataItemByNamePlural('peopleV2')?.id, + }, + }); + }, [ + activeObjectMetadataItem, + findObjectMetadataItemByNamePlural, + initForm, + loading, + navigate, + ]); const [objectViews, setObjectViews] = useState([]); + const [relationObjectViews, setRelationObjectViews] = useState([]); const { createOneObject: createOneViewField } = useCreateOneObjectRecord({ objectNameSingular: 'viewFieldV2', @@ -57,32 +81,100 @@ export const SettingsObjectNewFieldStep2 = () => { onCompleted: async (data: PaginatedObjectTypeResults) => { const views = data.edges; - if (!views) { - return; - } + if (!views) return; setObjectViews(data.edges.map(({ node }) => node)); }, }); + useFindManyObjectRecords({ + objectNamePlural: 'viewsV2', + skip: !formValues.relation?.objectMetadataId, + filter: { + type: { eq: ViewType.Table }, + objectMetadataId: { eq: formValues.relation?.objectMetadataId }, + }, + onCompleted: async (data: PaginatedObjectTypeResults) => { + const views = data.edges; + + if (!views) return; + + setRelationObjectViews(data.edges.map(({ node }) => node)); + }, + }); + + const { createOneRelationMetadata } = useCreateOneRelationMetadata(); + if (!activeObjectMetadataItem) return null; - const canSave = !!formValues.label; - const handleSave = async () => { - const createdField = await createMetadataField({ - ...formValues, - objectMetadataId: activeObjectMetadataItem.id, - }); - objectViews.forEach(async (view) => { - await createOneViewField?.({ - view: view.id, - fieldMetadataId: createdField.data?.createOneField.id, - position: activeObjectMetadataItem.fields.length, - isVisible: true, - size: 100, + if (!validatedFormValues) return; + + if (validatedFormValues.type === FieldMetadataType.Relation) { + const createdRelation = await createOneRelationMetadata({ + relationType: validatedFormValues.relation.type, + field: { + description: validatedFormValues.description, + icon: validatedFormValues.icon, + label: validatedFormValues.label, + }, + objectMetadataId: activeObjectMetadataItem.id, + connect: { + field: { + icon: validatedFormValues.relation.field.icon, + label: validatedFormValues.relation.field.label, + }, + objectMetadataId: validatedFormValues.relation.objectMetadataId, + }, }); - }); + + const relationObjectMetadataItem = findObjectMetadataItemById( + validatedFormValues.relation.objectMetadataId, + ); + + objectViews.forEach(async (view) => { + await createOneViewField?.({ + view: view.id, + fieldMetadataId: + validatedFormValues.relation.type === 'MANY_TO_ONE' + ? createdRelation.data?.createOneRelation.toFieldMetadataId + : createdRelation.data?.createOneRelation.fromFieldMetadataId, + position: activeObjectMetadataItem.fields.length, + isVisible: true, + size: 100, + }); + }); + relationObjectViews.forEach(async (view) => { + await createOneViewField?.({ + view: view.id, + fieldMetadataId: + validatedFormValues.relation.type === 'MANY_TO_ONE' + ? createdRelation.data?.createOneRelation.fromFieldMetadataId + : createdRelation.data?.createOneRelation.toFieldMetadataId, + position: relationObjectMetadataItem?.fields.length, + isVisible: true, + size: 100, + }); + }); + } else { + const createdField = await createMetadataField({ + description: validatedFormValues.description, + icon: validatedFormValues.icon, + label: validatedFormValues.label, + objectMetadataId: activeObjectMetadataItem.id, + type: validatedFormValues.type, + }); + objectViews.forEach(async (view) => { + await createOneViewField?.({ + view: view.id, + fieldMetadataId: createdField.data?.createOneField.id, + position: activeObjectMetadataItem.fields.length, + isVisible: true, + size: 100, + }); + }); + } + navigate(`/settings/objects/${objectSlug}`); }; @@ -110,24 +202,19 @@ export const SettingsObjectNewFieldStep2 = () => { iconKey={formValues.icon} name={formValues.label} description={formValues.description} - onChange={(values) => - setFormValues((previousValues) => ({ - ...previousValues, - ...values, - })) - } + onChange={handleFormChange} /> - setFormValues((previousValues) => ({ ...previousValues, type })) - } + fieldMetadata={{ + icon: formValues.icon, + label: formValues.label || 'Employees', + }} + objectMetadataId={activeObjectMetadataItem.id} + onChange={handleFormChange} + values={{ + type: formValues.type, + relation: formValues.relation, + }} /> diff --git a/front/src/testing/mock-data/metadata.ts b/front/src/testing/mock-data/metadata.ts index 8bb13802f..ab06463e9 100644 --- a/front/src/testing/mock-data/metadata.ts +++ b/front/src/testing/mock-data/metadata.ts @@ -1,3 +1,187 @@ +export const mockedCompaniesMetadata = { + node: { + id: 'a3195559-cc20-4749-9565-572a2f506581', + dataSourceId: '', + nameSingular: 'company', + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: null, + icon: 'IconBuildingSkyscraper', + isCustom: false, + isActive: true, + createdAt: '', + updatedAt: '', + fields: { + edges: [ + { + node: { + id: '397eabc0-c5a1-4550-8e68-839c878a8d0e', + type: 'TEXT', + name: 'name', + label: 'Name', + description: 'The company name.', + placeholder: null, + icon: 'IconBuildingSkyscraper', + isCustom: false, + isActive: true, + isNullable: false, + createdAt: '', + updatedAt: '', + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, + { + node: { + id: '7ad234c7-f3b9-4efc-813c-43dc97070b07', + type: 'URL', + name: 'URL', + label: 'URL', + description: + 'The company website URL. We use this url to fetch the company icon.', + placeholder: null, + icon: 'IconLink', + isCustom: false, + isActive: true, + isNullable: true, + createdAt: '', + updatedAt: '', + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, + { + node: { + id: 'a578ffb2-13db-483c-ace7-5c30a13dff2d', + type: 'RELATION', + name: 'accountOwner', + label: 'Account Owner', + description: + 'Your team member responsible for managing the company account.', + placeholder: null, + icon: 'IconUserCircle', + isCustom: false, + isActive: true, + isNullable: true, + createdAt: '', + updatedAt: '', + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, + { + node: { + id: 'b7fd622d-7d8b-4f5a-b148-a7e9fd2c4660', + type: 'NUMBER', + name: 'employees', + label: 'Employees', + description: 'Number of employees in the company.', + placeholder: null, + icon: 'IconUsers', + isCustom: true, + isActive: true, + isNullable: true, + createdAt: '', + updatedAt: '', + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, + { + node: { + id: '60ab27ed-a959-471e-b583-887387f7accd', + type: 'URL', + name: 'linkedin', + label: 'Linkedin', + description: null, + placeholder: null, + icon: 'IconBrandLinkedin', + isCustom: false, + isActive: true, + isNullable: true, + createdAt: '', + updatedAt: '', + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, + { + node: { + id: '6daadb98-83ca-4c85-bca5-7792a7d958ad', + type: 'BOOLEAN', + name: 'prioritySupport', + label: 'Priority Support', + description: 'Whether the company has priority support.', + placeholder: null, + icon: 'IconHeadphones', + isCustom: true, + isActive: false, + isNullable: true, + createdAt: '', + updatedAt: '', + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + totalCount: 6, + }, + }, +}; + +export const mockedWorkspacesMetadata = { + node: { + id: 'c973efa3-436e-47ea-8dbc-983ed869c04d', + dataSourceId: '', + nameSingular: 'workspace', + namePlural: 'workspaces', + labelSingular: 'Workspace', + labelPlural: 'Workspaces', + description: null, + icon: 'IconApps', + isCustom: true, + isActive: true, + createdAt: '', + updatedAt: '', + fields: { + edges: [ + { + node: { + id: 'f955402c-9e8f-4b91-a82c-95f6de392b99', + type: 'TEXT', + name: 'slug', + label: 'Slug', + description: null, + placeholder: null, + icon: 'IconSlash', + isCustom: true, + isActive: true, + isNullable: true, + createdAt: '', + updatedAt: '', + fromRelationMetadata: null, + toRelationMetadata: null, + }, + }, + ], + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + totalCount: 1, + }, + }, +}; + export const mockedObjectMetadataItems = { edges: [ { @@ -198,188 +382,8 @@ export const mockedObjectMetadataItems = { }, }, }, - { - node: { - id: 'a3195559-cc20-4749-9565-572a2f506581', - dataSourceId: '', - nameSingular: 'company', - namePlural: 'companies', - labelSingular: 'Company', - labelPlural: 'Companies', - description: null, - icon: 'IconBuildingSkyscraper', - isCustom: false, - isActive: true, - createdAt: '', - updatedAt: '', - fields: { - edges: [ - { - node: { - id: '397eabc0-c5a1-4550-8e68-839c878a8d0e', - type: 'TEXT', - name: 'name', - label: 'Name', - description: 'The company name.', - placeholder: null, - icon: 'IconBuildingSkyscraper', - isCustom: false, - isActive: true, - isNullable: false, - createdAt: '', - updatedAt: '', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - }, - { - node: { - id: '7ad234c7-f3b9-4efc-813c-43dc97070b07', - type: 'URL', - name: 'URL', - label: 'URL', - description: - 'The company website URL. We use this url to fetch the company icon.', - placeholder: null, - icon: 'IconLink', - isCustom: false, - isActive: true, - isNullable: true, - createdAt: '', - updatedAt: '', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - }, - { - node: { - id: 'a578ffb2-13db-483c-ace7-5c30a13dff2d', - type: 'RELATION', - name: 'accountOwner', - label: 'Account Owner', - description: - 'Your team member responsible for managing the company account.', - placeholder: null, - icon: 'IconUserCircle', - isCustom: false, - isActive: true, - isNullable: true, - createdAt: '', - updatedAt: '', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - }, - { - node: { - id: 'b7fd622d-7d8b-4f5a-b148-a7e9fd2c4660', - type: 'NUMBER', - name: 'employees', - label: 'Employees', - description: 'Number of employees in the company.', - placeholder: null, - icon: 'IconUsers', - isCustom: true, - isActive: true, - isNullable: true, - createdAt: '', - updatedAt: '', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - }, - { - node: { - id: '60ab27ed-a959-471e-b583-887387f7accd', - type: 'URL', - name: 'linkedin', - label: 'Linkedin', - description: null, - placeholder: null, - icon: 'IconBrandLinkedin', - isCustom: false, - isActive: true, - isNullable: true, - createdAt: '', - updatedAt: '', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - }, - { - node: { - id: '6daadb98-83ca-4c85-bca5-7792a7d958ad', - type: 'BOOLEAN', - name: 'prioritySupport', - label: 'Priority Support', - description: 'Whether the company has priority support.', - placeholder: null, - icon: 'IconHeadphones', - isCustom: true, - isActive: false, - isNullable: true, - createdAt: '', - updatedAt: '', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - }, - ], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - endCursor: null, - }, - totalCount: 6, - }, - }, - }, - { - node: { - id: 'c973efa3-436e-47ea-8dbc-983ed869c04d', - dataSourceId: '', - nameSingular: 'workspace', - namePlural: 'workspaces', - labelSingular: 'Workspace', - labelPlural: 'Workspaces', - description: null, - icon: 'IconApps', - isCustom: true, - isActive: true, - createdAt: '', - updatedAt: '', - fields: { - edges: [ - { - node: { - id: 'f955402c-9e8f-4b91-a82c-95f6de392b99', - type: 'TEXT', - name: 'slug', - label: 'Slug', - description: null, - placeholder: null, - icon: 'IconSlash', - isCustom: true, - isActive: true, - isNullable: true, - createdAt: '', - updatedAt: '', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - }, - ], - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - startCursor: null, - endCursor: null, - }, - totalCount: 1, - }, - }, - }, + mockedCompaniesMetadata, + mockedWorkspacesMetadata, ], pageInfo: { hasNextPage: false, diff --git a/server/src/coreV2/workspace/workspace.entity.ts b/server/src/coreV2/workspace/workspace.entity.ts index 3a614a206..860282278 100644 --- a/server/src/coreV2/workspace/workspace.entity.ts +++ b/server/src/coreV2/workspace/workspace.entity.ts @@ -9,8 +9,8 @@ import { UpdateDateColumn, } from 'typeorm'; -@Entity('workspaces') -@ObjectType('workspace') +@Entity('workspaceV2') +@ObjectType('workspaceV2') export class Workspace { @IDField(() => ID) @PrimaryGeneratedColumn('uuid')