Support ungrouped fields + improve edition UX (#18224)
## Demo https://github.com/user-attachments/assets/59e530ea-1c5b-44be-a012-42551e68221c ## Demo – creating a new group https://github.com/user-attachments/assets/e8511bc3-d586-422c-aca8-b02794a0c84f ## Demo – ungrouped fields https://github.com/user-attachments/assets/6ded4a90-fb08-485e-ad08-086f3a970752 Closes https://github.com/twentyhq/core-team-issues/issues/2232 Closes https://github.com/twentyhq/core-team-issues/issues/2237 Closes https://github.com/twentyhq/core-team-issues/issues/2238
This commit is contained in:
committed by
GitHub
parent
3b2bf39565
commit
5b544809f7
+47
@@ -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(
|
||||
|
||||
+11
@@ -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,
|
||||
});
|
||||
+11
@@ -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,
|
||||
});
|
||||
+11
@@ -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,
|
||||
});
|
||||
+11
@@ -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,
|
||||
});
|
||||
+79
-1
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+32
-13
@@ -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');
|
||||
|
||||
+35
-9
@@ -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
-4
@@ -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) {
|
||||
|
||||
+59
-5
@@ -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)
|
||||
}
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
|
||||
+23
-3
@@ -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>
|
||||
|
||||
+94
@@ -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>
|
||||
);
|
||||
};
|
||||
+27
-139
@@ -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}
|
||||
|
||||
+128
@@ -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>
|
||||
);
|
||||
};
|
||||
+71
@@ -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)}
|
||||
/>
|
||||
));
|
||||
};
|
||||
+8
-2
@@ -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;
|
||||
|
||||
+586
@@ -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();
|
||||
});
|
||||
},
|
||||
};
|
||||
+94
-18
@@ -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 };
|
||||
|
||||
+95
-4
@@ -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 };
|
||||
|
||||
+29
-21
@@ -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),
|
||||
|
||||
+24
@@ -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 };
|
||||
};
|
||||
+12
-4
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
-28
@@ -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 };
|
||||
};
|
||||
+66
-1
@@ -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 };
|
||||
};
|
||||
|
||||
+76
-8
@@ -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]);
|
||||
};
|
||||
|
||||
+56
@@ -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 };
|
||||
};
|
||||
+42
@@ -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 };
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type FieldsWidgetDisplayMode = 'grouped' | 'inline';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export type FieldsWidgetEditorMode = 'grouped' | 'ungrouped';
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const getFieldsConfigurationGroupEditDropdownId = (groupId: string) => {
|
||||
return `fields-configuration-group-edit-${groupId}`;
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export const getFieldsConfigurationGroupRenameDropdownId = (
|
||||
groupId: string,
|
||||
) => {
|
||||
return `fields-configuration-group-rename-${groupId}`;
|
||||
};
|
||||
+8
-2
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user