Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd174a6099 | ||
|
|
a06b35c70d |
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
|
||||
export const SaveRecordPageLayoutSingleRecordAction = () => {
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
@@ -24,11 +24,11 @@ export const SaveRecordPageLayoutSingleRecordAction = () => {
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const handleClick = async () => {
|
||||
await saveFieldsWidgetGroups();
|
||||
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
await saveFieldsWidgetGroups();
|
||||
|
||||
closeSidePanelMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
}
|
||||
|
||||
+48
@@ -1,10 +1,16 @@
|
||||
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseCreateFieldsWidgetEditorGroupParams = {
|
||||
pageLayoutId: string;
|
||||
@@ -31,6 +37,11 @@ export const useCreateFieldsWidgetEditorGroup = ({
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const createGroup = useCallback(
|
||||
@@ -74,6 +85,42 @@ export const useCreateFieldsWidgetEditorGroup = ({
|
||||
...prev,
|
||||
[widgetId]: 'grouped' as const,
|
||||
}));
|
||||
|
||||
const pageLayoutDraft = store.get(pageLayoutDraftState);
|
||||
|
||||
const widget = pageLayoutDraft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((w) => w.id === widgetId);
|
||||
|
||||
const fieldsConfiguration =
|
||||
isDefined(widget?.configuration) &&
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.FIELDS
|
||||
? (widget.configuration as FieldsConfiguration)
|
||||
: null;
|
||||
|
||||
if (isDefined(fieldsConfiguration)) {
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: prev.tabs.map((tab) => ({
|
||||
...tab,
|
||||
widgets: tab.widgets.map((w) =>
|
||||
w.id === widgetId
|
||||
? {
|
||||
...w,
|
||||
configuration: {
|
||||
...fieldsConfiguration,
|
||||
newFieldDefaultConfiguration: {
|
||||
isVisible: true,
|
||||
viewFieldGroupId: newId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: w,
|
||||
),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
const allDraftGroups = store.get(fieldsWidgetGroupsDraftState);
|
||||
|
||||
@@ -118,6 +165,7 @@ export const useCreateFieldsWidgetEditorGroup = ({
|
||||
fieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetUngroupedFieldsDraftState,
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
pageLayoutDraftState,
|
||||
widgetId,
|
||||
store,
|
||||
],
|
||||
|
||||
+76
@@ -1,9 +1,15 @@
|
||||
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type FieldsConfiguration,
|
||||
WidgetConfigurationType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type UseDeleteFieldsWidgetEditorGroupParams = {
|
||||
pageLayoutId: string;
|
||||
@@ -30,6 +36,11 @@ export const useDeleteFieldsWidgetEditorGroup = ({
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const deleteGroup = useCallback(
|
||||
@@ -44,6 +55,25 @@ export const useDeleteFieldsWidgetEditorGroup = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const pageLayoutDraft = store.get(pageLayoutDraftState);
|
||||
|
||||
const widget = pageLayoutDraft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((w) => w.id === widgetId);
|
||||
|
||||
const fieldsConfiguration =
|
||||
isDefined(widget?.configuration) &&
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.FIELDS
|
||||
? (widget.configuration as FieldsConfiguration)
|
||||
: null;
|
||||
|
||||
const currentViewFieldGroupId =
|
||||
fieldsConfiguration?.newFieldDefaultConfiguration?.viewFieldGroupId ??
|
||||
null;
|
||||
|
||||
const isReferencedGroupBeingDeleted = currentViewFieldGroupId === groupId;
|
||||
|
||||
const deletedGroupFields = deletedGroup.fields;
|
||||
const remainingGroups = currentGroups.filter(
|
||||
(group) => group.id !== groupId,
|
||||
@@ -69,6 +99,26 @@ export const useDeleteFieldsWidgetEditorGroup = ({
|
||||
[widgetId]: 'ungrouped' as const,
|
||||
}));
|
||||
|
||||
if (isDefined(fieldsConfiguration)) {
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: prev.tabs.map((tab) => ({
|
||||
...tab,
|
||||
widgets: tab.widgets.map((w) =>
|
||||
w.id === widgetId
|
||||
? {
|
||||
...w,
|
||||
configuration: {
|
||||
...fieldsConfiguration,
|
||||
newFieldDefaultConfiguration: null,
|
||||
},
|
||||
}
|
||||
: w,
|
||||
),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,11 +162,37 @@ export const useDeleteFieldsWidgetEditorGroup = ({
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
if (isReferencedGroupBeingDeleted && isDefined(fieldsConfiguration)) {
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: prev.tabs.map((tab) => ({
|
||||
...tab,
|
||||
widgets: tab.widgets.map((w) =>
|
||||
w.id === widgetId
|
||||
? {
|
||||
...w,
|
||||
configuration: {
|
||||
...fieldsConfiguration,
|
||||
newFieldDefaultConfiguration: {
|
||||
isVisible:
|
||||
fieldsConfiguration.newFieldDefaultConfiguration
|
||||
?.isVisible ?? true,
|
||||
viewFieldGroupId: targetGroup.id,
|
||||
},
|
||||
},
|
||||
}
|
||||
: w,
|
||||
),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
},
|
||||
[
|
||||
fieldsWidgetGroupsDraftState,
|
||||
fieldsWidgetUngroupedFieldsDraftState,
|
||||
fieldsWidgetEditorModeDraftState,
|
||||
pageLayoutDraftState,
|
||||
widgetId,
|
||||
store,
|
||||
],
|
||||
|
||||
+1
-12
@@ -1,6 +1,4 @@
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { getLastGroupId } from '@/page-layout/widgets/fields/utils/getLastGroupId';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -22,13 +20,6 @@ export const useGetNewFieldDefaultConfiguration = ({
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const fieldsWidgetGroupsDraft = useAtomComponentStateValue(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const draftGroups = fieldsWidgetGroupsDraft[widgetId] ?? [];
|
||||
|
||||
const widget = pageLayoutDraft.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((w) => w.id === widgetId);
|
||||
@@ -39,13 +30,11 @@ export const useGetNewFieldDefaultConfiguration = ({
|
||||
? (widget.configuration as FieldsConfiguration)
|
||||
: null;
|
||||
|
||||
const lastGroupId = getLastGroupId(draftGroups);
|
||||
|
||||
const persisted = fieldsConfiguration?.newFieldDefaultConfiguration;
|
||||
|
||||
const newFieldDefaultConfiguration = {
|
||||
isVisible: persisted?.isVisible ?? true,
|
||||
viewFieldGroupId: persisted?.viewFieldGroupId ?? lastGroupId,
|
||||
viewFieldGroupId: persisted?.viewFieldGroupId ?? null,
|
||||
};
|
||||
|
||||
return { newFieldDefaultConfiguration, fieldsConfiguration };
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
import { getLastGroupId } from '@/page-layout/widgets/fields/utils/getLastGroupId';
|
||||
|
||||
const makeGroup = (
|
||||
overrides: Partial<FieldsWidgetGroup> & { id: string },
|
||||
): FieldsWidgetGroup => ({
|
||||
name: 'Group',
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
fields: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('getLastGroupId', () => {
|
||||
it('should return null for empty array', () => {
|
||||
expect(getLastGroupId([])).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the id of a single group', () => {
|
||||
const groups = [makeGroup({ id: 'g1', position: 0 })];
|
||||
|
||||
expect(getLastGroupId(groups)).toBe('g1');
|
||||
});
|
||||
|
||||
it('should return the id of the group with the highest position', () => {
|
||||
const groups = [
|
||||
makeGroup({ id: 'g1', position: 0 }),
|
||||
makeGroup({ id: 'g2', position: 2 }),
|
||||
makeGroup({ id: 'g3', position: 1 }),
|
||||
];
|
||||
|
||||
expect(getLastGroupId(groups)).toBe('g2');
|
||||
});
|
||||
|
||||
it('should not mutate the original array', () => {
|
||||
const groups = [
|
||||
makeGroup({ id: 'g2', position: 2 }),
|
||||
makeGroup({ id: 'g1', position: 0 }),
|
||||
];
|
||||
|
||||
getLastGroupId(groups);
|
||||
|
||||
expect(groups[0].id).toBe('g2');
|
||||
expect(groups[1].id).toBe('g1');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
|
||||
|
||||
export const getLastGroupId = (groups: FieldsWidgetGroup[]): string | null => {
|
||||
if (groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedGroups = [...groups].sort((a, b) => a.position - b.position);
|
||||
|
||||
return sortedGroups[sortedGroups.length - 1].id;
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FieldsConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/fields-configuration.dto';
|
||||
import {
|
||||
PageLayoutWidgetException,
|
||||
PageLayoutWidgetExceptionCode,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
|
||||
export const validateFieldsWidgetNewFieldDefaultConfiguration = ({
|
||||
configuration,
|
||||
flatViewFieldGroupMaps,
|
||||
}: {
|
||||
configuration: FieldsConfigurationDTO;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
}): void => {
|
||||
const { viewId, newFieldDefaultConfiguration } = configuration;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasViewFieldGroups = Object.values(
|
||||
flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.some((group) => !isDefined(group.deletedAt) && group.viewId === viewId);
|
||||
|
||||
if (hasViewFieldGroups && !isDefined(newFieldDefaultConfiguration)) {
|
||||
throw new PageLayoutWidgetException(
|
||||
'Fields widget has view field groups but newFieldDefaultConfiguration is not defined. It must be set when groups exist.',
|
||||
PageLayoutWidgetExceptionCode.INVALID_PAGE_LAYOUT_WIDGET_DATA,
|
||||
);
|
||||
}
|
||||
};
|
||||
+20
@@ -20,7 +20,10 @@ import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/e
|
||||
import { type FlatPageLayout } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout.type';
|
||||
import { reconstructFlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
|
||||
import { UpdatePageLayoutTabWithWidgetsInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab-with-widgets.input';
|
||||
import { type FieldsConfigurationDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/fields-configuration.dto';
|
||||
import { UpdatePageLayoutWidgetWithIdInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget-with-id.input';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import { validateFieldsWidgetNewFieldDefaultConfiguration } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-fields-widget-new-field-default-configuration.util';
|
||||
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
|
||||
import { PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import {
|
||||
@@ -477,6 +480,23 @@ export class PageLayoutUpdateService {
|
||||
|
||||
const now = new Date();
|
||||
|
||||
for (const widgetInput of [
|
||||
...entitiesToCreate,
|
||||
...entitiesToUpdate,
|
||||
...entitiesToRestoreAndUpdate,
|
||||
]) {
|
||||
if (
|
||||
isDefined(widgetInput.configuration) &&
|
||||
widgetInput.configuration.configurationType ===
|
||||
WidgetConfigurationType.FIELDS
|
||||
) {
|
||||
validateFieldsWidgetNewFieldDefaultConfiguration({
|
||||
configuration: widgetInput.configuration as FieldsConfigurationDTO,
|
||||
flatViewFieldGroupMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const widgetsToCreate: FlatPageLayoutWidget[] = entitiesToCreate.map(
|
||||
(widgetInput) => {
|
||||
const widgetId = widgetInput.id ?? v4();
|
||||
|
||||
Reference in New Issue
Block a user