Baptiste Devessier
2026-03-04 12:45:14 +01:00
committed by GitHub
parent 3b2bf39565
commit 5b544809f7
36 changed files with 1839 additions and 282 deletions
@@ -1,6 +1,10 @@
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
@@ -54,6 +58,29 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
componentInstanceId,
);
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
componentInstanceId,
);
const fieldsWidgetUngroupedFieldsPersistedState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsPersistedComponentState,
componentInstanceId,
);
const fieldsWidgetEditorModeDraftState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModeDraftComponentState,
componentInstanceId,
);
const fieldsWidgetEditorModePersistedState =
useAtomComponentStateCallbackState(
fieldsWidgetEditorModePersistedComponentState,
componentInstanceId,
);
const resetDraftPageLayoutToPersistedPageLayout = useCallback(() => {
const pageLayoutPersisted = store.get(pageLayoutPersistedState);
@@ -83,6 +110,22 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
fieldsWidgetGroupsPersistedState,
);
store.set(fieldsWidgetGroupsDraftState, fieldsWidgetGroupsPersisted);
const fieldsWidgetUngroupedFieldsPersisted = store.get(
fieldsWidgetUngroupedFieldsPersistedState,
);
store.set(
fieldsWidgetUngroupedFieldsDraftState,
fieldsWidgetUngroupedFieldsPersisted,
);
const fieldsWidgetEditorModePersisted = store.get(
fieldsWidgetEditorModePersistedState,
);
store.set(
fieldsWidgetEditorModeDraftState,
fieldsWidgetEditorModePersisted,
);
}
}, [
pageLayoutDraftState,
@@ -90,6 +133,10 @@ export const useResetDraftPageLayoutToPersistedPageLayout = (
pageLayoutCurrentLayoutsState,
fieldsWidgetGroupsDraftState,
fieldsWidgetGroupsPersistedState,
fieldsWidgetUngroupedFieldsDraftState,
fieldsWidgetUngroupedFieldsPersistedState,
fieldsWidgetEditorModeDraftState,
fieldsWidgetEditorModePersistedState,
activeTabId,
store,
]);
@@ -1,6 +1,10 @@
import { UPSERT_FIELDS_WIDGET } from '@/page-layout/graphql/mutations/upsertFieldsWidget';
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useRefreshAllCoreViews } from '@/views/hooks/useRefreshAllCoreViews';
import { useMutation } from '@apollo/client';
@@ -50,6 +54,29 @@ export const useSaveFieldsWidgetGroups = ({
pageLayoutId,
);
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const fieldsWidgetUngroupedFieldsPersistedState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsPersistedComponentState,
pageLayoutId,
);
const fieldsWidgetEditorModeDraftState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModeDraftComponentState,
pageLayoutId,
);
const fieldsWidgetEditorModePersistedState =
useAtomComponentStateCallbackState(
fieldsWidgetEditorModePersistedComponentState,
pageLayoutId,
);
const [upsertFieldsWidgetMutation] = useMutation<
UpsertFieldsWidgetResult,
{ input: UpsertFieldsWidgetInput }
@@ -62,25 +89,57 @@ export const useSaveFieldsWidgetGroups = ({
const saveFieldsWidgetGroups = useCallback(async () => {
const allDraftGroups = store.get(fieldsWidgetGroupsDraftState);
const allPersistedGroups = store.get(fieldsWidgetGroupsPersistedState);
const allUngroupedFieldsDraft = store.get(
fieldsWidgetUngroupedFieldsDraftState,
);
const allEditorModes = store.get(fieldsWidgetEditorModeDraftState);
const widgetIds = new Set([
...Object.keys(allDraftGroups),
...Object.keys(allPersistedGroups),
...Object.keys(allUngroupedFieldsDraft),
]);
for (const widgetId of widgetIds) {
const draftGroups = allDraftGroups[widgetId] ?? [];
const editorMode = allEditorModes[widgetId] ?? 'ungrouped';
await upsertFieldsWidgetMutation({
variables: {
input: {
widgetId,
groups: draftGroups.map((group) => ({
id: group.id,
name: group.name,
position: group.position,
isVisible: group.isVisible,
fields: group.fields.flatMap((field) => {
if (editorMode === 'grouped') {
const draftGroups = allDraftGroups[widgetId] ?? [];
await upsertFieldsWidgetMutation({
variables: {
input: {
widgetId,
groups: draftGroups.map((group) => ({
id: group.id,
name: group.name,
position: group.position,
isVisible: group.isVisible,
fields: group.fields.flatMap((field) => {
if (!isDefined(field.viewFieldId)) {
return [];
}
return [
{
viewFieldId: field.viewFieldId,
isVisible: field.isVisible,
position: field.position,
},
];
}),
})),
},
},
});
} else {
const ungroupedFields = allUngroupedFieldsDraft[widgetId] ?? [];
await upsertFieldsWidgetMutation({
variables: {
input: {
widgetId,
fields: ungroupedFields.flatMap((field) => {
if (!isDefined(field.viewFieldId)) {
return [];
}
@@ -93,13 +152,18 @@ export const useSaveFieldsWidgetGroups = ({
},
];
}),
})),
},
},
},
});
});
}
}
store.set(fieldsWidgetGroupsPersistedState, allDraftGroups);
store.set(
fieldsWidgetUngroupedFieldsPersistedState,
allUngroupedFieldsDraft,
);
store.set(fieldsWidgetEditorModePersistedState, allEditorModes);
await refreshAllCoreViews();
@@ -107,6 +171,10 @@ export const useSaveFieldsWidgetGroups = ({
}, [
fieldsWidgetGroupsDraftState,
fieldsWidgetGroupsPersistedState,
fieldsWidgetUngroupedFieldsDraftState,
fieldsWidgetUngroupedFieldsPersistedState,
fieldsWidgetEditorModeDraftState,
fieldsWidgetEditorModePersistedState,
upsertFieldsWidgetMutation,
refreshAllCoreViews,
store,
@@ -56,15 +56,16 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
result.response.data?.updatePageLayoutWithTabsAndWidgets;
if (isDefined(updatedPageLayout)) {
const serverLayout: PageLayout = transformPageLayout(updatedPageLayout);
const persistedLayout: PageLayout =
transformPageLayout(updatedPageLayout);
const pageLayoutToPersist =
serverLayout.type === PageLayoutType.RECORD_PAGE
persistedLayout.type === PageLayoutType.RECORD_PAGE
? reInjectDynamicRelationWidgetsFromDraft(
serverLayout,
persistedLayout,
pageLayoutDraft,
)
: serverLayout;
: persistedLayout;
store.set(pageLayoutPersistedCallbackState, pageLayoutToPersist);
store.set(
@@ -0,0 +1,11 @@
import { type FieldsWidgetEditorMode } from '@/page-layout/widgets/fields/types/FieldsWidgetEditorMode';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const fieldsWidgetEditorModeDraftComponentState =
createAtomComponentState<Record<string, FieldsWidgetEditorMode>>({
key: 'fieldsWidgetEditorModeDraftComponentState',
defaultValue: {},
componentInstanceContext: PageLayoutComponentInstanceContext,
});
@@ -0,0 +1,11 @@
import { type FieldsWidgetEditorMode } from '@/page-layout/widgets/fields/types/FieldsWidgetEditorMode';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const fieldsWidgetEditorModePersistedComponentState =
createAtomComponentState<Record<string, FieldsWidgetEditorMode>>({
key: 'fieldsWidgetEditorModePersistedComponentState',
defaultValue: {},
componentInstanceContext: PageLayoutComponentInstanceContext,
});
@@ -0,0 +1,11 @@
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const fieldsWidgetUngroupedFieldsDraftComponentState =
createAtomComponentState<Record<string, FieldsWidgetGroupField[]>>({
key: 'fieldsWidgetUngroupedFieldsDraftComponentState',
defaultValue: {},
componentInstanceContext: PageLayoutComponentInstanceContext,
});
@@ -0,0 +1,11 @@
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
import { PageLayoutComponentInstanceContext } from './contexts/PageLayoutComponentInstanceContext';
export const fieldsWidgetUngroupedFieldsPersistedComponentState =
createAtomComponentState<Record<string, FieldsWidgetGroupField[]>>({
key: 'fieldsWidgetUngroupedFieldsPersistedComponentState',
defaultValue: {},
componentInstanceContext: PageLayoutComponentInstanceContext,
});
@@ -2,7 +2,11 @@ import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { convertPageLayoutDraftToUpdateInput } from '@/page-layout/utils/convertPageLayoutDraftToUpdateInput';
import { DYNAMIC_RELATION_WIDGET_ID_PREFIX } from '@/page-layout/utils/isDynamicRelationWidget';
import { PageLayoutType, WidgetType } from '~/generated-metadata/graphql';
import {
PageLayoutTabLayoutMode,
PageLayoutType,
WidgetType,
} from '~/generated-metadata/graphql';
const makeWidget = (
overrides: Partial<PageLayoutWidget> & { id: string },
@@ -134,4 +138,78 @@ describe('convertPageLayoutDraftToUpdateInput', () => {
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets.map((w) => w.id)).toEqual(['w1', 'w2']);
});
it('should produce VERTICAL_LIST position with index from widget.position', () => {
const widget = makeWidget({
id: 'w1',
position: {
__typename: 'PageLayoutWidgetVerticalListPosition',
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 3,
},
});
const draft = makeDraft([makeTab('tab-1', [widget])]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets[0].position).toEqual({
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 3,
});
});
it('should fall back to array index for VERTICAL_LIST when position typename is missing', () => {
const widget = makeWidget({
id: 'w1',
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
} as PageLayoutWidget['position'],
});
const draft = makeDraft([
makeTab('tab-1', [makeWidget({ id: 'w0' }), widget]),
]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets[1].position).toEqual({
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 1,
});
});
it('should produce CANVAS position without grid fields', () => {
const widget = makeWidget({
id: 'w1',
position: {
__typename: 'PageLayoutWidgetCanvasPosition',
layoutMode: PageLayoutTabLayoutMode.CANVAS,
},
});
const draft = makeDraft([makeTab('tab-1', [widget])]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets[0].position).toEqual({
layoutMode: PageLayoutTabLayoutMode.CANVAS,
});
});
it('should produce GRID position from gridPosition when position is null', () => {
const widget = makeWidget({
id: 'w1',
position: null,
gridPosition: { row: 1, column: 2, rowSpan: 3, columnSpan: 4 },
});
const draft = makeDraft([makeTab('tab-1', [widget])]);
const result = convertPageLayoutDraftToUpdateInput(draft);
expect(result.tabs[0].widgets[0].position).toEqual({
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 1,
column: 2,
rowSpan: 3,
columnSpan: 4,
});
});
});
@@ -24,7 +24,7 @@ const makeDynamicRelationWidget = (suffix: string): PageLayoutWidget =>
title: `Dynamic Relation ${suffix}`,
});
const makeServerLayout = (tabs: PageLayout['tabs']): PageLayout =>
const makePersistedLayout = (tabs: PageLayout['tabs']): PageLayout =>
({
id: 'layout-1',
name: 'Layout',
@@ -58,17 +58,20 @@ const makeTab = (
}) as PageLayout['tabs'][number];
describe('reInjectDynamicRelationWidgetsFromDraft', () => {
it('should return server layout unchanged when draft has no dynamic widgets', () => {
const serverLayout = makeServerLayout([
it('should return persisted layout unchanged when draft has no dynamic widgets', () => {
const persistedLayout = makePersistedLayout([
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
]);
const draft = makeDraftLayout([
makeTab('tab-1', [makeWidget({ id: 'w1' })]),
]);
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
const result = reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
draft,
);
expect(result).toBe(serverLayout);
expect(result).toBe(persistedLayout);
});
it('should inject dynamic widgets after the first FIELDS widget', () => {
@@ -82,14 +85,17 @@ describe('reInjectDynamicRelationWidgetsFromDraft', () => {
});
const dynamicWidget = makeDynamicRelationWidget('relation-1');
const serverLayout = makeServerLayout([
const persistedLayout = makePersistedLayout([
makeTab('tab-1', [fieldsWidget, timelineWidget]),
]);
const draft = makeDraftLayout([
makeTab('tab-1', [fieldsWidget, dynamicWidget, timelineWidget]),
]);
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
const result = reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
draft,
);
expect(result.tabs[0].widgets).toHaveLength(3);
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
@@ -106,12 +112,17 @@ describe('reInjectDynamicRelationWidgetsFromDraft', () => {
});
const dynamicWidget = makeDynamicRelationWidget('relation-1');
const serverLayout = makeServerLayout([makeTab('tab-1', [timelineWidget])]);
const persistedLayout = makePersistedLayout([
makeTab('tab-1', [timelineWidget]),
]);
const draft = makeDraftLayout([
makeTab('tab-1', [timelineWidget, dynamicWidget]),
]);
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
const result = reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
draft,
);
expect(result.tabs[0].widgets).toHaveLength(2);
expect(result.tabs[0].widgets[0].id).toBe('timeline-1');
@@ -131,7 +142,7 @@ describe('reInjectDynamicRelationWidgetsFromDraft', () => {
});
const dynamicWidget = makeDynamicRelationWidget('rel-1');
const serverLayout = makeServerLayout([
const persistedLayout = makePersistedLayout([
makeTab('tab-1', [fieldsWidget1]),
makeTab('tab-2', [fieldsWidget2]),
]);
@@ -140,7 +151,10 @@ describe('reInjectDynamicRelationWidgetsFromDraft', () => {
makeTab('tab-2', [fieldsWidget2]),
]);
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
const result = reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
draft,
);
// Tab 1 should have dynamic widget injected
expect(result.tabs[0].widgets).toHaveLength(2);
@@ -156,12 +170,17 @@ describe('reInjectDynamicRelationWidgetsFromDraft', () => {
const dynamicWidget1 = makeDynamicRelationWidget('rel-1');
const dynamicWidget2 = makeDynamicRelationWidget('rel-2');
const serverLayout = makeServerLayout([makeTab('tab-1', [fieldsWidget])]);
const persistedLayout = makePersistedLayout([
makeTab('tab-1', [fieldsWidget]),
]);
const draft = makeDraftLayout([
makeTab('tab-1', [fieldsWidget, dynamicWidget1, dynamicWidget2]),
]);
const result = reInjectDynamicRelationWidgetsFromDraft(serverLayout, draft);
const result = reInjectDynamicRelationWidgetsFromDraft(
persistedLayout,
draft,
);
expect(result.tabs[0].widgets).toHaveLength(3);
expect(result.tabs[0].widgets[0].id).toBe('fields-1');
@@ -1,10 +1,43 @@
import { type DraftPageLayout } from '@/page-layout/types/DraftPageLayout';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWidget';
import {
PageLayoutTabLayoutMode,
type UpdatePageLayoutWithTabsInput,
} from '~/generated-metadata/graphql';
const buildWidgetPosition = (widget: PageLayoutWidget, widgetIndex: number) => {
const layoutMode =
widget.position?.layoutMode ?? PageLayoutTabLayoutMode.GRID;
switch (layoutMode) {
case PageLayoutTabLayoutMode.VERTICAL_LIST: {
const index =
widget.position?.__typename === 'PageLayoutWidgetVerticalListPosition'
? widget.position.index
: widgetIndex;
return {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index,
};
}
case PageLayoutTabLayoutMode.CANVAS:
return {
layoutMode: PageLayoutTabLayoutMode.CANVAS,
};
case PageLayoutTabLayoutMode.GRID:
default:
return {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: widget.gridPosition.row,
column: widget.gridPosition.column,
rowSpan: widget.gridPosition.rowSpan,
columnSpan: widget.gridPosition.columnSpan,
};
}
};
export const convertPageLayoutDraftToUpdateInput = (
pageLayoutDraft: DraftPageLayout,
): UpdatePageLayoutWithTabsInput => {
@@ -18,7 +51,7 @@ export const convertPageLayoutDraftToUpdateInput = (
position: tab.position,
widgets: tab.widgets
.filter((widget) => !isDynamicRelationWidget(widget))
.map((widget) => ({
.map((widget, widgetIndex) => ({
id: widget.id,
pageLayoutTabId: widget.pageLayoutTabId,
title: widget.title,
@@ -30,14 +63,7 @@ export const convertPageLayoutDraftToUpdateInput = (
rowSpan: widget.gridPosition.rowSpan,
columnSpan: widget.gridPosition.columnSpan,
},
position: {
layoutMode:
widget.position?.layoutMode ?? PageLayoutTabLayoutMode.GRID,
row: widget.gridPosition.row,
column: widget.gridPosition.column,
rowSpan: widget.gridPosition.rowSpan,
columnSpan: widget.gridPosition.columnSpan,
},
position: buildWidgetPosition(widget, widgetIndex),
configuration: widget.configuration ?? null,
})),
})),
@@ -4,7 +4,7 @@ import { isDynamicRelationWidget } from '@/page-layout/utils/isDynamicRelationWi
import { WidgetType } from '~/generated-metadata/graphql';
export const reInjectDynamicRelationWidgetsFromDraft = (
serverLayout: PageLayout,
persistedLayout: PageLayout,
previousDraft: DraftPageLayout,
): PageLayout => {
const dynamicWidgetsByTabId = new Map(
@@ -19,12 +19,12 @@ export const reInjectDynamicRelationWidgetsFromDraft = (
);
if (!hasDynamicWidgets) {
return serverLayout;
return persistedLayout;
}
return {
...serverLayout,
tabs: serverLayout.tabs.map((tab) => {
...persistedLayout,
tabs: persistedLayout.tabs.map((tab) => {
const relationWidgets = dynamicWidgetsByTabId.get(tab.id);
if (!relationWidgets || relationWidgets.length === 0) {
@@ -7,14 +7,22 @@ import {
} from '@hello-pangea/dnd';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { FieldsConfigurationGroupEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor';
import { FieldsConfigurationUngroupedEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor';
import { useCreateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup';
import { useDeleteFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useDeleteFieldsWidgetEditorGroup';
import { useFieldsWidgetGroupsDraft } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsDraft';
import { useFieldsWidgetEditorMode } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetEditorMode';
import { useMoveFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveFieldInDraft';
import { useMoveUngroupedFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveUngroupedFieldInDraft';
import { useReorderFieldsWidgetEditorGroups } from '@/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups';
import { useToggleFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft';
import { useToggleUngroupedFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleUngroupedFieldVisibilityInDraft';
import { useUpdateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useUpdateFieldsWidgetEditorGroup';
import { getFieldsConfigurationGroupRenameDropdownId } from '@/page-layout/widgets/fields/utils/getFieldsConfigurationGroupRenameDropdownId';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
@@ -36,11 +44,25 @@ export const FieldsConfigurationEditor = ({
const { t } = useLingui();
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const { draftGroups } = useFieldsWidgetGroupsDraft({
const { editorMode } = useFieldsWidgetEditorMode({
pageLayoutId,
widgetId,
});
const fieldsWidgetGroupsDraft = useAtomComponentStateValue(
fieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const draftGroups = fieldsWidgetGroupsDraft[widgetId] ?? [];
const fieldsWidgetUngroupedFieldsDraft = useAtomComponentStateValue(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const ungroupedFields = fieldsWidgetUngroupedFieldsDraft[widgetId] ?? [];
const { createGroup } = useCreateFieldsWidgetEditorGroup({
pageLayoutId,
widgetId,
@@ -61,6 +83,17 @@ export const FieldsConfigurationEditor = ({
widgetId,
});
const { moveField: moveUngroupedField } = useMoveUngroupedFieldInDraft({
pageLayoutId,
widgetId,
});
const { toggleFieldVisibility: toggleUngroupedFieldVisibility } =
useToggleUngroupedFieldVisibilityInDraft({
pageLayoutId,
widgetId,
});
const { updateGroup } = useUpdateFieldsWidgetEditorGroup({
pageLayoutId,
widgetId,
@@ -71,6 +104,8 @@ export const FieldsConfigurationEditor = ({
widgetId,
});
const { openDropdown } = useOpenDropdown();
const [renamingGroupValue, setRenamingGroupValue] = useState('');
const handleStartRename = ({ groupName }: { groupName: string }) => {
@@ -149,11 +184,28 @@ export const FieldsConfigurationEditor = ({
);
};
const handleAddGroup = () => {
const handleAddGroup = ({ afterGroupId }: { afterGroupId?: string }) => {
const newGroupName = t`New Group`;
createGroup(newGroupName);
const newGroupId = createGroup({ name: newGroupName, afterGroupId });
setRenamingGroupValue(newGroupName);
openDropdown({
dropdownComponentInstanceIdFromProps:
getFieldsConfigurationGroupRenameDropdownId(newGroupId),
});
};
if (editorMode === 'ungrouped') {
return (
<FieldsConfigurationUngroupedEditor
ungroupedFields={ungroupedFields}
onMoveField={moveUngroupedField}
onToggleFieldVisibility={toggleUngroupedFieldVisibility}
onAddGroup={() => handleAddGroup({})}
/>
);
}
const sortedGroups = [...draftGroups].sort((a, b) => a.position - b.position);
if (sortedGroups.length === 0) {
@@ -182,7 +234,9 @@ export const FieldsConfigurationEditor = ({
objectMetadataItem={objectMetadataItem}
draggableProvided={draggableProvided}
isDragging={snapshot.isDragging}
onAddGroup={handleAddGroup}
onAddGroup={() =>
handleAddGroup({ afterGroupId: group.id })
}
onToggleFieldVisibility={(fieldMetadataId) =>
toggleFieldVisibility(group.id, fieldMetadataId)
}
@@ -3,6 +3,7 @@ import { IconDotsVertical, IconPencil, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { getFieldsConfigurationGroupEditDropdownId } from '@/page-layout/widgets/fields/utils/getFieldsConfigurationGroupEditDropdownId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
@@ -22,7 +23,7 @@ export const FieldsConfigurationGroupDropdown = ({
}: FieldsConfigurationGroupDropdownProps) => {
const { t } = useLingui();
const dropdownId = `fields-configuration-group-edit-${groupId}`;
const dropdownId = getFieldsConfigurationGroupEditDropdownId(groupId);
const { closeDropdown } = useCloseDropdown();
@@ -1,5 +1,5 @@
import { styled } from '@linaria/react';
import { Droppable, type DraggableProvided } from '@hello-pangea/dnd';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
@@ -9,6 +9,7 @@ import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/com
import { FieldsConfigurationGroupDropdown } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown';
import { FieldsConfigurationGroupRenameInput } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupRenameInput';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { getFieldsConfigurationGroupRenameDropdownId } from '@/page-layout/widgets/fields/utils/getFieldsConfigurationGroupRenameDropdownId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
@@ -23,6 +24,18 @@ const StyledFieldsDroppable = styled.div`
flex-direction: column;
`;
const StyledEmptyGroupDropZone = styled.div`
align-items: center;
border: 1px dashed ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
color: ${themeCssVariables.font.color.light};
display: flex;
font-size: ${themeCssVariables.font.size.sm};
justify-content: center;
margin: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
min-height: ${themeCssVariables.spacing[10]};
`;
const StyledGroupContainer = styled.div<{ isDragging: boolean }>`
background: ${({ isDragging }) =>
isDragging ? themeCssVariables.background.primary : 'transparent'};
@@ -84,7 +97,9 @@ export const FieldsConfigurationGroupEditor = ({
}: FieldsConfigurationGroupEditorProps) => {
const { t } = useLingui();
const renameDropdownId = `fields-configuration-group-rename-${group.id}`;
const renameDropdownId = getFieldsConfigurationGroupRenameDropdownId(
group.id,
);
const { openDropdown } = useOpenDropdown();
const { closeDropdown } = useCloseDropdown();
@@ -171,6 +186,11 @@ export const FieldsConfigurationGroupEditor = ({
// eslint-disable-next-line react/jsx-props-no-spreading
{...droppableProvided.droppableProps}
>
{sortedFields.length === 0 && (
<StyledEmptyGroupDropZone>
{t`Drop fields here`}
</StyledEmptyGroupDropZone>
)}
{sortedFields.map((field, fieldIndex) => {
return (
<DraggableItem
@@ -202,7 +222,7 @@ export const FieldsConfigurationGroupEditor = ({
<MenuItem
LeftIcon={IconNewSection}
withIconContainer
text={t`Add a Group`}
text={t`Add a Section`}
onClick={onAddGroup}
/>
</StyledGroupContainer>
@@ -0,0 +1,94 @@
import { DragDropContext, Droppable, type DropResult } from '@hello-pangea/dnd';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor';
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { IconNewSection } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
const StyledFieldsDroppable = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
type FieldsConfigurationUngroupedEditorProps = {
ungroupedFields: FieldsWidgetGroupField[];
onMoveField: (sourceIndex: number, destinationIndex: number) => void;
onToggleFieldVisibility: (fieldMetadataId: string) => void;
onAddGroup: () => void;
};
export const FieldsConfigurationUngroupedEditor = ({
ungroupedFields,
onMoveField,
onToggleFieldVisibility,
onAddGroup,
}: FieldsConfigurationUngroupedEditorProps) => {
const { t } = useLingui();
const sortedFields = [...ungroupedFields].sort(
(a, b) => a.position - b.position,
);
const handleDragEnd = (result: DropResult) => {
const { source, destination } = result;
if (!destination) {
return;
}
if (source.index === destination.index) {
return;
}
onMoveField(source.index, destination.index);
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="ungrouped-fields" type="FIELD">
{(provided) => (
<StyledFieldsDroppable
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{sortedFields.map((field, fieldIndex) => (
<DraggableItem
key={field.fieldMetadataItem.id}
draggableId={`field-${field.fieldMetadataItem.id}`}
index={fieldIndex}
isInsideScrollableContainer
itemComponent={
<FieldsConfigurationFieldEditor
field={{
fieldMetadataId: field.fieldMetadataItem.id,
position: field.position,
isVisible: field.isVisible,
}}
fieldMetadata={field.fieldMetadataItem}
onToggleVisibility={() => {
onToggleFieldVisibility(field.fieldMetadataItem.id);
}}
/>
}
/>
))}
{provided.placeholder}
<MenuItem
LeftIcon={IconNewSection}
withIconContainer
text={t`Add a Section`}
onClick={onAddGroup}
/>
</StyledFieldsDroppable>
)}
</Droppable>
</DragDropContext>
);
};
@@ -1,33 +1,15 @@
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { FieldsWidgetCellEditModePortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal';
import { FieldsWidgetCellHoveredPortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal';
import { FieldsWidgetFieldList } from '@/page-layout/widgets/fields/components/FieldsWidgetFieldList';
import { FieldsWidgetGroupContainer } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupContainer';
import { useFieldsWidgetGroupsForDisplay } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroupsForDisplay';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import {
@@ -38,8 +20,8 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
} from 'twenty-ui/layout';
import { type FieldsConfiguration } from '~/generated-metadata/graphql';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type FieldsConfiguration } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -58,6 +40,11 @@ const StyledPropertyBox = styled.div`
padding-bottom: ${themeCssVariables.spacing[3]};
`;
const StyledInlineFieldsPropertyBox = styled(StyledPropertyBox)`
padding-bottom: 0;
padding-top: 0;
`;
type FieldsWidgetProps = {
widget: PageLayoutWidget;
};
@@ -68,34 +55,13 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
const instanceId = `fields-${widget.id}-${targetRecord.id}${isInRightDrawer ? '-right-drawer' : ''}`;
const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
objectRecordId: targetRecord.id,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetRecord.targetObjectNameSingular,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { objectMetadataItems } = useObjectMetadataItems();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular: targetRecord.targetObjectNameSingular,
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId: targetRecord.id,
objectMetadataId: objectMetadataItem.id,
});
const setRecordFieldListHoverPosition = useSetAtomComponentState(
recordFieldListHoverPositionComponentState,
instanceId,
);
const fieldsConfiguration = widget.configuration as FieldsConfiguration;
const { groups } = useFieldsWidgetGroupsForDisplay({
const { groups, displayMode } = useFieldsWidgetGroupsForDisplay({
widgetId: widget.id,
viewId: fieldsConfiguration.viewId ?? null,
objectNameSingular: targetRecord.targetObjectNameSingular,
@@ -138,103 +104,25 @@ export const FieldsWidget = ({ widget }: FieldsWidgetProps) => {
instanceId,
}}
>
{groups.map((group) => (
<FieldsWidgetGroupContainer key={group.id} title={group.name}>
<StyledPropertyBox>
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
<>
{group.fields.map(({ fieldMetadataItem, globalIndex }) => {
const isActivityTarget = isActivityTargetField(
fieldMetadataItem.name,
targetRecord.targetObjectNameSingular,
);
return (
<FieldContext.Provider
key={targetRecord.id + fieldMetadataItem.id}
value={{
recordId: targetRecord.id,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition:
formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: globalIndex,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord: useUpdateOneObjectRecordMutation,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions:
getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly:
fieldMetadataItem.isUIReadOnly ?? false,
},
}),
onMouseEnter: () =>
setRecordFieldListHoverPosition(globalIndex),
anchorId: `${getRecordFieldInputInstanceId({
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
})}`,
isForbidden: isJunctionRelationForbidden({
fieldMetadataItem,
sourceObjectMetadataId: objectMetadataItem.id,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
}),
}}
>
{isActivityTarget ? (
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputInstanceId(
{
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
},
)}
activityObjectNameSingular={
targetRecord.targetObjectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={targetRecord.id}
showLabel={true}
maxWidth={200}
/>
) : (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId: targetRecord.id,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<RecordInlineCell loading={recordLoading} />
</RecordFieldComponentInstanceContext.Provider>
)}
</FieldContext.Provider>
);
})}
</>
)}
</StyledPropertyBox>
</FieldsWidgetGroupContainer>
))}
{displayMode === 'inline' ? (
<StyledInlineFieldsPropertyBox>
<FieldsWidgetFieldList
fields={groups.flatMap((group) => group.fields)}
instanceId={instanceId}
/>
</StyledInlineFieldsPropertyBox>
) : (
groups.map((group) => (
<FieldsWidgetGroupContainer key={group.id} title={group.name}>
<StyledPropertyBox>
<FieldsWidgetFieldList
fields={group.fields}
instanceId={instanceId}
/>
</StyledPropertyBox>
</FieldsWidgetGroupContainer>
))
)}
<FieldsWidgetCellHoveredPortal
objectMetadataItem={objectMetadataItem}
@@ -0,0 +1,128 @@
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { isActivityTargetField } from '@/object-record/record-field-list/utils/categorizeRelationFields';
import {
FieldContext,
type RecordUpdateHook,
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { isJunctionRelationForbidden } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationForbidden';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { type ObjectPermissions } from 'twenty-shared/types';
type FieldsWidgetFieldItemProps = {
fieldMetadataItem: FieldMetadataItem;
globalIndex: number;
recordId: string;
targetObjectNameSingular: string;
objectMetadataItem: ObjectMetadataItem;
objectMetadataItems: ObjectMetadataItem[];
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
isRecordReadOnly: boolean;
useUpdateRecord: RecordUpdateHook;
recordLoading: boolean;
instanceId: string;
onMouseEnter: () => void;
};
export const FieldsWidgetFieldItem = ({
fieldMetadataItem,
globalIndex,
recordId,
targetObjectNameSingular,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
isRecordReadOnly,
useUpdateRecord,
recordLoading,
instanceId,
onMouseEnter,
}: FieldsWidgetFieldItemProps) => {
const isActivityTarget = isActivityTargetField(
fieldMetadataItem.name,
targetObjectNameSingular,
);
return (
<FieldContext.Provider
key={recordId + fieldMetadataItem.id}
value={{
recordId,
maxWidth: 200,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsColumnDefinition({
field: fieldMetadataItem,
position: globalIndex,
objectMetadataItem,
showLabel: true,
labelWidth: 90,
}),
useUpdateRecord,
isDisplayModeFixHeight: true,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly,
objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: objectMetadataItem.id,
}),
fieldMetadataItem: {
id: fieldMetadataItem.id,
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
},
}),
onMouseEnter,
anchorId: `${getRecordFieldInputInstanceId({
recordId,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
})}`,
isForbidden: isJunctionRelationForbidden({
fieldMetadataItem,
sourceObjectMetadataId: objectMetadataItem.id,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
}),
}}
>
{isActivityTarget ? (
<ActivityTargetsInlineCell
componentInstanceId={getRecordFieldInputInstanceId({
recordId,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
})}
activityObjectNameSingular={
targetObjectNameSingular as
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task
}
activityRecordId={recordId}
showLabel={true}
maxWidth={200}
/>
) : (
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldMetadataItem.name,
prefix: instanceId,
}),
}}
>
<RecordInlineCell loading={recordLoading} />
</RecordFieldComponentInstanceContext.Provider>
)}
</FieldContext.Provider>
);
};
@@ -0,0 +1,71 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState';
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { FieldsWidgetFieldItem } from '@/page-layout/widgets/fields/components/FieldsWidgetFieldItem';
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
type FieldsWidgetFieldListProps = {
fields: FieldsWidgetGroupField[];
instanceId: string;
};
export const FieldsWidgetFieldList = ({
fields,
instanceId,
}: FieldsWidgetFieldListProps) => {
const targetRecord = useTargetRecord();
const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({
objectRecordId: targetRecord.id,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetRecord.targetObjectNameSingular,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { objectMetadataItems } = useObjectMetadataItems();
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
objectNameSingular: targetRecord.targetObjectNameSingular,
});
const isRecordReadOnly = useIsRecordReadOnly({
recordId: targetRecord.id,
objectMetadataId: objectMetadataItem.id,
});
const setRecordFieldListHoverPosition = useSetAtomComponentState(
recordFieldListHoverPositionComponentState,
instanceId,
);
if (isPrefetchLoading) {
return <PropertyBoxSkeletonLoader />;
}
return fields.map(({ fieldMetadataItem, globalIndex }) => (
<FieldsWidgetFieldItem
key={targetRecord.id + fieldMetadataItem.id}
fieldMetadataItem={fieldMetadataItem}
globalIndex={globalIndex}
recordId={targetRecord.id}
targetObjectNameSingular={targetRecord.targetObjectNameSingular}
objectMetadataItem={objectMetadataItem}
objectMetadataItems={objectMetadataItems}
objectPermissionsByObjectMetadataId={objectPermissionsByObjectMetadataId}
isRecordReadOnly={isRecordReadOnly}
useUpdateRecord={useUpdateOneObjectRecordMutation}
recordLoading={recordLoading}
instanceId={instanceId}
onMouseEnter={() => setRecordFieldListHoverPosition(globalIndex)}
/>
));
};
@@ -15,7 +15,11 @@ export const FieldsWidgetGroupsDraftInitializationEffect = ({
}: FieldsWidgetGroupsDraftInitializationEffectProps) => {
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const { groups: serverGroups } = useFieldsWidgetEditorGroupsData({
const {
groups: persistedGroups,
ungroupedFields,
editorMode,
} = useFieldsWidgetEditorGroupsData({
viewId,
objectNameSingular: objectMetadataItem.nameSingular,
});
@@ -23,7 +27,9 @@ export const FieldsWidgetGroupsDraftInitializationEffect = ({
useInitializeFieldsWidgetGroupsDraft({
pageLayoutId,
widgetId,
serverGroups,
persistedGroups,
persistedUngroupedFields: ungroupedFields,
persistedEditorMode: editorMode,
});
return null;
@@ -0,0 +1,586 @@
import {
type ApolloClient,
type NormalizedCacheObject,
useApolloClient,
} from '@apollo/client';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { MemoryRouter } from 'react-router-dom';
import { expect, waitFor, within } from 'storybook/test';
import { isAppMetadataReadyState } from '@/metadata-store/states/isAppMetadataReadyState';
import { ApolloCoreClientContext } from '@/object-metadata/contexts/ApolloCoreClientContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { PageLayoutContentProvider } from '@/page-layout/contexts/PageLayoutContentContext';
import {
PAGE_LAYOUT_TEST_INSTANCE_ID,
PageLayoutTestWrapper,
} from '@/page-layout/hooks/__tests__/PageLayoutTestWrapper';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { type PageLayout } from '@/page-layout/types/PageLayout';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { FieldsWidget } from '@/page-layout/widgets/fields/components/FieldsWidget';
import { WidgetComponentInstanceContext } from '@/page-layout/widgets/states/contexts/WidgetComponentInstanceContext';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { ComponentDecorator } from 'twenty-ui/testing';
import {
ViewOpenRecordIn as CoreViewOpenRecordIn,
ViewType as CoreViewType,
ViewVisibility as CoreViewVisibility,
PageLayoutTabLayoutMode,
PageLayoutType,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
import { FileUploadDecorator } from '~/testing/decorators/FileUploadDecorator';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
import { getMockFieldMetadataItemOrThrow } from '~/testing/utils/getMockFieldMetadataItemOrThrow';
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
const companyObjectMetadataItem = getMockObjectMetadataItemOrThrow(
CoreObjectNameSingular.Company,
);
const nameField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'name',
});
const addressField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'address',
});
const employeesField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'employees',
});
const annualRecurringRevenueField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'annualRecurringRevenue',
});
const linkedinField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'linkedinLink',
});
const idealCustomerProfileField = getMockFieldMetadataItemOrThrow({
objectMetadataItem: companyObjectMetadataItem,
fieldName: 'idealCustomerProfile',
});
const TEST_RECORD_ID = 'test-fields-widget-record-123';
const FIELDS_VIEW_ID = 'test-fields-view-001';
const TAB_ID_OVERVIEW = 'tab-overview';
const mockCompanyRecord: ObjectRecord = {
__typename: 'Company',
id: TEST_RECORD_ID,
name: 'Acme Corporation',
address: {
addressStreet1: '123 Business St',
addressStreet2: null,
addressCity: 'San Francisco',
addressState: 'CA',
addressPostcode: '94102',
addressCountry: 'United States',
addressLat: null,
addressLng: null,
},
employees: 250,
linkedinLink: {
primaryLinkUrl: 'https://linkedin.com/company/acme',
primaryLinkLabel: null,
secondaryLinks: null,
},
idealCustomerProfile: true,
annualRecurringRevenue: {
__typename: 'Currency',
amountMicros: 5000000000000,
currencyCode: 'USD',
},
};
const setRecordInStore = (recordId: string, record: ObjectRecord) => {
jotaiStore.set(recordStoreFamilyState.atomFamily(recordId), record);
};
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: [],
});
const CoreClientProviderWrapper = ({
children,
}: {
children: React.ReactNode;
}) => {
const apolloClient = useApolloClient() as ApolloClient<NormalizedCacheObject>;
return (
<ApolloCoreClientContext.Provider value={apolloClient}>
{children}
</ApolloCoreClientContext.Provider>
);
};
const createPageLayoutWithWidget = (
widget: PageLayoutWidget,
objectMetadataId: string,
): PageLayout => ({
id: PAGE_LAYOUT_TEST_INSTANCE_ID,
name: 'Mock Page Layout',
type: PageLayoutType.RECORD_PAGE,
objectMetadataId,
tabs: [
{
__typename: 'PageLayoutTab' as const,
applicationId: '',
id: TAB_ID_OVERVIEW,
title: 'Overview',
position: 0,
pageLayoutId: PAGE_LAYOUT_TEST_INSTANCE_ID,
widgets: [widget],
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
},
],
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
});
const createFieldsWidget = (viewId: string | null): PageLayoutWidget => ({
__typename: 'PageLayoutWidget',
id: 'widget-fields',
pageLayoutTabId: TAB_ID_OVERVIEW,
type: WidgetType.FIELDS,
title: 'Fields',
objectMetadataId: companyObjectMetadataItem.id,
gridPosition: {
__typename: 'GridPosition',
row: 0,
column: 0,
rowSpan: 4,
columnSpan: 4,
},
configuration: {
__typename: 'FieldsConfiguration',
configurationType: WidgetConfigurationType.FIELDS,
viewId,
},
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
deletedAt: null,
});
const createCoreView = (
overrides: Partial<CoreViewWithRelations> = {},
): CoreViewWithRelations => ({
id: FIELDS_VIEW_ID,
name: 'Company Fields',
objectMetadataId: companyObjectMetadataItem.id,
type: CoreViewType.FIELDS_WIDGET,
icon: 'IconList',
key: null,
shouldHideEmptyGroups: false,
position: 0,
isCompact: false,
openRecordIn: CoreViewOpenRecordIn.SIDE_PANEL,
viewFields: [],
viewGroups: [],
viewFilters: [],
viewSorts: [],
visibility: CoreViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
__typename: 'CoreView',
...overrides,
});
const meta: Meta<typeof FieldsWidget> = {
title: 'Modules/PageLayout/Widgets/FieldsWidget',
component: FieldsWidget,
decorators: [
ComponentDecorator,
ChipGeneratorsDecorator,
FileUploadDecorator,
(Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof FieldsWidget>;
export const WithViewFieldGroups: Story = {
render: () => {
const coreView = createCoreView({
viewFieldGroups: [
{
id: 'group-contact-info',
name: 'Contact Info',
position: 0,
isVisible: true,
viewId: FIELDS_VIEW_ID,
viewFields: [
{
id: 'vf-name',
fieldMetadataId: nameField.id,
position: 0,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-address',
fieldMetadataId: addressField.id,
position: 1,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-linkedin',
fieldMetadataId: linkedinField.id,
position: 2,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
],
},
{
id: 'group-business',
name: 'Business',
position: 1,
isVisible: true,
viewId: FIELDS_VIEW_ID,
viewFields: [
{
id: 'vf-employees',
fieldMetadataId: employeesField.id,
position: 0,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-arr',
fieldMetadataId: annualRecurringRevenueField.id,
position: 1,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-icp',
fieldMetadataId: idealCustomerProfileField.id,
position: 2,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
],
},
],
});
const widget = createFieldsWidget(FIELDS_VIEW_ID);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
);
jotaiStore.set(
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(coreViewsState.atom, [coreView]);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
jotaiStore.set(
pageLayoutDraftComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
setRecordInStore(TEST_RECORD_ID, mockCompanyRecord);
return (
<div style={{ width: '400px', padding: '20px' }}>
<JestMetadataAndApolloMocksWrapper>
<CoreClientProviderWrapper>
<PageLayoutTestWrapper store={jotaiStore}>
<LayoutRenderingProvider
value={{
isInRightDrawer: false,
layoutType: PageLayoutType.RECORD_PAGE,
targetRecordIdentifier: {
id: TEST_RECORD_ID,
targetObjectNameSingular:
companyObjectMetadataItem.nameSingular,
},
}}
>
<PageLayoutContentProvider
value={{
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
tabId: TAB_ID_OVERVIEW,
}}
>
<WidgetComponentInstanceContext.Provider
value={{ instanceId: widget.id }}
>
<FieldsWidget widget={widget} />
</WidgetComponentInstanceContext.Provider>
</PageLayoutContentProvider>
</LayoutRenderingProvider>
</PageLayoutTestWrapper>
</CoreClientProviderWrapper>
</JestMetadataAndApolloMocksWrapper>
</div>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const contactInfoHeader = await canvas.findByText('Contact Info');
expect(contactInfoHeader).toBeVisible();
const businessHeader = await canvas.findByText('Business');
expect(businessHeader).toBeVisible();
const companyName = await canvas.findByText('Acme Corporation');
expect(companyName).toBeVisible();
},
};
export const WithInlineViewFields: Story = {
render: () => {
const coreView = createCoreView({
viewFields: [
{
id: 'vf-name',
fieldMetadataId: nameField.id,
position: 0,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-employees',
fieldMetadataId: employeesField.id,
position: 1,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
{
id: 'vf-address',
fieldMetadataId: addressField.id,
position: 2,
isVisible: true,
size: 200,
aggregateOperation: null,
viewId: FIELDS_VIEW_ID,
},
],
});
const widget = createFieldsWidget(FIELDS_VIEW_ID);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
);
jotaiStore.set(
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(coreViewsState.atom, [coreView]);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
jotaiStore.set(
pageLayoutDraftComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
setRecordInStore(TEST_RECORD_ID, mockCompanyRecord);
return (
<div style={{ width: '400px', padding: '20px' }}>
<JestMetadataAndApolloMocksWrapper>
<CoreClientProviderWrapper>
<PageLayoutTestWrapper store={jotaiStore}>
<LayoutRenderingProvider
value={{
isInRightDrawer: false,
layoutType: PageLayoutType.RECORD_PAGE,
targetRecordIdentifier: {
id: TEST_RECORD_ID,
targetObjectNameSingular:
companyObjectMetadataItem.nameSingular,
},
}}
>
<PageLayoutContentProvider
value={{
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
tabId: TAB_ID_OVERVIEW,
}}
>
<WidgetComponentInstanceContext.Provider
value={{ instanceId: widget.id }}
>
<FieldsWidget widget={widget} />
</WidgetComponentInstanceContext.Provider>
</PageLayoutContentProvider>
</LayoutRenderingProvider>
</PageLayoutTestWrapper>
</CoreClientProviderWrapper>
</JestMetadataAndApolloMocksWrapper>
</div>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const companyName = await canvas.findByText('Acme Corporation');
expect(companyName).toBeVisible();
const contactInfoHeader = canvas.queryByText('Contact Info');
expect(contactInfoHeader).toBeNull();
const generalHeader = canvas.queryByText('General');
expect(generalHeader).toBeNull();
},
};
export const Empty: Story = {
render: () => {
const coreView = createCoreView({
viewFieldGroups: [
{
id: 'group-empty',
name: 'Empty Group',
position: 0,
isVisible: false,
viewId: FIELDS_VIEW_ID,
viewFields: [],
},
],
});
const widget = createFieldsWidget(FIELDS_VIEW_ID);
const pageLayoutData = createPageLayoutWithWidget(
widget,
companyObjectMetadataItem.id,
);
jotaiStore.set(
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
jotaiStore.set(isAppMetadataReadyState.atom, true);
jotaiStore.set(coreViewsState.atom, [coreView]);
jotaiStore.set(
pageLayoutPersistedComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
jotaiStore.set(
pageLayoutDraftComponentState.atomFamily({
instanceId: PAGE_LAYOUT_TEST_INSTANCE_ID,
}),
pageLayoutData,
);
setRecordInStore(TEST_RECORD_ID, mockCompanyRecord);
return (
<div style={{ width: '400px', padding: '20px' }}>
<JestMetadataAndApolloMocksWrapper>
<CoreClientProviderWrapper>
<PageLayoutTestWrapper store={jotaiStore}>
<LayoutRenderingProvider
value={{
isInRightDrawer: false,
layoutType: PageLayoutType.RECORD_PAGE,
targetRecordIdentifier: {
id: TEST_RECORD_ID,
targetObjectNameSingular:
companyObjectMetadataItem.nameSingular,
},
}}
>
<PageLayoutContentProvider
value={{
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
tabId: TAB_ID_OVERVIEW,
}}
>
<WidgetComponentInstanceContext.Provider
value={{ instanceId: widget.id }}
>
<FieldsWidget widget={widget} />
</WidgetComponentInstanceContext.Provider>
</PageLayoutContentProvider>
</LayoutRenderingProvider>
</PageLayoutTestWrapper>
</CoreClientProviderWrapper>
</JestMetadataAndApolloMocksWrapper>
</div>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(() => {
expect(canvas.getByText('No fields to display')).toBeVisible();
});
await waitFor(() => {
expect(
canvas.getByText('Configure this widget to display fields'),
).toBeVisible();
});
},
};
@@ -1,4 +1,6 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@@ -18,33 +20,107 @@ export const useCreateFieldsWidgetEditorGroup = ({
pageLayoutId,
);
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const fieldsWidgetEditorModeDraftState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModeDraftComponentState,
pageLayoutId,
);
const store = useStore();
const createGroup = useCallback(
(name: string) => {
const allDraftGroups = store.get(fieldsWidgetGroupsDraftState);
({ name, afterGroupId }: { name: string; afterGroupId?: string }) => {
const allEditorModes = store.get(fieldsWidgetEditorModeDraftState);
const currentEditorMode = allEditorModes[widgetId] ?? 'ungrouped';
const currentGroups = allDraftGroups[widgetId] ?? [];
const maxPosition = Math.max(...currentGroups.map((g) => g.position), -1);
const newId = v4();
store.set(fieldsWidgetGroupsDraftState, (prev) => ({
...prev,
[widgetId]: [
...(prev[widgetId] ?? []),
{
id: newId,
name,
position: maxPosition + 1,
isVisible: true,
fields: [],
},
],
}));
if (currentEditorMode === 'ungrouped') {
const allUngroupedFields = store.get(
fieldsWidgetUngroupedFieldsDraftState,
);
const ungroupedFields = allUngroupedFields[widgetId] ?? [];
store.set(fieldsWidgetGroupsDraftState, (prev) => ({
...prev,
[widgetId]: [
{
id: newId,
name,
position: 0,
isVisible: true,
fields: ungroupedFields.map((field, index) => ({
...field,
position: index,
globalIndex: index,
})),
},
],
}));
store.set(fieldsWidgetUngroupedFieldsDraftState, (prev) => ({
...prev,
[widgetId]: [],
}));
store.set(fieldsWidgetEditorModeDraftState, (prev) => ({
...prev,
[widgetId]: 'grouped' as const,
}));
} else {
const allDraftGroups = store.get(fieldsWidgetGroupsDraftState);
const currentGroups = allDraftGroups[widgetId] ?? [];
const afterGroup = afterGroupId
? currentGroups.find((g) => g.id === afterGroupId)
: undefined;
const newPosition =
afterGroup !== undefined
? afterGroup.position + 1
: Math.max(...currentGroups.map((g) => g.position), -1) + 1;
const shiftedGroups =
afterGroup !== undefined
? currentGroups.map((g) =>
g.position >= newPosition
? { ...g, position: g.position + 1 }
: g,
)
: currentGroups;
store.set(fieldsWidgetGroupsDraftState, (prev) => ({
...prev,
[widgetId]: [
...shiftedGroups,
{
id: newId,
name,
position: newPosition,
isVisible: true,
fields: [],
},
],
}));
}
return newId;
},
[fieldsWidgetGroupsDraftState, widgetId, store],
[
fieldsWidgetGroupsDraftState,
fieldsWidgetUngroupedFieldsDraftState,
fieldsWidgetEditorModeDraftState,
widgetId,
store,
],
);
return { createGroup };
@@ -1,4 +1,6 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
@@ -17,18 +19,107 @@ export const useDeleteFieldsWidgetEditorGroup = ({
pageLayoutId,
);
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const fieldsWidgetEditorModeDraftState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModeDraftComponentState,
pageLayoutId,
);
const store = useStore();
const deleteGroup = useCallback(
(groupId: string) => {
const allDraftGroups = store.get(fieldsWidgetGroupsDraftState);
const currentGroups = allDraftGroups[widgetId] ?? [];
const deletedGroup = currentGroups.find((group) => group.id === groupId);
if (!deletedGroup) {
return;
}
const deletedGroupFields = deletedGroup.fields;
const remainingGroups = currentGroups.filter(
(group) => group.id !== groupId,
);
if (remainingGroups.length === 0) {
store.set(fieldsWidgetGroupsDraftState, (prev) => ({
...prev,
[widgetId]: [],
}));
store.set(fieldsWidgetUngroupedFieldsDraftState, (prev) => ({
...prev,
[widgetId]: deletedGroupFields.map((field, index) => ({
...field,
position: index,
globalIndex: index,
})),
}));
store.set(fieldsWidgetEditorModeDraftState, (prev) => ({
...prev,
[widgetId]: 'ungrouped' as const,
}));
return;
}
const sortedRemaining = [...remainingGroups].sort(
(a, b) => a.position - b.position,
);
const deletedGroupPosition = deletedGroup.position;
const nextGroup = sortedRemaining.find(
(group) => group.position > deletedGroupPosition,
);
const targetGroup =
nextGroup ?? sortedRemaining[sortedRemaining.length - 1];
store.set(fieldsWidgetGroupsDraftState, (prev) => ({
...prev,
[widgetId]: (prev[widgetId] ?? []).filter(
(group) => group.id !== groupId,
),
[widgetId]: remainingGroups.map((group) => {
if (group.id !== targetGroup.id) {
return group;
}
const existingFields = [...group.fields].sort(
(a, b) => a.position - b.position,
);
const maxPosition =
existingFields.length > 0
? Math.max(...existingFields.map((f) => f.position))
: -1;
const appendedFields = deletedGroupFields.map((field, index) => ({
...field,
position: maxPosition + 1 + index,
}));
return {
...group,
fields: [...existingFields, ...appendedFields],
};
}),
}));
},
[fieldsWidgetGroupsDraftState, widgetId, store],
[
fieldsWidgetGroupsDraftState,
fieldsWidgetUngroupedFieldsDraftState,
fieldsWidgetEditorModeDraftState,
widgetId,
store,
],
);
return { deleteGroup };
@@ -1,31 +1,45 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { type FieldsWidgetEditorMode } from '@/page-layout/widgets/fields/types/FieldsWidgetEditorMode';
import {
type FieldsWidgetGroup,
type FieldsWidgetGroupField,
} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { useGetViewById } from '@/views/hooks/useGetViewById';
import { useLingui } from '@lingui/react/macro';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
type UseFieldsWidgetEditorGroupsDataParams = {
viewId: string | null;
objectNameSingular: string;
};
type UseFieldsWidgetEditorGroupsDataResult = {
groups: FieldsWidgetGroup[];
ungroupedFields: FieldsWidgetGroupField[];
editorMode: FieldsWidgetEditorMode;
isFromView: boolean;
};
export const useFieldsWidgetEditorGroupsData = ({
viewId,
objectNameSingular,
}: UseFieldsWidgetEditorGroupsDataParams) => {
const { t } = useLingui();
}: UseFieldsWidgetEditorGroupsDataParams): UseFieldsWidgetEditorGroupsDataResult => {
const { view } = useGetViewById(viewId);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const groups = useMemo<FieldsWidgetGroup[]>(() => {
const result = useMemo<
Pick<
UseFieldsWidgetEditorGroupsDataResult,
'groups' | 'ungroupedFields' | 'editorMode'
>
>(() => {
if (!isDefined(objectMetadataItem)) {
return [];
return { groups: [], ungroupedFields: [], editorMode: 'ungrouped' };
}
if (isDefined(view) && isDefined(view.viewFieldGroups)) {
if (isDefined(view) && isNonEmptyArray(view.viewFieldGroups)) {
const viewFieldGroups = view.viewFieldGroups;
const sortedGroups = [...viewFieldGroups].sort(
@@ -34,7 +48,7 @@ export const useFieldsWidgetEditorGroupsData = ({
let globalIndex = 0;
return sortedGroups.map((group) => {
const groups = sortedGroups.map((group) => {
const groupFields = [...(group.viewFields ?? [])].sort(
(a, b) => a.position - b.position,
);
@@ -67,6 +81,8 @@ export const useFieldsWidgetEditorGroupsData = ({
fields,
};
});
return { groups, ungroupedFields: [], editorMode: 'grouped' };
}
if (isDefined(view) && view.viewFields.length > 0) {
@@ -94,23 +110,15 @@ export const useFieldsWidgetEditorGroupsData = ({
.filter(isDefined);
if (fields.length > 0) {
return [
{
id: `${view.id}-group-general`,
name: t`General`,
position: 0,
isVisible: true,
fields,
},
];
return { groups: [], ungroupedFields: fields, editorMode: 'ungrouped' };
}
}
return [];
}, [objectMetadataItem, t, view]);
return { groups: [], ungroupedFields: [], editorMode: 'ungrouped' };
}, [objectMetadataItem, view]);
return {
groups,
...result,
isFromView:
isDefined(view) &&
(isDefined(view.viewFieldGroups) || view.viewFields.length > 0),
@@ -0,0 +1,24 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { type FieldsWidgetEditorMode } from '@/page-layout/widgets/fields/types/FieldsWidgetEditorMode';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
type UseFieldsWidgetEditorModeParams = {
pageLayoutId: string;
widgetId: string;
};
export const useFieldsWidgetEditorMode = ({
pageLayoutId,
widgetId,
}: UseFieldsWidgetEditorModeParams): {
editorMode: FieldsWidgetEditorMode;
} => {
const fieldsWidgetEditorModeDraft = useAtomComponentStateValue(
fieldsWidgetEditorModeDraftComponentState,
pageLayoutId,
);
const editorMode = fieldsWidgetEditorModeDraft[widgetId];
return { editorMode };
};
@@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems';
import { type FieldsWidgetDisplayMode } from '@/page-layout/widgets/fields/types/FieldsWidgetDisplayMode';
import {
type FieldsWidgetGroup,
type FieldsWidgetGroupField,
@@ -7,7 +8,7 @@ import {
import { useGetViewById } from '@/views/hooks/useGetViewById';
import { useLingui } from '@lingui/react/macro';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
type UseFieldsWidgetGroupsParams = {
viewId: string | null;
@@ -33,8 +34,7 @@ export const useFieldsWidgetGroups = ({
return [];
}
// Views are seeded for object
if (isDefined(view) && isDefined(view.viewFieldGroups)) {
if (isDefined(view) && isNonEmptyArray(view.viewFieldGroups)) {
const sortedGroups = view.viewFieldGroups.toSorted(
(a, b) => a.position - b.position,
);
@@ -192,10 +192,18 @@ export const useFieldsWidgetGroups = ({
inlineFieldMetadataItems,
]);
const displayMode: FieldsWidgetDisplayMode =
isDefined(view) &&
!isNonEmptyArray(view.viewFieldGroups) &&
view.viewFields.length > 0
? 'inline'
: 'grouped';
return {
groups,
displayMode,
isFromView:
isDefined(view) &&
(isDefined(view.viewFieldGroups) || view.viewFields.length > 0),
(isNonEmptyArray(view.viewFieldGroups) || view.viewFields.length > 0),
};
};
@@ -1,28 +0,0 @@
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useMemo } from 'react';
type UseFieldsWidgetGroupsDraftParams = {
pageLayoutId: string;
widgetId: string;
};
export const useFieldsWidgetGroupsDraft = ({
pageLayoutId,
widgetId,
}: UseFieldsWidgetGroupsDraftParams): {
draftGroups: FieldsWidgetGroup[];
} => {
const fieldsWidgetGroupsDraft = useAtomComponentStateValue(
fieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const draftGroups = useMemo(
() => fieldsWidgetGroupsDraft[widgetId] ?? [],
[fieldsWidgetGroupsDraft, widgetId],
);
return { draftGroups };
};
@@ -1,9 +1,13 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
import { type FieldsWidgetDisplayMode } from '@/page-layout/widgets/fields/types/FieldsWidgetDisplayMode';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { filterDraftGroupsForDisplay } from '@/page-layout/widgets/fields/utils/filterDraftGroupsForDisplay';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useLingui } from '@lingui/react/macro';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -18,6 +22,8 @@ export const useFieldsWidgetGroupsForDisplay = ({
viewId,
objectNameSingular,
}: UseFieldsWidgetGroupsForDisplayParams) => {
const { t } = useLingui();
const isPageLayoutInEditMode = useAtomComponentStateValue(
isPageLayoutInEditModeComponentState,
);
@@ -26,16 +32,58 @@ export const useFieldsWidgetGroupsForDisplay = ({
fieldsWidgetGroupsDraftComponentState,
);
const fieldsWidgetUngroupedFieldsDraft = useAtomComponentStateValue(
fieldsWidgetUngroupedFieldsDraftComponentState,
);
const fieldsWidgetEditorModeDraft = useAtomComponentStateValue(
fieldsWidgetEditorModeDraftComponentState,
);
const viewGroups = useFieldsWidgetGroups({
viewId,
objectNameSingular,
});
const draftGroupsForWidget = fieldsWidgetGroupsDraft[widgetId];
const draftEditorMode = fieldsWidgetEditorModeDraft[widgetId];
const ungroupedFieldsForWidget = fieldsWidgetUngroupedFieldsDraft[widgetId];
const hasDraftGroups =
isDefined(draftGroupsForWidget) && draftGroupsForWidget.length > 0;
const hasDraftUngroupedFields =
isDefined(ungroupedFieldsForWidget) && ungroupedFieldsForWidget.length > 0;
const groups = useMemo<FieldsWidgetGroup[]>(() => {
if (
isPageLayoutInEditMode &&
draftEditorMode === 'ungrouped' &&
hasDraftUngroupedFields
) {
const visibleFields = ungroupedFieldsForWidget
.filter((field) => field.isVisible)
.sort((a, b) => a.position - b.position)
.map((field, index) => ({
...field,
globalIndex: index,
}));
if (visibleFields.length === 0) {
return [];
}
return [
{
id: `${widgetId}-ungrouped-display`,
name: t`General`,
position: 0,
isVisible: true,
fields: visibleFields,
},
];
}
if (isPageLayoutInEditMode && hasDraftGroups) {
return filterDraftGroupsForDisplay(draftGroupsForWidget);
}
@@ -43,10 +91,27 @@ export const useFieldsWidgetGroupsForDisplay = ({
return viewGroups.groups;
}, [
isPageLayoutInEditMode,
draftEditorMode,
hasDraftGroups,
hasDraftUngroupedFields,
draftGroupsForWidget,
ungroupedFieldsForWidget,
viewGroups.groups,
widgetId,
t,
]);
return { groups };
const displayMode: FieldsWidgetDisplayMode = (() => {
if (isPageLayoutInEditMode) {
if (draftEditorMode === 'ungrouped' && hasDraftUngroupedFields) {
return 'inline';
}
if (hasDraftGroups) {
return 'grouped';
}
}
return viewGroups.displayMode;
})();
return { groups, displayMode };
};
@@ -1,7 +1,15 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { type FieldsWidgetEditorMode } from '@/page-layout/widgets/fields/types/FieldsWidgetEditorMode';
import {
type FieldsWidgetGroup,
type FieldsWidgetGroupField,
} from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback, useEffect } from 'react';
@@ -9,13 +17,17 @@ import { useCallback, useEffect } from 'react';
type UseInitializeFieldsWidgetGroupsDraftParams = {
pageLayoutId: string;
widgetId: string;
serverGroups: FieldsWidgetGroup[];
persistedGroups: FieldsWidgetGroup[];
persistedUngroupedFields: FieldsWidgetGroupField[];
persistedEditorMode: FieldsWidgetEditorMode;
};
export const useInitializeFieldsWidgetGroupsDraft = ({
pageLayoutId,
widgetId,
serverGroups,
persistedGroups,
persistedUngroupedFields,
persistedEditorMode,
}: UseInitializeFieldsWidgetGroupsDraftParams) => {
const fieldsWidgetGroupsDraftState = useAtomComponentStateCallbackState(
fieldsWidgetGroupsDraftComponentState,
@@ -27,6 +39,29 @@ export const useInitializeFieldsWidgetGroupsDraft = ({
pageLayoutId,
);
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const fieldsWidgetUngroupedFieldsPersistedState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsPersistedComponentState,
pageLayoutId,
);
const fieldsWidgetEditorModeDraftState = useAtomComponentStateCallbackState(
fieldsWidgetEditorModeDraftComponentState,
pageLayoutId,
);
const fieldsWidgetEditorModePersistedState =
useAtomComponentStateCallbackState(
fieldsWidgetEditorModePersistedComponentState,
pageLayoutId,
);
const hasInitializedFieldsWidgetGroupsDraftState =
useAtomComponentStateCallbackState(
hasInitializedFieldsWidgetGroupsDraftComponentState,
@@ -51,12 +86,36 @@ export const useInitializeFieldsWidgetGroupsDraft = ({
if (!hasDraftForWidget) {
store.set(fieldsWidgetGroupsDraftState, (prev) => ({
...prev,
[widgetId]: serverGroups,
[widgetId]: persistedGroups,
}));
store.set(fieldsWidgetGroupsPersistedState, (prev) => ({
...prev,
[widgetId]: serverGroups,
[widgetId]: persistedGroups,
}));
store.set(fieldsWidgetUngroupedFieldsDraftState, (prev) => ({
...prev,
[widgetId]: persistedUngroupedFields,
}));
store.set(fieldsWidgetUngroupedFieldsPersistedState, (prev) => ({
...prev,
[widgetId]: persistedUngroupedFields,
}));
}
const currentEditorModes = store.get(fieldsWidgetEditorModeDraftState);
if (!(widgetId in currentEditorModes)) {
store.set(fieldsWidgetEditorModeDraftState, (prev) => ({
...prev,
[widgetId]: persistedEditorMode,
}));
store.set(fieldsWidgetEditorModePersistedState, (prev) => ({
...prev,
[widgetId]: persistedEditorMode,
}));
}
@@ -68,14 +127,23 @@ export const useInitializeFieldsWidgetGroupsDraft = ({
hasInitializedFieldsWidgetGroupsDraftState,
fieldsWidgetGroupsDraftState,
fieldsWidgetGroupsPersistedState,
fieldsWidgetUngroupedFieldsDraftState,
fieldsWidgetUngroupedFieldsPersistedState,
fieldsWidgetEditorModeDraftState,
fieldsWidgetEditorModePersistedState,
widgetId,
serverGroups,
persistedGroups,
persistedUngroupedFields,
persistedEditorMode,
store,
]);
useEffect(() => {
if (serverGroups.length > 0) {
const hasData =
persistedGroups.length > 0 || persistedUngroupedFields.length > 0;
if (hasData) {
initializeDraft();
}
}, [serverGroups, initializeDraft]);
}, [persistedGroups, persistedUngroupedFields, initializeDraft]);
};
@@ -0,0 +1,56 @@
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
type UseMoveUngroupedFieldInDraftParams = {
pageLayoutId: string;
widgetId: string;
};
export const useMoveUngroupedFieldInDraft = ({
pageLayoutId,
widgetId,
}: UseMoveUngroupedFieldInDraftParams) => {
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const store = useStore();
const moveField = useCallback(
(sourceIndex: number, destinationIndex: number) => {
store.set(fieldsWidgetUngroupedFieldsDraftState, (prev) => {
const currentFields = prev[widgetId] ?? [];
const sortedFields = [...currentFields].sort(
(a, b) => a.position - b.position,
);
const [movedField] = sortedFields.splice(sourceIndex, 1);
if (!movedField) {
return prev;
}
sortedFields.splice(destinationIndex, 0, movedField);
const updatedFields = sortedFields.map((field, index) => ({
...field,
position: index,
globalIndex: index,
}));
return {
...prev,
[widgetId]: updatedFields,
};
});
},
[fieldsWidgetUngroupedFieldsDraftState, widgetId, store],
);
return { moveField };
};
@@ -0,0 +1,42 @@
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
type UseToggleUngroupedFieldVisibilityInDraftParams = {
pageLayoutId: string;
widgetId: string;
};
export const useToggleUngroupedFieldVisibilityInDraft = ({
pageLayoutId,
widgetId,
}: UseToggleUngroupedFieldVisibilityInDraftParams) => {
const fieldsWidgetUngroupedFieldsDraftState =
useAtomComponentStateCallbackState(
fieldsWidgetUngroupedFieldsDraftComponentState,
pageLayoutId,
);
const store = useStore();
const toggleFieldVisibility = useCallback(
(fieldMetadataId: string) => {
store.set(fieldsWidgetUngroupedFieldsDraftState, (prev) => {
const currentFields = prev[widgetId] ?? [];
return {
...prev,
[widgetId]: currentFields.map((field) =>
field.fieldMetadataItem.id === fieldMetadataId
? { ...field, isVisible: !field.isVisible }
: field,
),
};
});
},
[fieldsWidgetUngroupedFieldsDraftState, widgetId, store],
);
return { toggleFieldVisibility };
};
@@ -0,0 +1 @@
export type FieldsWidgetDisplayMode = 'grouped' | 'inline';
@@ -0,0 +1 @@
export type FieldsWidgetEditorMode = 'grouped' | 'ungrouped';
@@ -0,0 +1,3 @@
export const getFieldsConfigurationGroupEditDropdownId = (groupId: string) => {
return `fields-configuration-group-edit-${groupId}`;
};
@@ -0,0 +1,5 @@
export const getFieldsConfigurationGroupRenameDropdownId = (
groupId: string,
) => {
return `fields-configuration-group-rename-${groupId}`;
};
@@ -1,7 +1,7 @@
import { styled } from '@linaria/react';
import { type WidgetCardVariant } from '~/modules/page-layout/widgets/types/WidgetCardVariant';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type WidgetCardVariant } from '~/modules/page-layout/widgets/types/WidgetCardVariant';
type WidgetCardStyledProps = {
variant: WidgetCardVariant;
@@ -129,6 +129,8 @@ export type WidgetCardProps = {
children?: React.ReactNode;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
'data-testid'?: string;
'data-widget-id'?: string;
};
export const WidgetCard = ({
@@ -144,6 +146,8 @@ export const WidgetCard = ({
children,
onMouseEnter,
onMouseLeave,
'data-testid': dataTestId,
'data-widget-id': dataWidgetId,
}: WidgetCardProps) => {
return (
<StyledWidgetCard
@@ -159,6 +163,8 @@ export const WidgetCard = ({
className={className}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
data-testid={dataTestId}
data-widget-id={dataWidgetId}
>
{children}
</StyledWidgetCard>
@@ -1,5 +1,5 @@
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
export const coreViewsState = createAtomState<CoreViewWithRelations[]>({
key: 'coreViewsState',