diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql index c325f41900..264bada594 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql @@ -1169,6 +1169,7 @@ type FieldConfiguration { """Display mode for field configuration widgets""" enum FieldDisplayMode { CARD + EDITOR FIELD VIEW } diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts index 3927612968..632c9c4332 100644 --- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts +++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts @@ -886,7 +886,7 @@ export interface FieldConfiguration { /** Display mode for field configuration widgets */ -export type FieldDisplayMode = 'CARD' | 'FIELD' | 'VIEW' +export type FieldDisplayMode = 'CARD' | 'EDITOR' | 'FIELD' | 'VIEW' export interface FieldRichTextConfiguration { configurationType: WidgetConfigurationType @@ -9203,6 +9203,7 @@ export const enumBarChartLayout = { export const enumFieldDisplayMode = { CARD: 'CARD' as const, + EDITOR: 'EDITOR' as const, FIELD: 'FIELD' as const, VIEW: 'VIEW' as const } diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index f322f2a853..142b2688c2 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1819,6 +1819,7 @@ export type FieldConnection = { /** Display mode for field configuration widgets */ export enum FieldDisplayMode { CARD = 'CARD', + EDITOR = 'EDITOR', FIELD = 'FIELD', VIEW = 'VIEW' } diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx index ed99d089d8..659e6fd08c 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal.tsx @@ -1,6 +1,6 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; @@ -53,9 +53,7 @@ export const RecordInlineCellAnchoredPortal = ({ prefix: instanceIdPrefix, }); - const anchorElement = document.body.querySelector( - `#${fieldInstanceId}`, - ); + const anchorElement = document.getElementById(fieldInstanceId); const isRecordFieldReadOnly = useIsRecordFieldReadOnly({ fieldMetadataId: fieldMetadataItem.id, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts index 4c814641b5..939e581d6b 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreateRecordPageFieldWidget.ts @@ -1,10 +1,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; +import { isDefined } from 'twenty-shared/utils'; import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext'; import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; import { createDefaultFieldWidget } from '@/page-layout/utils/createDefaultFieldWidget'; +import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields'; +import { getFieldWidgetDefaultDisplayMode } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig'; import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState'; import { useStore } from 'jotai'; @@ -21,9 +23,9 @@ export const useCreateRecordPageFieldWidget = () => { objectNameSingular: targetObjectNameSingular, }); - const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({ - objectNameSingular: targetObjectNameSingular, - }); + const allFieldWidgetFields = useFieldWidgetEligibleFields( + targetObjectNameSingular, + ); const { currentPageLayout } = useCurrentPageLayoutOrThrow(); @@ -52,12 +54,11 @@ export const useCreateRecordPageFieldWidget = () => { }), ); - const unusedRelationField = boxedRelationFieldMetadataItems.find( + const unusedField = allFieldWidgetFields.find( (field) => !usedFieldMetadataIds.has(field.id), ); - const selectedField = - unusedRelationField ?? boxedRelationFieldMetadataItems[0]; + const selectedField = unusedField ?? allFieldWidgetFields[0]; const fieldMetadataId = selectedField?.id ?? ''; const title = selectedField?.label ?? ''; @@ -70,6 +71,9 @@ export const useCreateRecordPageFieldWidget = () => { pageLayoutTabId: tabId, title, fieldMetadataId, + fieldDisplayMode: isDefined(selectedField) + ? getFieldWidgetDefaultDisplayMode(selectedField.type) + : undefined, objectMetadataId: objectMetadataItem.id, positionIndex, }); @@ -79,7 +83,7 @@ export const useCreateRecordPageFieldWidget = () => { tabs: addWidgetToTab(prev.tabs, tabId, newWidget), })); }, [ - boxedRelationFieldMetadataItems, + allFieldWidgetFields, currentPageLayout.tabs, objectMetadataItem.id, pageLayoutDraftState, diff --git a/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts index 5618824235..4d733a951b 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/createDefaultFieldWidget.ts @@ -11,6 +11,7 @@ export const createDefaultFieldWidget = ({ pageLayoutTabId, title, fieldMetadataId, + fieldDisplayMode = FieldDisplayMode.CARD, objectMetadataId, positionIndex, }: { @@ -18,6 +19,7 @@ export const createDefaultFieldWidget = ({ pageLayoutTabId: string; title: string; fieldMetadataId: string; + fieldDisplayMode?: FieldDisplayMode; objectMetadataId: string; positionIndex: number; }): PageLayoutWidget => { @@ -31,7 +33,7 @@ export const createDefaultFieldWidget = ({ __typename: 'FieldConfiguration', configurationType: WidgetConfigurationType.FIELD, fieldMetadataId, - fieldDisplayMode: FieldDisplayMode.CARD, + fieldDisplayMode, }, gridPosition: { __typename: 'GridPosition', diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidget.tsx index 4eda868e5b..85ce008403 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/components/FieldWidget.tsx @@ -3,10 +3,12 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useResolveFieldMetadataIdFromNameOrId'; import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { FieldWidgetDisplay } from '@/page-layout/widgets/field/components/FieldWidgetDisplay'; +import { FieldWidgetRichTextEditor } from '@/page-layout/widgets/field/components/FieldWidgetRichTextEditor'; import { FieldWidgetMorphRelationCard } from '@/page-layout/widgets/field/components/FieldWidgetMorphRelationCard'; import { FieldWidgetMorphRelationField } from '@/page-layout/widgets/field/components/FieldWidgetMorphRelationField'; import { FieldWidgetRelationCard } from '@/page-layout/widgets/field/components/FieldWidgetRelationCard'; @@ -135,6 +137,19 @@ export const FieldWidget = ({ widget }: FieldWidgetProps) => { ); } + if ( + isFieldRichText(fieldDefinition) && + fieldDisplayMode === FieldDisplayMode.EDITOR + ) { + return ( + + ); + } + return ( + import( + '@/object-record/record-field/ui/meta-types/input/components/RichTextFieldEditor' + ).then((module) => ({ + default: module.RichTextFieldEditor, + })), +); + +const StyledContainer = styled.div` + box-sizing: border-box; + display: flex; + flex-direction: column; + width: 100%; +`; + +const StyledEditorContainer = styled.div` + box-sizing: border-box; + padding-inline: 44px; + width: 100%; +`; + +const StyledSkeletonContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + padding: 0 ${themeCssVariables.spacing[4]}; +`; + +const LoadingSkeleton = () => { + const { theme } = useContext(ThemeContext); + return ( + + + + + + ); +}; + +type FieldWidgetRichTextEditorProps = { + fieldMetadataItem: FieldMetadataItem; + objectMetadataItem: EnrichedObjectMetadataItem; + recordId: string; +}; + +export const FieldWidgetRichTextEditor = ({ + fieldMetadataItem, + objectMetadataItem, + recordId, +}: FieldWidgetRichTextEditorProps) => { + const objectNameSingular = objectMetadataItem.nameSingular; + const fieldName = fieldMetadataItem.name; + + const fieldValue = useAtomFamilySelectorValue(recordStoreFamilySelector, { + recordId, + fieldName, + }); + + if (isUndefined(fieldValue)) { + return ; + } + + return ( + + + + }> + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/constants/fieldWidgetConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/constants/fieldWidgetConfig.ts new file mode 100644 index 0000000000..c8d3625a30 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/constants/fieldWidgetConfig.ts @@ -0,0 +1,24 @@ +import { FieldMetadataType } from 'twenty-shared/types'; +import { FieldDisplayMode } from '~/generated-metadata/graphql'; + +type FieldWidgetFieldTypeConfig = { + availableDisplayModes: FieldDisplayMode[]; + defaultDisplayMode: FieldDisplayMode; +}; + +export const FIELD_WIDGET_CONFIG: Partial< + Record +> = { + [FieldMetadataType.RELATION]: { + availableDisplayModes: [FieldDisplayMode.FIELD, FieldDisplayMode.CARD], + defaultDisplayMode: FieldDisplayMode.CARD, + }, + [FieldMetadataType.MORPH_RELATION]: { + availableDisplayModes: [FieldDisplayMode.FIELD, FieldDisplayMode.CARD], + defaultDisplayMode: FieldDisplayMode.CARD, + }, + [FieldMetadataType.RICH_TEXT]: { + availableDisplayModes: [FieldDisplayMode.FIELD, FieldDisplayMode.EDITOR], + defaultDisplayMode: FieldDisplayMode.EDITOR, + }, +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/constants/fieldWidgetSupportedFieldTypes.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/constants/fieldWidgetSupportedFieldTypes.ts new file mode 100644 index 0000000000..e887ef3ec5 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/constants/fieldWidgetSupportedFieldTypes.ts @@ -0,0 +1,7 @@ +import { type FieldMetadataType } from 'twenty-shared/types'; + +import { FIELD_WIDGET_CONFIG } from '@/page-layout/widgets/field/constants/fieldWidgetConfig'; + +export const FIELD_WIDGET_SUPPORTED_FIELD_TYPES = Object.keys( + FIELD_WIDGET_CONFIG, +) as FieldMetadataType[]; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields.ts new file mode 100644 index 0000000000..333140128e --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields.ts @@ -0,0 +1,16 @@ +import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; +import { FIELD_WIDGET_SUPPORTED_FIELD_TYPES } from '@/page-layout/widgets/field/constants/fieldWidgetSupportedFieldTypes'; +import { useMemo } from 'react'; + +export const useFieldWidgetEligibleFields = (objectNameSingular: string) => { + const { boxedRelationFieldMetadataItems, inlineFieldMetadataItems } = + useFieldListFieldMetadataItems({ objectNameSingular }); + + return useMemo(() => { + const eligibleInlineFields = inlineFieldMetadataItems.filter((field) => + FIELD_WIDGET_SUPPORTED_FIELD_TYPES.includes(field.type), + ); + + return [...boxedRelationFieldMetadataItems, ...eligibleInlineFields]; + }, [boxedRelationFieldMetadataItems, inlineFieldMetadataItems]); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig.ts b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig.ts new file mode 100644 index 0000000000..9f79d257e6 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig.ts @@ -0,0 +1,24 @@ +import { type FieldMetadataType } from 'twenty-shared/types'; +import { FieldDisplayMode } from '~/generated-metadata/graphql'; + +import { FIELD_WIDGET_CONFIG } from '@/page-layout/widgets/field/constants/fieldWidgetConfig'; + +export const getFieldWidgetConfig = (fieldType: FieldMetadataType) => + FIELD_WIDGET_CONFIG[fieldType]; + +export const getFieldWidgetDefaultDisplayMode = ( + fieldType: FieldMetadataType, +) => + getFieldWidgetConfig(fieldType)?.defaultDisplayMode ?? FieldDisplayMode.FIELD; + +export const getFieldWidgetAvailableDisplayModes = ( + fieldType: FieldMetadataType, +) => + getFieldWidgetConfig(fieldType)?.availableDisplayModes ?? [ + FieldDisplayMode.FIELD, + ]; + +export const isDisplayModeValidForFieldType = ( + fieldType: FieldMetadataType, + displayMode: FieldDisplayMode, +) => getFieldWidgetAvailableDisplayModes(fieldType).includes(displayMode); diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx index 8c6b374b2a..b4237767b9 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/SidePanelPageLayoutRecordPageWidgetTypeSelect.tsx @@ -1,7 +1,6 @@ import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { FIND_MANY_FRONT_COMPONENTS } from '@/front-components/graphql/queries/findManyFrontComponents'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; import { useInsertCreatedWidgetAtContext } from '@/page-layout/hooks/useInsertCreatedWidgetAtContext'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; @@ -14,6 +13,8 @@ import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils import { getWidgetConfigurationViewId } from '@/page-layout/utils/getWidgetConfigurationViewId'; import { isVerticalListPosition } from '@/page-layout/utils/isVerticalListPosition'; import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab'; +import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields'; +import { getFieldWidgetDefaultDisplayMode } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig'; import { useCreateViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useCreateViewForFieldsWidget'; import { useDeleteViewForFieldsWidget } from '@/page-layout/widgets/fields/hooks/useDeleteViewForFieldsWidget'; import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget'; @@ -102,9 +103,9 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { objectNameSingular: targetObjectNameSingular, }); - const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({ - objectNameSingular: targetObjectNameSingular, - }); + const allFieldWidgetFields = useFieldWidgetEligibleFields( + targetObjectNameSingular, + ); const editingWidgetTab = isDefined(pageLayoutEditingWidgetId) ? pageLayoutDraft.tabs.find((tab) => @@ -274,12 +275,11 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { }), ); - const unusedRelationField = boxedRelationFieldMetadataItems.find( + const unusedField = allFieldWidgetFields.find( (field) => !usedFieldMetadataIds.has(field.id), ); - const selectedField = - unusedRelationField ?? boxedRelationFieldMetadataItems[0]; + const selectedField = unusedField ?? allFieldWidgetFields[0]; const fieldMetadataId = selectedField?.id ?? ''; const title = selectedField?.label ?? ''; @@ -289,6 +289,9 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { pageLayoutTabId: tabId, title, fieldMetadataId, + fieldDisplayMode: isDefined(selectedField) + ? getFieldWidgetDefaultDisplayMode(selectedField.type) + : undefined, objectMetadataId: objectMetadataItem.id, positionIndex, }); @@ -307,7 +310,7 @@ export const SidePanelPageLayoutRecordPageWidgetTypeSelect = () => { resetNavigationStack: true, }); }, [ - boxedRelationFieldMetadataItems, + allFieldWidgetFields, getExistingWidgetPositionIndex, insertCreatedWidgetAtContext, navigatePageLayoutSidePanel, diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx index 24f9f4f421..1db99184da 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetFieldDropdownContent.tsx @@ -1,6 +1,11 @@ import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; -import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems'; +import { isDefined } from 'twenty-shared/utils'; import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; +import { useFieldWidgetEligibleFields } from '@/page-layout/widgets/field/hooks/useFieldWidgetEligibleFields'; +import { + getFieldWidgetDefaultDisplayMode, + isDisplayModeValidForFieldType, +} from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig'; import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore'; import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode'; @@ -35,9 +40,8 @@ export const FieldWidgetFieldDropdownContent = () => { const currentFieldMetadataId = fieldConfiguration?.fieldMetadataId; - const { boxedRelationFieldMetadataItems } = useFieldListFieldMetadataItems({ - objectNameSingular, - }); + const allFieldWidgetFieldMetadataItems = + useFieldWidgetEligibleFields(objectNameSingular); const dropdownId = useAvailableComponentInstanceIdOrThrow( DropdownComponentInstanceContext, @@ -58,7 +62,7 @@ export const FieldWidgetFieldDropdownContent = () => { const { getIcon } = useIcons(); const availableFields = filterBySearchQuery({ - items: boxedRelationFieldMetadataItems, + items: allFieldWidgetFieldMetadataItems, searchQuery, getSearchableValues: (item) => [item.label], }); @@ -67,16 +71,28 @@ export const FieldWidgetFieldDropdownContent = () => { useFieldMetadataItemById(currentFieldMetadataId ?? ''); const handleSelectField = (fieldMetadataId: string) => { + const selectedField = allFieldWidgetFieldMetadataItems.find( + (field) => field.id === fieldMetadataId, + ); + + const currentDisplayMode = fieldConfiguration?.fieldDisplayMode; + + const needsDisplayModeSwitch = + isDefined(selectedField) && + isDefined(currentDisplayMode) && + !isDisplayModeValidForFieldType(selectedField.type, currentDisplayMode); + updateCurrentWidgetConfig({ configToUpdate: { fieldMetadataId, + ...(needsDisplayModeSwitch && { + fieldDisplayMode: getFieldWidgetDefaultDisplayMode( + selectedField.type, + ), + }), }, }); - const selectedField = boxedRelationFieldMetadataItems.find( - (field) => field.id === fieldMetadataId, - ); - if (widgetInEditMode && selectedField) { updatePageLayoutWidget(widgetInEditMode.id, { title: selectedField.label, diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx index 3b730477cd..6d55db2bb7 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/dropdown-content/FieldWidgetLayoutDropdownContent.tsx @@ -1,3 +1,5 @@ +import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById'; +import { getFieldWidgetAvailableDisplayModes } from '@/page-layout/widgets/field/utils/getFieldWidgetDisplayModeConfig'; import { usePageLayoutIdFromContextStore } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdFromContextStore'; import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; import { useWidgetInEditMode } from '@/side-panel/pages/page-layout/hooks/useWidgetInEditMode'; @@ -10,23 +12,25 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue'; import { useLingui } from '@lingui/react/macro'; -import { IconLayoutKanban, IconListDetails } from 'twenty-ui/display'; +import { useMemo } from 'react'; +import { + type IconComponent, + IconFileText, + IconLayoutKanban, + IconListDetails, +} from 'twenty-ui/display'; import { MenuItemSelect } from 'twenty-ui/navigation'; import { FieldDisplayMode, type FieldConfiguration, } from '~/generated-metadata/graphql'; -const LAYOUT_OPTIONS = [ - { - id: FieldDisplayMode.FIELD, - Icon: IconListDetails, - }, - { - id: FieldDisplayMode.CARD, - Icon: IconLayoutKanban, - }, -] as const; +const DISPLAY_MODE_ICONS: Record = { + [FieldDisplayMode.FIELD]: IconListDetails, + [FieldDisplayMode.CARD]: IconLayoutKanban, + [FieldDisplayMode.EDITOR]: IconFileText, + [FieldDisplayMode.VIEW]: IconListDetails, +}; export const FieldWidgetLayoutDropdownContent = () => { const { t } = useLingui(); @@ -40,6 +44,19 @@ export const FieldWidgetLayoutDropdownContent = () => { | undefined; const currentDisplayMode = fieldConfiguration?.fieldDisplayMode; + const currentFieldMetadataId = fieldConfiguration?.fieldMetadataId; + + const { fieldMetadataItem } = useFieldMetadataItemById( + currentFieldMetadataId ?? '', + ); + + const layoutOptions = useMemo( + () => + fieldMetadataItem + ? getFieldWidgetAvailableDisplayModes(fieldMetadataItem.type) + : [FieldDisplayMode.FIELD], + [fieldMetadataItem], + ); const dropdownId = useAvailableComponentInstanceIdOrThrow( DropdownComponentInstanceContext, @@ -64,9 +81,10 @@ export const FieldWidgetLayoutDropdownContent = () => { closeDropdown(); }; - const layoutLabels: Record<(typeof LAYOUT_OPTIONS)[number]['id'], string> = { + const layoutLabels: Record = { [FieldDisplayMode.FIELD]: t`Field`, [FieldDisplayMode.CARD]: t`Card`, + [FieldDisplayMode.EDITOR]: t`Editor`, }; return ( @@ -74,23 +92,23 @@ export const FieldWidgetLayoutDropdownContent = () => { option.id)} + selectableItemIdArray={layoutOptions} > - {LAYOUT_OPTIONS.map((option) => ( + {layoutOptions.map((displayMode) => ( { - handleSelectLayout(option.id); + handleSelectLayout(displayMode); }} > { - handleSelectLayout(option.id); + handleSelectLayout(displayMode); }} /> diff --git a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx index 627b7c0d77..e341f412be 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/page-layout/components/record-page/SidePanelRecordPageFieldSettings.tsx @@ -62,6 +62,7 @@ export const SidePanelRecordPageFieldSettings = () => { const displayModeLabels: Record = { [FieldDisplayMode.FIELD]: t`Field`, [FieldDisplayMode.CARD]: t`Card`, + [FieldDisplayMode.EDITOR]: t`Editor`, }; const layoutLabel = isDefined(currentDisplayMode) diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum.ts index 42d7d7a8b2..ba8dcd6fe3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/enums/field-display-mode.enum.ts @@ -2,6 +2,7 @@ import { registerEnumType } from '@nestjs/graphql'; export enum FieldDisplayMode { CARD = 'CARD', + EDITOR = 'EDITOR', FIELD = 'FIELD', VIEW = 'VIEW', } diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts index 2dd2cecf4c..64bfc533bd 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-widget/utils/validate-widget-configuration-input.util.ts @@ -2,6 +2,7 @@ import { isNotEmptyObject, type ValidationError } from 'class-validator'; import { AggregateChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/aggregate-chart-configuration.dto'; import { BarChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/bar-chart-configuration.dto'; +import { FieldConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/field-configuration.dto'; import { FrontComponentConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/front-component-configuration.dto'; import { GaugeChartConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/gauge-chart-configuration.dto'; import { IframeConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/iframe-configuration.dto'; @@ -133,10 +134,11 @@ export const validateWidgetConfigurationInput = ({ PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, ); case WidgetConfigurationType.FIELD: - throw new PageLayoutWidgetException( - 'Field configuration is not supported yet', - PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA, + errors = validateWidgetConfigurationByDto( + FieldConfigurationDTO, + configuration, ); + break; case WidgetConfigurationType.FIELDS: throw new PageLayoutWidgetException( 'Fields configuration is not supported yet', diff --git a/packages/twenty-shared/src/types/page-layout/page-layout-widget-configuration.type.ts b/packages/twenty-shared/src/types/page-layout/page-layout-widget-configuration.type.ts index 2958271e44..52f331a0e0 100644 --- a/packages/twenty-shared/src/types/page-layout/page-layout-widget-configuration.type.ts +++ b/packages/twenty-shared/src/types/page-layout/page-layout-widget-configuration.type.ts @@ -99,7 +99,7 @@ export type RecordTableConfiguration = { export type FieldConfiguration = { configurationType: 'FIELD'; fieldMetadataId: string; - fieldDisplayMode: 'CARD' | 'FIELD' | 'VIEW'; + fieldDisplayMode: 'CARD' | 'EDITOR' | 'FIELD' | 'VIEW'; }; export type FieldsConfiguration = {