Add rich-text field widget (#19512)
## Context - Extend the FIELD widget to support RICH_TEXT fields alongside existing RELATION/MORPH_RELATION fields - Add EDITOR display mode that renders a full rich text editor, and FIELD display mode that shows a compact single-line preview - Enforce mutual exclusivity: EDITOR is only available for RICH_TEXT, CARD only for RELATION, FIELD for both - Refactor widget configuration into a centralized FIELD_WIDGET_CONFIG constant and shared useFieldWidgetEligibleFields hook for better scalability. Later we might use discriminative union from graphql scehma) <details> <summary> EDITOR mode </summary> <img width="1095" height="731" alt="Screenshot 2026-04-09 at 17 31 41" src="https://github.com/user-attachments/assets/cebffd0e-07ea-4f74-a3dc-ef987daa17ea" /> </details> <details> <summary> FIELD mode </summary> <img width="986" height="378" alt="Screenshot 2026-04-09 at 17 35 00" src="https://github.com/user-attachments/assets/c90a8046-fdd0-4321-8ba6-f47d89e9d42a" /> </details> <details> <summary> Open FIELD mode </summary> <img width="758" height="480" alt="Screenshot 2026-04-09 at 17 35 04" src="https://github.com/user-attachments/assets/c53cc120-0b6f-47a0-808c-27b26f9f53ec" /> </details>
This commit is contained in:
+2
-4
@@ -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<HTMLAnchorElement>(
|
||||
`#${fieldInstanceId}`,
|
||||
);
|
||||
const anchorElement = document.getElementById(fieldInstanceId);
|
||||
|
||||
const isRecordFieldReadOnly = useIsRecordFieldReadOnly({
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
|
||||
+12
-8
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
+15
@@ -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 (
|
||||
<FieldWidgetRichTextEditor
|
||||
fieldMetadataItem={fieldMetadataItem}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
recordId={targetRecord.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldWidgetDisplay
|
||||
fieldDefinition={fieldDefinition}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { lazy, Suspense, useContext } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const RichTextFieldEditor = lazy(() =>
|
||||
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 (
|
||||
<StyledSkeletonContainer>
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={theme.border.radius.sm}
|
||||
>
|
||||
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
|
||||
</SkeletonTheme>
|
||||
</StyledSkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
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 <LoadingSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ScrollWrapper
|
||||
componentInstanceId={`scroll-wrapper-field-widget-rich-text-${recordId}-${fieldName}`}
|
||||
>
|
||||
<StyledEditorContainer>
|
||||
<Suspense fallback={<LoadingSkeleton />}>
|
||||
<RichTextFieldEditor
|
||||
recordId={recordId}
|
||||
objectNameSingular={objectNameSingular}
|
||||
fieldName={fieldName}
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledEditorContainer>
|
||||
</ScrollWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+24
@@ -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, FieldWidgetFieldTypeConfig>
|
||||
> = {
|
||||
[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,
|
||||
},
|
||||
};
|
||||
+7
@@ -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[];
|
||||
+16
@@ -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]);
|
||||
};
|
||||
+24
@@ -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);
|
||||
+11
-8
@@ -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,
|
||||
|
||||
+25
-9
@@ -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,
|
||||
|
||||
+40
-22
@@ -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, IconComponent> = {
|
||||
[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<string, string> = {
|
||||
[FieldDisplayMode.FIELD]: t`Field`,
|
||||
[FieldDisplayMode.CARD]: t`Card`,
|
||||
[FieldDisplayMode.EDITOR]: t`Editor`,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -74,23 +92,23 @@ export const FieldWidgetLayoutDropdownContent = () => {
|
||||
<SelectableList
|
||||
selectableListInstanceId={dropdownId}
|
||||
focusId={dropdownId}
|
||||
selectableItemIdArray={LAYOUT_OPTIONS.map((option) => option.id)}
|
||||
selectableItemIdArray={layoutOptions}
|
||||
>
|
||||
{LAYOUT_OPTIONS.map((option) => (
|
||||
{layoutOptions.map((displayMode) => (
|
||||
<SelectableListItem
|
||||
key={option.id}
|
||||
itemId={option.id}
|
||||
key={displayMode}
|
||||
itemId={displayMode}
|
||||
onEnter={() => {
|
||||
handleSelectLayout(option.id);
|
||||
handleSelectLayout(displayMode);
|
||||
}}
|
||||
>
|
||||
<MenuItemSelect
|
||||
text={layoutLabels[option.id]}
|
||||
selected={currentDisplayMode === option.id}
|
||||
focused={selectedItemId === option.id}
|
||||
LeftIcon={option.Icon}
|
||||
text={layoutLabels[displayMode]}
|
||||
selected={currentDisplayMode === displayMode}
|
||||
focused={selectedItemId === displayMode}
|
||||
LeftIcon={DISPLAY_MODE_ICONS[displayMode]}
|
||||
onClick={() => {
|
||||
handleSelectLayout(option.id);
|
||||
handleSelectLayout(displayMode);
|
||||
}}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
|
||||
+1
@@ -62,6 +62,7 @@ export const SidePanelRecordPageFieldSettings = () => {
|
||||
const displayModeLabels: Record<string, string> = {
|
||||
[FieldDisplayMode.FIELD]: t`Field`,
|
||||
[FieldDisplayMode.CARD]: t`Card`,
|
||||
[FieldDisplayMode.EDITOR]: t`Editor`,
|
||||
};
|
||||
|
||||
const layoutLabel = isDefined(currentDisplayMode)
|
||||
|
||||
Reference in New Issue
Block a user