Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1334e1707d | ||
|
|
e87285d223 | ||
|
|
e2dae88a3d |
File diff suppressed because one or more lines are too long
+16
@@ -94,6 +94,20 @@ export const PageLayoutInitializationQueryEffect = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageLayoutIsInitialized && isDefined(pageLayout)) {
|
||||
const currentPersisted = store.get(
|
||||
pageLayoutPersistedComponentCallbackState,
|
||||
);
|
||||
|
||||
// After a reset, pageLayoutIsInitialized is set to false while the
|
||||
// metadata store refetches. Skip re-initialization when the data
|
||||
// hasn't actually changed yet to avoid overwriting with stale data.
|
||||
if (
|
||||
isDefined(currentPersisted) &&
|
||||
isDeeplyEqual(pageLayout, currentPersisted)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializePageLayout(pageLayout);
|
||||
setPageLayoutIsInitialized(true);
|
||||
}
|
||||
@@ -102,6 +116,8 @@ export const PageLayoutInitializationQueryEffect = ({
|
||||
pageLayoutIsInitialized,
|
||||
pageLayout,
|
||||
setPageLayoutIsInitialized,
|
||||
store,
|
||||
pageLayoutPersistedComponentCallbackState,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { PAGE_LAYOUT_TAB_FRAGMENT } from '@/dashboards/graphql/fragments/pageLayoutTabFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const RESET_PAGE_LAYOUT_TAB_TO_DEFAULT = gql`
|
||||
${PAGE_LAYOUT_TAB_FRAGMENT}
|
||||
mutation ResetPageLayoutTabToDefault($id: String!) {
|
||||
resetPageLayoutTabToDefault(id: $id) {
|
||||
...PageLayoutTabFragment
|
||||
}
|
||||
}
|
||||
`;
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
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 { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
|
||||
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
|
||||
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
import { ResetPageLayoutTabToDefaultDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useResetPageLayoutTabToDefault = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const [resetMutation] = useMutation(ResetPageLayoutTabToDefaultDocument);
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
|
||||
const { invalidateMetadataStore } = useInvalidateMetadataStore();
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const hasInitializedState = useAtomComponentStateCallbackState(
|
||||
hasInitializedFieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const groupsDraftState = useAtomComponentStateCallbackState(
|
||||
fieldsWidgetGroupsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const groupsPersistedState = useAtomComponentStateCallbackState(
|
||||
fieldsWidgetGroupsPersistedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const ungroupedDraftState = useAtomComponentStateCallbackState(
|
||||
fieldsWidgetUngroupedFieldsDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const ungroupedPersistedState = useAtomComponentStateCallbackState(
|
||||
fieldsWidgetUngroupedFieldsPersistedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const editorModeDraftState = useAtomComponentStateCallbackState(
|
||||
fieldsWidgetEditorModeDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const editorModePersistedState = useAtomComponentStateCallbackState(
|
||||
fieldsWidgetEditorModePersistedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutIsInitializedState = useAtomComponentStateCallbackState(
|
||||
pageLayoutIsInitializedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const clearAllWidgetDraftStates = useCallback(() => {
|
||||
store.set(hasInitializedState, {});
|
||||
store.set(groupsDraftState, {});
|
||||
store.set(groupsPersistedState, {});
|
||||
store.set(ungroupedDraftState, {});
|
||||
store.set(ungroupedPersistedState, {});
|
||||
store.set(editorModeDraftState, {});
|
||||
store.set(editorModePersistedState, {});
|
||||
}, [
|
||||
store,
|
||||
hasInitializedState,
|
||||
groupsDraftState,
|
||||
groupsPersistedState,
|
||||
ungroupedDraftState,
|
||||
ungroupedPersistedState,
|
||||
editorModeDraftState,
|
||||
editorModePersistedState,
|
||||
]);
|
||||
|
||||
const resetPageLayoutTabToDefault = useCallback(
|
||||
async (tabId: string) => {
|
||||
try {
|
||||
await resetMutation({
|
||||
variables: { id: tabId },
|
||||
});
|
||||
|
||||
exitLayoutCustomizationMode();
|
||||
clearAllWidgetDraftStates();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
store.set(pageLayoutIsInitializedState, false);
|
||||
invalidateMetadataStore();
|
||||
} catch (error) {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
handleMetadataError(error, {
|
||||
primaryMetadataName: 'pageLayoutTab',
|
||||
operationType: CrudOperationType.UPDATE,
|
||||
});
|
||||
} else {
|
||||
enqueueErrorSnackBar({ message: t`An error occurred.` });
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
resetMutation,
|
||||
exitLayoutCustomizationMode,
|
||||
clearAllWidgetDraftStates,
|
||||
setIsPageLayoutInEditMode,
|
||||
invalidateMetadataStore,
|
||||
handleMetadataError,
|
||||
enqueueErrorSnackBar,
|
||||
],
|
||||
);
|
||||
|
||||
return { resetPageLayoutTabToDefault };
|
||||
};
|
||||
+21
-3
@@ -1,5 +1,7 @@
|
||||
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
|
||||
import { useExitLayoutCustomizationMode } from '@/layout-customization/hooks/useExitLayoutCustomizationMode';
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useInvalidateMetadataStore } from '@/metadata-store/hooks/useInvalidateMetadataStore';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
|
||||
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
|
||||
@@ -8,12 +10,13 @@ import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
|
||||
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
|
||||
import { pageLayoutIsInitializedComponentState } from '@/page-layout/states/pageLayoutIsInitializedComponentState';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -32,6 +35,10 @@ export const useResetPageLayoutWidgetToDefault = (
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
|
||||
const { invalidateMetadataStore } = useInvalidateMetadataStore();
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
const { invalidateMetadataStore } = useInvalidateMetadataStore();
|
||||
|
||||
@@ -72,6 +79,11 @@ export const useResetPageLayoutWidgetToDefault = (
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutIsInitializedState = useAtomComponentStateCallbackState(
|
||||
pageLayoutIsInitializedComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const clearWidgetDraftState = useCallback(
|
||||
(widgetId: string) => {
|
||||
const removeWidgetEntry = <T>(prev: Record<string, T>) => {
|
||||
@@ -107,8 +119,11 @@ export const useResetPageLayoutWidgetToDefault = (
|
||||
variables: { id: widgetId },
|
||||
});
|
||||
|
||||
closeSidePanelMenu();
|
||||
exitLayoutCustomizationMode();
|
||||
clearWidgetDraftState(widgetId);
|
||||
setIsPageLayoutInEditMode(false);
|
||||
store.set(pageLayoutIsInitializedState, false);
|
||||
closeSidePanelMenu();
|
||||
invalidateMetadataStore();
|
||||
} catch (error) {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
@@ -123,6 +138,9 @@ export const useResetPageLayoutWidgetToDefault = (
|
||||
},
|
||||
[
|
||||
resetMutation,
|
||||
exitLayoutCustomizationMode,
|
||||
clearWidgetDraftState,
|
||||
setIsPageLayoutInEditMode,
|
||||
closeSidePanelMenu,
|
||||
clearWidgetDraftState,
|
||||
invalidateMetadataStore,
|
||||
|
||||
+3
-1
@@ -41,12 +41,14 @@ export const pageLayoutsWithRelationsSelector = createAtomSelector<
|
||||
}
|
||||
}
|
||||
|
||||
return flatPageLayouts.map((flatPageLayout) => ({
|
||||
const result = flatPageLayouts.map((flatPageLayout) => ({
|
||||
...flatPageLayout,
|
||||
tabs: (tabsByPageLayoutId.get(flatPageLayout.id) ?? []).map((tab) => ({
|
||||
...tab,
|
||||
widgets: widgetsByTabId.get(tab.id) ?? [],
|
||||
})),
|
||||
}));
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
+2
-6
@@ -13,14 +13,9 @@ const buildWidgetPosition = (
|
||||
) => {
|
||||
switch (tabLayoutMode) {
|
||||
case PageLayoutTabLayoutMode.VERTICAL_LIST: {
|
||||
const index =
|
||||
widget.position?.__typename === 'PageLayoutWidgetVerticalListPosition'
|
||||
? widget.position.index
|
||||
: widgetIndex;
|
||||
|
||||
return {
|
||||
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
|
||||
index,
|
||||
index: widgetIndex, // we should read in widget.position.index instead. Not sure why this change
|
||||
};
|
||||
}
|
||||
case PageLayoutTabLayoutMode.CANVAS:
|
||||
@@ -57,6 +52,7 @@ export const convertPageLayoutDraftToUpdateInput = (
|
||||
id: tab.id,
|
||||
title: tab.title,
|
||||
position: tab.position,
|
||||
icon: tab.icon ?? null,
|
||||
layoutMode: tab.layoutMode,
|
||||
widgets: widgets.map((widget, widgetIndex) => ({
|
||||
id: widget.id,
|
||||
|
||||
+24
-2
@@ -133,6 +133,13 @@ export const useFieldsWidgetEditorGroupsData = ({
|
||||
};
|
||||
});
|
||||
|
||||
const viewFieldByFieldMetadataId = new Map(
|
||||
(view.viewFields ?? []).map((viewField) => [
|
||||
viewField.fieldMetadataId,
|
||||
viewField,
|
||||
]),
|
||||
);
|
||||
|
||||
const lastGroup = groups[groups.length - 1];
|
||||
const lastFieldPosition =
|
||||
lastGroup.fields.length > 0
|
||||
@@ -145,8 +152,23 @@ export const useFieldsWidgetEditorGroupsData = ({
|
||||
startPosition: lastFieldPosition,
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
lastGroup.fields = [...lastGroup.fields, ...missingFields];
|
||||
const missingFieldsWithViewFieldId = missingFields.map((field) => {
|
||||
const existingViewField = viewFieldByFieldMetadataId.get(
|
||||
field.fieldMetadataItem.id,
|
||||
);
|
||||
|
||||
if (isDefined(existingViewField)) {
|
||||
return { ...field, viewFieldId: existingViewField.id };
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
|
||||
if (missingFieldsWithViewFieldId.length > 0) {
|
||||
lastGroup.fields = [
|
||||
...lastGroup.fields,
|
||||
...missingFieldsWithViewFieldId,
|
||||
];
|
||||
}
|
||||
|
||||
return { groups, ungroupedFields: [], editorMode: 'grouped' };
|
||||
|
||||
+1
@@ -165,6 +165,7 @@ export const SidePanelPageLayoutFieldsSettings = () => {
|
||||
Icon={IconRefreshDot}
|
||||
label={t`Reset to default`}
|
||||
onClick={handleResetToDefault}
|
||||
disabled={!widgetInEditMode.isOverridden}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem itemId="delete" onEnter={handleDelete}>
|
||||
|
||||
+38
@@ -2,6 +2,7 @@ import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { useDeletePageLayoutTab } from '@/page-layout/hooks/useDeletePageLayoutTab';
|
||||
import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab';
|
||||
import { useMovePageLayoutTab } from '@/page-layout/hooks/useMovePageLayoutTab';
|
||||
import { useResetPageLayoutTabToDefault } from '@/page-layout/hooks/useResetPageLayoutTabToDefault';
|
||||
import { useSetAsPinnedTab } from '@/page-layout/hooks/useSetAsPinnedTab';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
|
||||
@@ -11,6 +12,8 @@ import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/side-panel/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
@@ -21,10 +24,13 @@ import {
|
||||
IconChevronRight,
|
||||
IconCopyPlus,
|
||||
IconPinned,
|
||||
IconRefreshDot,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
import { PageLayoutType } from '~/generated-metadata/graphql';
|
||||
|
||||
const RESET_TAB_TO_DEFAULT_MODAL_ID = 'reset-tab-to-default-modal';
|
||||
|
||||
type SidePanelPageLayoutTabSettingsContentProps = {
|
||||
pageLayoutId: string;
|
||||
recordId: string;
|
||||
@@ -63,6 +69,9 @@ export const SidePanelPageLayoutTabSettingsContent = ({
|
||||
tabListInstanceId,
|
||||
});
|
||||
const { setAsPinnedTab } = useSetAsPinnedTab(pageLayoutId);
|
||||
const { resetPageLayoutTabToDefault } =
|
||||
useResetPageLayoutTabToDefault(pageLayoutId);
|
||||
const { openModal } = useModal();
|
||||
|
||||
if (!isDefined(pageLayoutTabSettingsOpenTabId)) {
|
||||
return null;
|
||||
@@ -83,6 +92,14 @@ export const SidePanelPageLayoutTabSettingsContent = ({
|
||||
const canSetAsPinned =
|
||||
isRecordPage && !isAlreadyPinned && tabsSorted.length > 1;
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
openModal(RESET_TAB_TO_DEFAULT_MODAL_ID);
|
||||
};
|
||||
|
||||
const handleConfirmReset = () => {
|
||||
resetPageLayoutTabToDefault(tab.id);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteTab(tab.id);
|
||||
setPageLayoutTabSettingsOpenTabId(null);
|
||||
@@ -94,6 +111,7 @@ export const SidePanelPageLayoutTabSettingsContent = ({
|
||||
...(canMoveRight ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.MOVE_RIGHT] : []),
|
||||
...(canSetAsPinned ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.SET_AS_PINNED] : []),
|
||||
TAB_SETTINGS_SELECTABLE_ITEM_IDS.DUPLICATE,
|
||||
TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT,
|
||||
...(canDelete ? [TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE] : []),
|
||||
];
|
||||
|
||||
@@ -151,6 +169,18 @@ export const SidePanelPageLayoutTabSettingsContent = ({
|
||||
onClick={() => duplicateTab(tab.id)}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
<SelectableListItem
|
||||
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
|
||||
onEnter={handleResetToDefault}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={TAB_SETTINGS_SELECTABLE_ITEM_IDS.RESET_TO_DEFAULT}
|
||||
Icon={IconRefreshDot}
|
||||
label={t`Reset to default`}
|
||||
onClick={handleResetToDefault}
|
||||
disabled={!tab.isOverridden}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
{canDelete && (
|
||||
<SelectableListItem
|
||||
itemId={TAB_SETTINGS_SELECTABLE_ITEM_IDS.DELETE}
|
||||
@@ -166,6 +196,14 @@ export const SidePanelPageLayoutTabSettingsContent = ({
|
||||
)}
|
||||
</SidePanelGroup>
|
||||
</SidePanelList>
|
||||
<ConfirmationModal
|
||||
modalInstanceId={RESET_TAB_TO_DEFAULT_MODAL_ID}
|
||||
title={t`Reset to default`}
|
||||
subtitle={t`This will cancel all modifications done on the tab and its widgets. This action cannot be undone.`}
|
||||
onConfirmClick={handleConfirmReset}
|
||||
confirmButtonText={t`Reset`}
|
||||
confirmButtonAccent="blue"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
@@ -3,5 +3,6 @@ export const TAB_SETTINGS_SELECTABLE_ITEM_IDS = {
|
||||
MOVE_RIGHT: 'tab-move-right',
|
||||
SET_AS_PINNED: 'tab-set-as-pinned',
|
||||
DUPLICATE: 'tab-duplicate',
|
||||
RESET_TO_DEFAULT: 'tab-reset-to-default',
|
||||
DELETE: 'tab-delete',
|
||||
} as const;
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
|
||||
export type CascadingIsOverriddenContext = {
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
flatViewFieldMaps: FlatViewFieldMaps;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
};
|
||||
+1
-2
@@ -15,7 +15,6 @@ import {
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -73,7 +72,7 @@ export class PageLayoutTabResolver {
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
isOverridden(@Parent() tab: PageLayoutTabDTO): boolean {
|
||||
return isDefined(tab.overrides) && Object.keys(tab.overrides).length > 0;
|
||||
return tab.isOverridden;
|
||||
}
|
||||
|
||||
@Query(() => [PageLayoutTabDTO])
|
||||
|
||||
+64
-5
@@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/flat-page-layout-tab/utils/from-update-page-layout-tab-input-to-flat-page-layout-tab-to-update-or-throw.util';
|
||||
import { reconstructFlatPageLayoutTabWithWidgets } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util';
|
||||
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
import { CreatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/create-page-layout-tab.input';
|
||||
import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layout-tab/dtos/inputs/update-page-layout-tab.input';
|
||||
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
|
||||
@@ -46,8 +49,19 @@ export class PageLayoutTabService {
|
||||
workspaceId: string;
|
||||
pageLayoutId: string;
|
||||
}): Promise<PageLayoutTabDTO[]> {
|
||||
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
|
||||
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
|
||||
const {
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} = await this.getPageLayoutTabFlatEntityMaps(workspaceId);
|
||||
|
||||
const cascadingContext = await this.buildCascadingIsOverriddenContext({
|
||||
workspaceId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
});
|
||||
|
||||
return Object.values(flatPageLayoutTabMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
@@ -61,6 +75,7 @@ export class PageLayoutTabService {
|
||||
tab,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
cascadingContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -72,8 +87,19 @@ export class PageLayoutTabService {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<PageLayoutTabDTO> {
|
||||
const { flatPageLayoutTabMaps, flatPageLayoutWidgetMaps } =
|
||||
await this.getPageLayoutTabFlatEntityMaps(workspaceId);
|
||||
const {
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} = await this.getPageLayoutTabFlatEntityMaps(workspaceId);
|
||||
|
||||
const cascadingContext = await this.buildCascadingIsOverriddenContext({
|
||||
workspaceId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
});
|
||||
|
||||
const flatTab = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
@@ -95,21 +121,54 @@ export class PageLayoutTabService {
|
||||
tab: flatTab,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
cascadingContext,
|
||||
);
|
||||
}
|
||||
|
||||
private async getPageLayoutTabFlatEntityMaps(workspaceId: string): Promise<{
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
flatViewFieldMaps: FlatViewFieldMaps;
|
||||
}> {
|
||||
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps', 'flatPageLayoutWidgetMaps'],
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewFieldMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async buildCascadingIsOverriddenContext({
|
||||
workspaceId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
flatViewFieldMaps: FlatViewFieldMaps;
|
||||
}): Promise<CascadingIsOverriddenContext> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
return {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
async create({
|
||||
createPageLayoutTabInput,
|
||||
workspaceId,
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
|
||||
const hasNonEmptyOverrides = (entity: { overrides: unknown }): boolean =>
|
||||
isDefined(entity.overrides) &&
|
||||
typeof entity.overrides === 'object' &&
|
||||
entity.overrides !== null &&
|
||||
Object.keys(entity.overrides).length > 0;
|
||||
|
||||
export const computeTabCascadingIsOverridden = ({
|
||||
tab,
|
||||
childWidgets,
|
||||
widgetCascadingIsOverriddenById,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
}: {
|
||||
tab: FlatPageLayoutTab;
|
||||
childWidgets: FlatPageLayoutWidget[];
|
||||
widgetCascadingIsOverriddenById: Record<string, boolean>;
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
}): boolean => {
|
||||
if (
|
||||
tab.applicationUniversalIdentifier ===
|
||||
workspaceCustomApplicationUniversalIdentifier
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasNonEmptyOverrides(tab)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const widget of childWidgets) {
|
||||
if (
|
||||
widget.applicationUniversalIdentifier ===
|
||||
workspaceCustomApplicationUniversalIdentifier
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!widget.isActive || hasNonEmptyOverrides(widget)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (widgetCascadingIsOverriddenById[widget.id] === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
+8
-1
@@ -5,19 +5,26 @@ import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-t
|
||||
|
||||
export const fromFlatPageLayoutTabToPageLayoutTabDto = (
|
||||
flatPageLayoutTab: FlatPageLayoutTab,
|
||||
isOverriddenOverride?: boolean,
|
||||
): Omit<PageLayoutTabDTO, 'widgets'> => {
|
||||
const {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
widgetIds: _widgetIds,
|
||||
overrides,
|
||||
...rest
|
||||
} = flatPageLayoutTab;
|
||||
|
||||
const resolvedOverrides = isDefined(overrides) ? overrides : {};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...resolvedOverrides,
|
||||
overrides,
|
||||
isOverridden:
|
||||
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
|
||||
isOverriddenOverride ??
|
||||
(isDefined(overrides) && Object.keys(overrides).length > 0),
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
deletedAt: deletedAt ? new Date(deletedAt) : null,
|
||||
|
||||
+45
-2
@@ -1,15 +1,58 @@
|
||||
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
|
||||
import { type FlatPageLayoutTabWithWidgets } from 'src/engine/metadata-modules/flat-page-layout-tab/utils/reconstruct-flat-page-layout-tab-with-widgets.util';
|
||||
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
|
||||
import { computeTabCascadingIsOverridden } from 'src/engine/metadata-modules/page-layout-tab/utils/compute-tab-cascading-is-overridden.util';
|
||||
import { fromFlatPageLayoutTabToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout-tab/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util';
|
||||
import { sortWidgetDtosByPosition } from 'src/engine/metadata-modules/page-layout-widget/utils/sort-widget-dtos-by-position.util';
|
||||
import { computeWidgetCascadingIsOverridden } from 'src/engine/metadata-modules/page-layout-widget/utils/compute-widget-cascading-is-overridden.util';
|
||||
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
|
||||
|
||||
export const fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto = (
|
||||
flatPageLayoutTabWithWidgets: FlatPageLayoutTabWithWidgets,
|
||||
cascadingContext?: CascadingIsOverriddenContext,
|
||||
): PageLayoutTabDTO => {
|
||||
const { widgets, ...flatPageLayoutTab } = flatPageLayoutTabWithWidgets;
|
||||
|
||||
if (!cascadingContext) {
|
||||
const widgetDtos = widgets.map((widget) =>
|
||||
fromFlatPageLayoutWidgetToPageLayoutWidgetDto(widget),
|
||||
);
|
||||
|
||||
return {
|
||||
...fromFlatPageLayoutTabToPageLayoutTabDto(flatPageLayoutTab),
|
||||
widgets: sortWidgetDtosByPosition(widgetDtos),
|
||||
};
|
||||
}
|
||||
|
||||
const widgetCascadingIsOverriddenById: Record<string, boolean> = {};
|
||||
|
||||
const widgetDtos = widgets.map((widget) => {
|
||||
const isOverridden = computeWidgetCascadingIsOverridden({
|
||||
widget,
|
||||
flatViewFieldGroupMaps: cascadingContext.flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps: cascadingContext.flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
cascadingContext.workspaceCustomApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
widgetCascadingIsOverriddenById[widget.id] = isOverridden;
|
||||
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(widget, isOverridden);
|
||||
});
|
||||
|
||||
const tabIsOverridden = computeTabCascadingIsOverridden({
|
||||
tab: flatPageLayoutTab,
|
||||
childWidgets: widgets,
|
||||
widgetCascadingIsOverriddenById,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
cascadingContext.workspaceCustomApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
return {
|
||||
...fromFlatPageLayoutTabToPageLayoutTabDto(flatPageLayoutTab),
|
||||
widgets: widgets.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto),
|
||||
...fromFlatPageLayoutTabToPageLayoutTabDto(
|
||||
flatPageLayoutTab,
|
||||
tabIsOverridden,
|
||||
),
|
||||
widgets: sortWidgetDtosByPosition(widgetDtos),
|
||||
};
|
||||
};
|
||||
|
||||
+1
-21
@@ -6,9 +6,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
|
||||
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -23,7 +21,6 @@ import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-wid
|
||||
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
|
||||
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
|
||||
import { resolveOverridableEntityProperty } from 'src/engine/metadata-modules/utils/resolve-overridable-entity-property.util';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
|
||||
@MetadataResolver(() => PageLayoutWidgetDTO)
|
||||
@@ -98,21 +95,6 @@ export class PageLayoutWidgetResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField(() => String)
|
||||
title(@Parent() widget: PageLayoutWidgetDTO): string {
|
||||
return resolveOverridableEntityProperty(widget, 'title');
|
||||
}
|
||||
|
||||
@ResolveField(() => GraphQLJSON, { nullable: true })
|
||||
position(@Parent() widget: PageLayoutWidgetDTO) {
|
||||
return resolveOverridableEntityProperty(widget, 'position');
|
||||
}
|
||||
|
||||
@ResolveField(() => GraphQLJSON, { nullable: true })
|
||||
conditionalDisplay(@Parent() widget: PageLayoutWidgetDTO) {
|
||||
return resolveOverridableEntityProperty(widget, 'conditionalDisplay');
|
||||
}
|
||||
|
||||
@ResolveField(() => WidgetConfiguration, { nullable: true })
|
||||
configuration(@Parent() widget: PageLayoutWidgetDTO) {
|
||||
return widget.configuration;
|
||||
@@ -120,8 +102,6 @@ export class PageLayoutWidgetResolver {
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
isOverridden(@Parent() widget: PageLayoutWidgetDTO): boolean {
|
||||
return (
|
||||
isDefined(widget.overrides) && Object.keys(widget.overrides).length > 0
|
||||
);
|
||||
return widget.isOverridden;
|
||||
}
|
||||
}
|
||||
|
||||
+64
-6
@@ -8,6 +8,8 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
import { FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { fromCreatePageLayoutWidgetInputToFlatPageLayoutWidgetToCreate } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-create-page-layout-widget-input-to-flat-page-layout-widget-to-create.util';
|
||||
import { fromDestroyPageLayoutWidgetInputToFlatPageLayoutWidgetOrThrow } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-destroy-page-layout-widget-input-to-flat-page-layout-widget-or-throw.util';
|
||||
@@ -26,6 +28,7 @@ import {
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { type AllPageLayoutWidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/types/all-page-layout-widget-configuration.type';
|
||||
import { computeWidgetCascadingIsOverridden } from 'src/engine/metadata-modules/page-layout-widget/utils/compute-widget-cascading-is-overridden.util';
|
||||
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
|
||||
import { validateChartConfigurationFieldReferencesOrThrow } from 'src/engine/metadata-modules/page-layout-widget/utils/validate-chart-configuration-field-references.util';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
@@ -61,6 +64,25 @@ export class PageLayoutWidgetService {
|
||||
return flatPageLayoutWidgetMaps;
|
||||
}
|
||||
|
||||
private async getWidgetAndViewFieldFlatEntityMaps(
|
||||
workspaceId: string,
|
||||
): Promise<{
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
flatViewFieldMaps: FlatViewFieldMaps;
|
||||
}> {
|
||||
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewFieldMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async validateAndRunWidgetMigration({
|
||||
workspaceId,
|
||||
operations,
|
||||
@@ -155,8 +177,16 @@ export class PageLayoutWidgetService {
|
||||
workspaceId: string;
|
||||
pageLayoutTabId: string;
|
||||
}): Promise<PageLayoutWidgetDTO[]> {
|
||||
const flatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
const {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} = await this.getWidgetAndViewFieldFlatEntityMaps(workspaceId);
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
return Object.values(flatPageLayoutWidgetMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
@@ -170,7 +200,18 @@ export class PageLayoutWidgetService {
|
||||
new Date(widgetA.createdAt).getTime() -
|
||||
new Date(widgetB.createdAt).getTime(),
|
||||
)
|
||||
.map(fromFlatPageLayoutWidgetToPageLayoutWidgetDto);
|
||||
.map((widget) =>
|
||||
fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
|
||||
widget,
|
||||
computeWidgetCascadingIsOverridden({
|
||||
widget,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async findByIdOrThrow({
|
||||
@@ -180,8 +221,16 @@ export class PageLayoutWidgetService {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<PageLayoutWidgetDTO> {
|
||||
const flatPageLayoutWidgetMaps =
|
||||
await this.getFlatPageLayoutWidgetMaps(workspaceId);
|
||||
const {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} = await this.getWidgetAndViewFieldFlatEntityMaps(workspaceId);
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const flatWidget = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
@@ -198,7 +247,16 @@ export class PageLayoutWidgetService {
|
||||
);
|
||||
}
|
||||
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(flatWidget);
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(
|
||||
flatWidget,
|
||||
computeWidgetCascadingIsOverridden({
|
||||
widget: flatWidget,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async create({
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
|
||||
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
|
||||
const hasNonEmptyOverrides = (entity: { overrides: unknown }): boolean =>
|
||||
isDefined(entity.overrides) &&
|
||||
typeof entity.overrides === 'object' &&
|
||||
entity.overrides !== null &&
|
||||
Object.keys(entity.overrides).length > 0;
|
||||
|
||||
export const computeWidgetCascadingIsOverridden = ({
|
||||
widget,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
}: {
|
||||
widget: FlatPageLayoutWidget;
|
||||
flatViewFieldGroupMaps: {
|
||||
byUniversalIdentifier: Partial<
|
||||
Record<string, FlatViewFieldGroup | undefined>
|
||||
>;
|
||||
};
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: Partial<Record<string, FlatViewField | undefined>>;
|
||||
};
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
}): boolean => {
|
||||
if (
|
||||
widget.applicationUniversalIdentifier ===
|
||||
workspaceCustomApplicationUniversalIdentifier
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasNonEmptyOverrides(widget)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!isFlatPageLayoutWidgetConfigurationOfType(
|
||||
widget,
|
||||
WidgetConfigurationType.FIELDS,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const viewId = widget.configuration.viewId;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const viewFieldGroups = Object.values(
|
||||
flatViewFieldGroupMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((group) => group.viewId === viewId && !isDefined(group.deletedAt));
|
||||
|
||||
for (const group of viewFieldGroups) {
|
||||
if (
|
||||
group.applicationUniversalIdentifier ===
|
||||
workspaceCustomApplicationUniversalIdentifier
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!group.isActive || hasNonEmptyOverrides(group)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const viewFields = Object.values(flatViewFieldMaps.byUniversalIdentifier)
|
||||
.filter(isDefined)
|
||||
.filter((field) => field.viewId === viewId && !isDefined(field.deletedAt));
|
||||
|
||||
for (const field of viewFields) {
|
||||
if (
|
||||
field.applicationUniversalIdentifier ===
|
||||
workspaceCustomApplicationUniversalIdentifier
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!field.isActive || hasNonEmptyOverrides(field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
+15
-3
@@ -5,14 +5,26 @@ import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layou
|
||||
|
||||
export const fromFlatPageLayoutWidgetToPageLayoutWidgetDto = (
|
||||
flatPageLayoutWidget: FlatPageLayoutWidget,
|
||||
isOverriddenOverride?: boolean,
|
||||
): PageLayoutWidgetDTO => {
|
||||
const { createdAt, updatedAt, deletedAt, objectMetadataId, ...rest } =
|
||||
flatPageLayoutWidget;
|
||||
const {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
deletedAt,
|
||||
objectMetadataId,
|
||||
overrides,
|
||||
...rest
|
||||
} = flatPageLayoutWidget;
|
||||
|
||||
const resolvedOverrides = isDefined(overrides) ? overrides : {};
|
||||
|
||||
return {
|
||||
...rest,
|
||||
...resolvedOverrides,
|
||||
overrides,
|
||||
isOverridden:
|
||||
isDefined(rest.overrides) && Object.keys(rest.overrides).length > 0,
|
||||
isOverriddenOverride ??
|
||||
(isDefined(overrides) && Object.keys(overrides).length > 0),
|
||||
objectMetadataId: objectMetadataId ?? undefined,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
|
||||
|
||||
export const sortWidgetDtosByPosition = (
|
||||
widgets: PageLayoutWidgetDTO[],
|
||||
): PageLayoutWidgetDTO[] => {
|
||||
return [...widgets].sort((a, b) => {
|
||||
const posA = a.position;
|
||||
const posB = b.position;
|
||||
|
||||
if (!posA || !posB) return 0;
|
||||
|
||||
if ('index' in posA && 'index' in posB) {
|
||||
return posA.index - posB.index;
|
||||
}
|
||||
|
||||
if ('row' in posA && 'row' in posB) {
|
||||
if (posA.row !== posB.row) return posA.row - posB.row;
|
||||
|
||||
if ('column' in posA && 'column' in posB) {
|
||||
return posA.column - posB.column;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
};
|
||||
+14
-1
@@ -8,13 +8,14 @@ import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
|
||||
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
|
||||
import { CreatePageLayoutInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/create-page-layout.input';
|
||||
import { UpdatePageLayoutWithTabsInput } from 'src/engine/metadata-modules/page-layout/dtos/inputs/update-page-layout-with-tabs.input';
|
||||
@@ -136,4 +137,16 @@ export class PageLayoutResolver {
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => PageLayoutTabDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.LAYOUTS))
|
||||
async resetPageLayoutTabToDefault(
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
|
||||
return this.pageLayoutResetService.resetPageLayoutTabToDefault({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+316
-2
@@ -4,13 +4,23 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { splitEntitiesByResetStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-reset-strategy.util';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { isFlatPageLayoutWidgetConfigurationOfType } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/is-flat-page-layout-widget-configuration-of-type.util';
|
||||
import { type FlatViewFieldGroup } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group.type';
|
||||
import { type FlatViewField } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field.type';
|
||||
import { type PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
|
||||
import {
|
||||
PageLayoutTabException,
|
||||
PageLayoutTabExceptionCode,
|
||||
PageLayoutTabExceptionMessageKey,
|
||||
generatePageLayoutTabExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-tab/exceptions/page-layout-tab.exception';
|
||||
import { fromFlatPageLayoutTabToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout-tab/utils/from-flat-page-layout-tab-to-page-layout-tab-dto.util';
|
||||
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
|
||||
import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type';
|
||||
import {
|
||||
PageLayoutWidgetException,
|
||||
@@ -18,8 +28,8 @@ import {
|
||||
PageLayoutWidgetExceptionMessageKey,
|
||||
generatePageLayoutWidgetExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout-widget/exceptions/page-layout-widget.exception';
|
||||
import { type PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
|
||||
import { fromFlatPageLayoutWidgetToPageLayoutWidgetDto } from 'src/engine/metadata-modules/page-layout-widget/utils/from-flat-page-layout-widget-to-page-layout-widget-dto.util';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
|
||||
@@ -33,6 +43,7 @@ export class PageLayoutResetService {
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly dashboardSyncService: DashboardSyncService,
|
||||
private readonly viewService: ViewService,
|
||||
) {}
|
||||
|
||||
async resetPageLayoutWidgetToDefault({
|
||||
@@ -187,6 +198,309 @@ export class PageLayoutResetService {
|
||||
return fromFlatPageLayoutWidgetToPageLayoutWidgetDto(updatedWidget);
|
||||
}
|
||||
|
||||
async resetPageLayoutTabToDefault({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<Omit<PageLayoutTabDTO, 'widgets'>> {
|
||||
const {
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewFieldMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const tab = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(tab) || isDefined(tab.deletedAt)) {
|
||||
throw new PageLayoutTabException(
|
||||
generatePageLayoutTabExceptionMessage(
|
||||
PageLayoutTabExceptionMessageKey.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
PageLayoutTabExceptionCode.PAGE_LAYOUT_TAB_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
if (
|
||||
tab.applicationUniversalIdentifier ===
|
||||
workspaceCustomFlatApplication.universalIdentifier
|
||||
) {
|
||||
throw new PageLayoutTabException(
|
||||
`Custom tab "${id}" cannot be reset to default`,
|
||||
PageLayoutTabExceptionCode.INVALID_PAGE_LAYOUT_TAB_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const tabToUpdate: FlatPageLayoutTab = {
|
||||
...tab,
|
||||
overrides: null,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
const {
|
||||
widgetsToUpdate,
|
||||
widgetsToDelete,
|
||||
viewFieldGroupsToUpdate,
|
||||
viewFieldGroupsToDelete,
|
||||
viewFieldsToUpdate,
|
||||
viewFieldsToDelete,
|
||||
orphanedViewIds,
|
||||
} = this.computeTabChildResetOperations({
|
||||
tabId: id,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
const validateAndBuildResult =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
pageLayoutTab: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: [tabToUpdate],
|
||||
flatEntityToDelete: [],
|
||||
},
|
||||
pageLayoutWidget: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: widgetsToUpdate,
|
||||
flatEntityToDelete: widgetsToDelete,
|
||||
},
|
||||
viewFieldGroup: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: viewFieldGroupsToUpdate,
|
||||
flatEntityToDelete: viewFieldGroupsToDelete,
|
||||
},
|
||||
viewField: {
|
||||
flatEntityToCreate: [],
|
||||
flatEntityToUpdate: viewFieldsToUpdate,
|
||||
flatEntityToDelete: viewFieldsToDelete,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
applicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
if (validateAndBuildResult.status === 'fail') {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
validateAndBuildResult,
|
||||
'Multiple validation errors occurred while resetting page layout tab to default',
|
||||
);
|
||||
}
|
||||
|
||||
await this.destroyOrphanedFieldsWidgetViews({
|
||||
viewIds: orphanedViewIds,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const { flatPageLayoutTabMaps: recomputedTabMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatPageLayoutTabMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const updatedTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: recomputedTabMaps,
|
||||
});
|
||||
|
||||
await this.dashboardSyncService.updateLinkedDashboardsUpdatedAtByTabId({
|
||||
tabId: id,
|
||||
workspaceId,
|
||||
updatedAt: new Date(updatedTab.updatedAt),
|
||||
});
|
||||
|
||||
return fromFlatPageLayoutTabToPageLayoutTabDto(updatedTab);
|
||||
}
|
||||
|
||||
private computeTabChildResetOperations({
|
||||
tabId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
now,
|
||||
}: {
|
||||
tabId: string;
|
||||
flatPageLayoutWidgetMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatPageLayoutWidget | undefined>;
|
||||
};
|
||||
flatViewFieldGroupMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatViewFieldGroup | undefined>;
|
||||
};
|
||||
flatViewFieldMaps: {
|
||||
byUniversalIdentifier: Record<string, FlatViewField | undefined>;
|
||||
};
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
now: string;
|
||||
}): {
|
||||
widgetsToUpdate: FlatPageLayoutWidget[];
|
||||
widgetsToDelete: FlatPageLayoutWidget[];
|
||||
viewFieldGroupsToUpdate: FlatViewFieldGroup[];
|
||||
viewFieldGroupsToDelete: FlatViewFieldGroup[];
|
||||
viewFieldsToUpdate: FlatViewField[];
|
||||
viewFieldsToDelete: FlatViewField[];
|
||||
orphanedViewIds: string[];
|
||||
} {
|
||||
const existingWidgets = Object.values(
|
||||
flatPageLayoutWidgetMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter(
|
||||
(widget) =>
|
||||
widget.pageLayoutTabId === tabId && !isDefined(widget.deletedAt),
|
||||
);
|
||||
|
||||
const { toHardDelete: widgetsToDelete, toReset: widgetsToReset } =
|
||||
splitEntitiesByResetStrategy({
|
||||
entities: existingWidgets,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
let allViewFieldGroupsToUpdate: FlatViewFieldGroup[] = [];
|
||||
let allViewFieldGroupsToDelete: FlatViewFieldGroup[] = [];
|
||||
let allViewFieldsToUpdate: FlatViewField[] = [];
|
||||
let allViewFieldsToDelete: FlatViewField[] = [];
|
||||
|
||||
for (const widget of widgetsToReset) {
|
||||
if (
|
||||
!isFlatPageLayoutWidgetConfigurationOfType(
|
||||
widget,
|
||||
WidgetConfigurationType.FIELDS,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewId = widget.configuration.viewId;
|
||||
|
||||
if (!isDefined(viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const {
|
||||
viewFieldGroupsToUpdate,
|
||||
viewFieldGroupsToDelete,
|
||||
viewFieldsToUpdate,
|
||||
viewFieldsToDelete,
|
||||
} = this.computeFieldsWidgetChildResetOperations({
|
||||
viewId,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
now,
|
||||
});
|
||||
|
||||
allViewFieldGroupsToUpdate = [
|
||||
...allViewFieldGroupsToUpdate,
|
||||
...viewFieldGroupsToUpdate,
|
||||
];
|
||||
allViewFieldGroupsToDelete = [
|
||||
...allViewFieldGroupsToDelete,
|
||||
...viewFieldGroupsToDelete,
|
||||
];
|
||||
allViewFieldsToUpdate = [...allViewFieldsToUpdate, ...viewFieldsToUpdate];
|
||||
allViewFieldsToDelete = [...allViewFieldsToDelete, ...viewFieldsToDelete];
|
||||
}
|
||||
|
||||
const orphanedViewIds =
|
||||
this.collectOrphanedViewIdsFromDeletedWidgets(widgetsToDelete);
|
||||
|
||||
return {
|
||||
widgetsToUpdate: widgetsToReset,
|
||||
widgetsToDelete,
|
||||
viewFieldGroupsToUpdate: allViewFieldGroupsToUpdate,
|
||||
viewFieldGroupsToDelete: allViewFieldGroupsToDelete,
|
||||
viewFieldsToUpdate: allViewFieldsToUpdate,
|
||||
viewFieldsToDelete: allViewFieldsToDelete,
|
||||
orphanedViewIds,
|
||||
};
|
||||
}
|
||||
|
||||
private collectOrphanedViewIdsFromDeletedWidgets(
|
||||
widgets: FlatPageLayoutWidget[],
|
||||
): string[] {
|
||||
const viewIds: string[] = [];
|
||||
|
||||
for (const widget of widgets) {
|
||||
const viewId = this.getViewIdFromFieldsWidget(widget);
|
||||
|
||||
if (isDefined(viewId)) {
|
||||
viewIds.push(viewId);
|
||||
}
|
||||
}
|
||||
|
||||
return viewIds;
|
||||
}
|
||||
|
||||
private getViewIdFromFieldsWidget(
|
||||
widget: FlatPageLayoutWidget,
|
||||
): string | undefined {
|
||||
if (
|
||||
widget.configuration.configurationType !== WidgetConfigurationType.FIELDS
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const viewId = (widget.configuration as { viewId?: string | null }).viewId;
|
||||
|
||||
return typeof viewId === 'string' ? viewId : undefined;
|
||||
}
|
||||
|
||||
private async destroyOrphanedFieldsWidgetViews({
|
||||
viewIds,
|
||||
workspaceId,
|
||||
}: {
|
||||
viewIds: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
for (const viewId of viewIds) {
|
||||
try {
|
||||
await this.viewService.destroyOne({
|
||||
destroyViewInput: { id: viewId },
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to destroy orphaned view ${viewId} during tab reset: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private computeFieldsWidgetChildResetOperations({
|
||||
viewId,
|
||||
flatViewFieldGroupMaps,
|
||||
|
||||
+147
-96
@@ -10,13 +10,13 @@ import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types
|
||||
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { splitEntitiesByRemovalStrategy } from 'src/engine/metadata-modules/flat-entity/utils/split-entities-by-removal-strategy.util';
|
||||
import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant';
|
||||
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
import { type FlatPageLayoutTab } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab.type';
|
||||
import { FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-widget/constants/flat-page-layout-widget-editable-properties.constant';
|
||||
import { type FlatPageLayoutWidget } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget.type';
|
||||
import { buildFlatPageLayoutWidgetCommonProperties } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/build-flat-page-layout-widget-common-properties.util';
|
||||
import { fromPageLayoutWidgetConfigurationToUniversalConfiguration } from 'src/engine/metadata-modules/flat-page-layout-widget/utils/from-page-layout-widget-configuration-to-universal-configuration.util';
|
||||
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';
|
||||
@@ -33,9 +33,12 @@ import {
|
||||
generatePageLayoutExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/page-layout/exceptions/page-layout.exception';
|
||||
import { fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto } from 'src/engine/metadata-modules/page-layout/utils/from-flat-page-layout-with-tabs-and-widgets-to-page-layout-dto.util';
|
||||
import { isCallerOverridingEntity } from 'src/engine/metadata-modules/utils/is-caller-overriding-entity.util';
|
||||
import { sanitizeOverridableEntityInput } from 'src/engine/metadata-modules/utils/sanitize-overridable-entity-input.util';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
|
||||
import { DashboardSyncService } from 'src/modules/dashboard-sync/services/dashboard-sync.service';
|
||||
|
||||
type UpdatePageLayoutWithTabsParams = {
|
||||
@@ -322,39 +325,61 @@ export class PageLayoutUpdateService {
|
||||
},
|
||||
);
|
||||
|
||||
const tabsToUpdate: FlatPageLayoutTab[] = entitiesToUpdate.map(
|
||||
(tabInput) => {
|
||||
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: tabInput.id,
|
||||
flatEntityMaps: flatPageLayoutTabMaps,
|
||||
const buildTabUpdateWithOverrides = (
|
||||
tabInput: UpdatePageLayoutTabWithWidgetsInput,
|
||||
additionalProps?: Partial<FlatPageLayoutTab>,
|
||||
): FlatPageLayoutTab => {
|
||||
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: tabInput.id,
|
||||
flatEntityMaps: flatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
const editableProperties: Record<string, unknown> = {
|
||||
title: tabInput.title,
|
||||
position: tabInput.position,
|
||||
...(tabInput.icon !== undefined ? { icon: tabInput.icon } : {}),
|
||||
...(tabInput.layoutMode !== undefined
|
||||
? { layoutMode: tabInput.layoutMode }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const shouldOverride = isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier:
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
entityApplicationUniversalIdentifier:
|
||||
existingTab.applicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const { overrides, updatedEditableProperties } =
|
||||
sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutTab',
|
||||
existingFlatEntity: existingTab,
|
||||
updatedEditableProperties: editableProperties,
|
||||
shouldOverride,
|
||||
});
|
||||
|
||||
return {
|
||||
...existingTab,
|
||||
title: tabInput.title,
|
||||
position: tabInput.position,
|
||||
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
},
|
||||
);
|
||||
return {
|
||||
...mergeUpdateInExistingRecord({
|
||||
existing: existingTab,
|
||||
properties: [...FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES],
|
||||
update: updatedEditableProperties,
|
||||
}),
|
||||
overrides,
|
||||
updatedAt: now.toISOString(),
|
||||
...additionalProps,
|
||||
} as FlatPageLayoutTab;
|
||||
};
|
||||
|
||||
const tabsToUpdate: FlatPageLayoutTab[] =
|
||||
entitiesToUpdate.map((tabInput) =>
|
||||
buildTabUpdateWithOverrides(tabInput),
|
||||
);
|
||||
|
||||
const tabsToRestoreAndUpdate: FlatPageLayoutTab[] =
|
||||
entitiesToRestoreAndUpdate.map((tabInput) => {
|
||||
const existingTab = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: tabInput.id,
|
||||
flatEntityMaps: flatPageLayoutTabMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
...existingTab,
|
||||
title: tabInput.title,
|
||||
position: tabInput.position,
|
||||
layoutMode: tabInput.layoutMode ?? existingTab.layoutMode,
|
||||
isActive: true,
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
});
|
||||
entitiesToRestoreAndUpdate.map((tabInput) =>
|
||||
buildTabUpdateWithOverrides(tabInput, { isActive: true }),
|
||||
);
|
||||
|
||||
const tabsToRemove = idsToRemove
|
||||
.map((tabId) =>
|
||||
@@ -548,77 +573,103 @@ export class PageLayoutUpdateService {
|
||||
},
|
||||
);
|
||||
|
||||
const widgetsToUpdate: FlatPageLayoutWidget[] = entitiesToUpdate.map(
|
||||
(widgetInput) => {
|
||||
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: widgetInput.id,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
const buildWidgetUpdateWithOverrides = (
|
||||
widgetInput: UpdatePageLayoutWidgetWithIdInput,
|
||||
additionalProps?: Partial<FlatPageLayoutWidget>,
|
||||
): FlatPageLayoutWidget => {
|
||||
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: widgetInput.id,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
const editableProperties: Record<string, unknown> = {
|
||||
title: widgetInput.title,
|
||||
type: widgetInput.type,
|
||||
objectMetadataId: widgetInput.objectMetadataId,
|
||||
gridPosition: widgetInput.gridPosition,
|
||||
position: widgetInput.position,
|
||||
...(widgetInput.configuration !== undefined
|
||||
? { configuration: widgetInput.configuration }
|
||||
: {}),
|
||||
...(widgetInput.conditionalDisplay !== undefined
|
||||
? { conditionalDisplay: widgetInput.conditionalDisplay }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const shouldOverride = isCallerOverridingEntity({
|
||||
callerApplicationUniversalIdentifier:
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
entityApplicationUniversalIdentifier:
|
||||
existingWidget.applicationUniversalIdentifier,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const { overrides, updatedEditableProperties } =
|
||||
sanitizeOverridableEntityInput({
|
||||
metadataName: 'pageLayoutWidget',
|
||||
existingFlatEntity: existingWidget,
|
||||
updatedEditableProperties: editableProperties,
|
||||
shouldOverride,
|
||||
});
|
||||
|
||||
const updatedConfiguration = widgetInput.configuration ?? null;
|
||||
const {
|
||||
pageLayoutTabUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
} = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'pageLayoutWidget',
|
||||
foreignKeyValues: {
|
||||
pageLayoutTabId: widgetInput.pageLayoutTabId,
|
||||
objectMetadataId: widgetInput.objectMetadataId,
|
||||
},
|
||||
flatEntityMaps: { flatPageLayoutTabMaps, flatObjectMetadataMaps },
|
||||
});
|
||||
|
||||
return {
|
||||
...existingWidget,
|
||||
...buildFlatPageLayoutWidgetCommonProperties({
|
||||
widgetInput,
|
||||
flatPageLayoutTabMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}),
|
||||
configuration: updatedConfiguration,
|
||||
updatedAt: now.toISOString(),
|
||||
...(isDefined(updatedConfiguration) && {
|
||||
universalConfiguration:
|
||||
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
|
||||
configuration: updatedConfiguration,
|
||||
fieldMetadataUniversalIdentifierById:
|
||||
flatFieldMetadataMaps.universalIdentifierById,
|
||||
frontComponentUniversalIdentifierById:
|
||||
flatFrontComponentMaps.universalIdentifierById,
|
||||
viewFieldGroupUniversalIdentifierById:
|
||||
flatViewFieldGroupMaps.universalIdentifierById,
|
||||
viewUniversalIdentifierById:
|
||||
flatViewMaps.universalIdentifierById,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
const mergedWidget = {
|
||||
...mergeUpdateInExistingRecord({
|
||||
existing: existingWidget,
|
||||
properties: [
|
||||
...FLAT_PAGE_LAYOUT_WIDGET_EDITABLE_PROPERTIES,
|
||||
],
|
||||
update: updatedEditableProperties,
|
||||
}),
|
||||
overrides,
|
||||
pageLayoutTabId: widgetInput.pageLayoutTabId,
|
||||
pageLayoutTabUniversalIdentifier,
|
||||
objectMetadataUniversalIdentifier,
|
||||
updatedAt: now.toISOString(),
|
||||
...additionalProps,
|
||||
} as FlatPageLayoutWidget;
|
||||
|
||||
const resolvedConfiguration =
|
||||
mergedWidget.configuration ?? existingWidget.configuration;
|
||||
|
||||
if (isDefined(resolvedConfiguration)) {
|
||||
mergedWidget.universalConfiguration =
|
||||
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
|
||||
configuration: resolvedConfiguration,
|
||||
fieldMetadataUniversalIdentifierById:
|
||||
flatFieldMetadataMaps.universalIdentifierById,
|
||||
frontComponentUniversalIdentifierById:
|
||||
flatFrontComponentMaps.universalIdentifierById,
|
||||
viewFieldGroupUniversalIdentifierById:
|
||||
flatViewFieldGroupMaps.universalIdentifierById,
|
||||
viewUniversalIdentifierById:
|
||||
flatViewMaps.universalIdentifierById,
|
||||
});
|
||||
}
|
||||
|
||||
return mergedWidget;
|
||||
};
|
||||
|
||||
const widgetsToUpdate: FlatPageLayoutWidget[] =
|
||||
entitiesToUpdate.map((widgetInput) =>
|
||||
buildWidgetUpdateWithOverrides(widgetInput),
|
||||
);
|
||||
|
||||
const widgetsToRestoreAndUpdate: FlatPageLayoutWidget[] =
|
||||
entitiesToRestoreAndUpdate.map((widgetInput) => {
|
||||
const existingWidget = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: widgetInput.id,
|
||||
flatEntityMaps: flatPageLayoutWidgetMaps,
|
||||
});
|
||||
|
||||
const restoredConfiguration = widgetInput.configuration ?? null;
|
||||
|
||||
return {
|
||||
...existingWidget,
|
||||
...buildFlatPageLayoutWidgetCommonProperties({
|
||||
widgetInput,
|
||||
flatPageLayoutTabMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}),
|
||||
configuration: restoredConfiguration,
|
||||
isActive: true,
|
||||
updatedAt: now.toISOString(),
|
||||
...(isDefined(restoredConfiguration) && {
|
||||
universalConfiguration:
|
||||
fromPageLayoutWidgetConfigurationToUniversalConfiguration({
|
||||
configuration: restoredConfiguration,
|
||||
fieldMetadataUniversalIdentifierById:
|
||||
flatFieldMetadataMaps.universalIdentifierById,
|
||||
frontComponentUniversalIdentifierById:
|
||||
flatFrontComponentMaps.universalIdentifierById,
|
||||
viewFieldGroupUniversalIdentifierById:
|
||||
flatViewFieldGroupMaps.universalIdentifierById,
|
||||
viewUniversalIdentifierById:
|
||||
flatViewMaps.universalIdentifierById,
|
||||
}),
|
||||
}),
|
||||
};
|
||||
});
|
||||
entitiesToRestoreAndUpdate.map((widgetInput) =>
|
||||
buildWidgetUpdateWithOverrides(widgetInput, { isActive: true }),
|
||||
);
|
||||
|
||||
const widgetsToRemove = idsToRemove
|
||||
.map((widgetId) =>
|
||||
|
||||
+62
@@ -5,11 +5,14 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
import { type FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
import { type FlatPageLayoutMaps } from 'src/engine/metadata-modules/flat-page-layout/types/flat-page-layout-maps.type';
|
||||
import { type FlatViewFieldGroupMaps } from 'src/engine/metadata-modules/flat-view-field-group/types/flat-view-field-group-maps.type';
|
||||
import { type FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type';
|
||||
import { fromCreatePageLayoutInputToFlatPageLayoutToCreate } from 'src/engine/metadata-modules/flat-page-layout/utils/from-create-page-layout-input-to-flat-page-layout-to-create.util';
|
||||
import { fromDestroyPageLayoutInputToFlatPageLayoutOrThrow } from 'src/engine/metadata-modules/flat-page-layout/utils/from-destroy-page-layout-input-to-flat-page-layout-or-throw.util';
|
||||
import {
|
||||
@@ -50,8 +53,17 @@ export class PageLayoutService {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
|
||||
|
||||
const cascadingContext = await this.buildCascadingIsOverriddenContext({
|
||||
workspaceId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
});
|
||||
|
||||
const activeLayouts = Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)
|
||||
@@ -65,6 +77,7 @@ export class PageLayoutService {
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
cascadingContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -83,8 +96,17 @@ export class PageLayoutService {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
|
||||
|
||||
const cascadingContext = await this.buildCascadingIsOverriddenContext({
|
||||
workspaceId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
});
|
||||
|
||||
const activeLayouts = Object.values(
|
||||
flatPageLayoutMaps.byUniversalIdentifier,
|
||||
)
|
||||
@@ -108,6 +130,7 @@ export class PageLayoutService {
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
cascadingContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -123,8 +146,17 @@ export class PageLayoutService {
|
||||
flatPageLayoutMaps,
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
} = await this.getPageLayoutFlatEntityMaps(workspaceId);
|
||||
|
||||
const cascadingContext = await this.buildCascadingIsOverriddenContext({
|
||||
workspaceId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
});
|
||||
|
||||
const flatLayout = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: id,
|
||||
flatEntityMaps: flatPageLayoutMaps,
|
||||
@@ -149,6 +181,7 @@ export class PageLayoutService {
|
||||
flatPageLayoutTabMaps,
|
||||
flatPageLayoutWidgetMaps,
|
||||
}),
|
||||
cascadingContext,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -156,6 +189,8 @@ export class PageLayoutService {
|
||||
flatPageLayoutMaps: FlatPageLayoutMaps;
|
||||
flatPageLayoutTabMaps: FlatPageLayoutTabMaps;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
flatViewFieldMaps: FlatViewFieldMaps;
|
||||
}> {
|
||||
return this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
@@ -164,11 +199,38 @@ export class PageLayoutService {
|
||||
'flatPageLayoutMaps',
|
||||
'flatPageLayoutTabMaps',
|
||||
'flatPageLayoutWidgetMaps',
|
||||
'flatViewFieldGroupMaps',
|
||||
'flatViewFieldMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async buildCascadingIsOverriddenContext({
|
||||
workspaceId,
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
flatPageLayoutWidgetMaps: FlatPageLayoutWidgetMaps;
|
||||
flatViewFieldGroupMaps: FlatViewFieldGroupMaps;
|
||||
flatViewFieldMaps: FlatViewFieldMaps;
|
||||
}): Promise<CascadingIsOverriddenContext> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
return {
|
||||
flatPageLayoutWidgetMaps,
|
||||
flatViewFieldGroupMaps,
|
||||
flatViewFieldMaps,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
workspaceCustomFlatApplication.universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
async create({
|
||||
createPageLayoutInput,
|
||||
workspaceId,
|
||||
|
||||
+5
-1
@@ -1,3 +1,4 @@
|
||||
import { type CascadingIsOverriddenContext } from 'src/engine/metadata-modules/flat-entity/types/cascading-is-overridden-context.type';
|
||||
import { type FlatPageLayoutWithTabsAndWidgets } from 'src/engine/metadata-modules/flat-page-layout/utils/reconstruct-flat-page-layout-with-tabs-and-widgets.util';
|
||||
import { type PageLayoutDTO } from 'src/engine/metadata-modules/page-layout/dtos/page-layout.dto';
|
||||
import { fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto } from 'src/engine/metadata-modules/page-layout-tab/utils/from-flat-page-layout-tab-with-widgets-to-page-layout-tab-dto.util';
|
||||
@@ -5,11 +6,14 @@ import { fromFlatPageLayoutToPageLayoutDto } from 'src/engine/metadata-modules/p
|
||||
|
||||
export const fromFlatPageLayoutWithTabsAndWidgetsToPageLayoutDto = (
|
||||
flatPageLayoutWithTabsAndWidgets: FlatPageLayoutWithTabsAndWidgets,
|
||||
cascadingContext?: CascadingIsOverriddenContext,
|
||||
): PageLayoutDTO => {
|
||||
const { tabs, ...flatPageLayout } = flatPageLayoutWithTabsAndWidgets;
|
||||
|
||||
return {
|
||||
...fromFlatPageLayoutToPageLayoutDto(flatPageLayout),
|
||||
tabs: tabs.map(fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto),
|
||||
tabs: tabs.map((tab) =>
|
||||
fromFlatPageLayoutTabWithWidgetsToPageLayoutTabDto(tab, cascadingContext),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
+13
@@ -1,4 +1,5 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant';
|
||||
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
|
||||
@@ -21,6 +22,18 @@ export const flatEntityToScalarFlatEntity = <T extends AllMetadataName>({
|
||||
result[key] = flatEntityRecord[key];
|
||||
}
|
||||
|
||||
const overrides = flatEntityRecord.overrides;
|
||||
|
||||
if (isDefined(overrides) && typeof overrides === 'object') {
|
||||
for (const [key, value] of Object.entries(
|
||||
overrides as Record<string, unknown>,
|
||||
)) {
|
||||
if (key in config) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.id = flatEntityRecord.id;
|
||||
result.workspaceId = flatEntityRecord.workspaceId;
|
||||
result.applicationId = flatEntityRecord.applicationId;
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { LazyEmbed } from '@/design-system/components';
|
||||
import type { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledIllustrationContainer = styled.div`
|
||||
justify-self: stretch;
|
||||
margin-top: ${theme.spacing(6)};
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledIllustrationEmbed = styled(LazyEmbed)`
|
||||
border: none;
|
||||
display: block;
|
||||
height: 462px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type IllustrationProps = {
|
||||
illustration: IllustrationType;
|
||||
backgroundColor: string;
|
||||
};
|
||||
|
||||
export function Illustration({
|
||||
illustration,
|
||||
backgroundColor,
|
||||
}: IllustrationProps) {
|
||||
return (
|
||||
<StyledIllustrationContainer>
|
||||
<StyledIllustrationEmbed
|
||||
eager
|
||||
src={illustration.src}
|
||||
title={illustration.title}
|
||||
unloadWhenHidden={false}
|
||||
allow="clipboard-write; encrypted-media; gyroscope; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
style={{
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
/>
|
||||
</StyledIllustrationContainer>
|
||||
);
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
'use client';
|
||||
|
||||
import { Center, useGLTF, Float, PresentationControls } from '@react-three/drei';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { Suspense, useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
||||
import * as THREE from 'three';
|
||||
|
||||
function Model({ url, color }: { url: string; color: string }) {
|
||||
const { scene } = useGLTF(url);
|
||||
const groupRef = useRef<THREE.Group>(null);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const material = useMemo(
|
||||
() =>
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: new THREE.Color(color),
|
||||
emissive: new THREE.Color(color),
|
||||
emissiveIntensity: 0.15,
|
||||
roughness: 0.2,
|
||||
metalness: 0.8,
|
||||
wireframe: true,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
}),
|
||||
[color],
|
||||
);
|
||||
|
||||
const coloredScene = useMemo(() => {
|
||||
const clonedScene = scene.clone();
|
||||
clonedScene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.material = material;
|
||||
}
|
||||
});
|
||||
return clonedScene;
|
||||
}, [scene, material]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
material.dispose();
|
||||
coloredScene.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.geometry?.dispose();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [material, coloredScene]);
|
||||
|
||||
const handlePointerOver = useCallback(() => {
|
||||
setHovered(true);
|
||||
}, []);
|
||||
|
||||
const handlePointerOut = useCallback(() => {
|
||||
setHovered(false);
|
||||
}, []);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (!groupRef.current) return;
|
||||
|
||||
const amp = hovered ? 1.8 : 1.2;
|
||||
const targetRotationX = (-state.pointer.y * Math.PI * amp) / 8;
|
||||
const targetRotationY = (state.pointer.x * Math.PI * amp) / 8;
|
||||
|
||||
groupRef.current.rotation.x = THREE.MathUtils.damp(
|
||||
groupRef.current.rotation.x,
|
||||
targetRotationX,
|
||||
4,
|
||||
delta,
|
||||
);
|
||||
|
||||
groupRef.current.rotation.y = THREE.MathUtils.damp(
|
||||
groupRef.current.rotation.y,
|
||||
targetRotationY,
|
||||
4,
|
||||
delta,
|
||||
);
|
||||
|
||||
groupRef.current.rotation.z = THREE.MathUtils.damp(
|
||||
groupRef.current.rotation.z,
|
||||
Math.sin(state.clock.elapsedTime * 0.4) * 0.08,
|
||||
2,
|
||||
delta,
|
||||
);
|
||||
|
||||
const targetScale = hovered ? 1.15 : 1.0;
|
||||
groupRef.current.scale.setScalar(
|
||||
THREE.MathUtils.damp(groupRef.current.scale.x, targetScale, 6, delta),
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<group
|
||||
ref={groupRef}
|
||||
onPointerOver={handlePointerOver}
|
||||
onPointerOut={handlePointerOut}
|
||||
>
|
||||
<Float speed={2.5} rotationIntensity={0.4} floatIntensity={0.8}>
|
||||
<PresentationControls
|
||||
global
|
||||
rotation={[0.13, 0.1, 0]}
|
||||
polar={[-0.4, 0.2]}
|
||||
azimuth={[-1, 0.75]}
|
||||
snap={true}
|
||||
>
|
||||
<Center>
|
||||
<primitive object={coloredScene} scale={1.2} />
|
||||
</Center>
|
||||
</PresentationControls>
|
||||
</Float>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export function GlbViewer({ src }: { src: string }) {
|
||||
return (
|
||||
<Canvas
|
||||
camera={{ position: [0, 0, 6], fov: 45 }}
|
||||
style={{ width: '100%', height: '100%', cursor: 'pointer' }}
|
||||
dpr={[1, 1.5]}
|
||||
frameloop="demand"
|
||||
gl={{
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
powerPreference: 'low-power',
|
||||
}}
|
||||
>
|
||||
<ambientLight intensity={0.6} />
|
||||
<directionalLight position={[10, 10, 5]} intensity={2.5} color="#ffffff" />
|
||||
<directionalLight
|
||||
position={[-10, -10, -5]}
|
||||
intensity={1.5}
|
||||
color="#120AF2"
|
||||
/>
|
||||
<spotLight
|
||||
position={[0, 5, 10]}
|
||||
intensity={2.5}
|
||||
penumbra={0.8}
|
||||
color="#ffffff"
|
||||
angle={0.6}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<Model url={src} color="#120AF2" />
|
||||
</Suspense>
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user