Record page layout edition frontend (#17519)
Hidden behind a new feature flag: `IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED` https://github.com/user-attachments/assets/112f5a8d-0dd0-4b9f-8825-9980db35fcbd
This commit is contained in:
committed by
GitHub
parent
97a37604bd
commit
b15d092abd
+93
-1
@@ -21,6 +21,10 @@ import { NavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/re
|
||||
import { NavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/components/NavigateToPreviousRecordSingleRecordAction';
|
||||
import { RemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/components/RemoveFromFavoritesSingleRecordAction';
|
||||
import { RestoreSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/components/RestoreSingleRecordAction';
|
||||
import { CancelRecordPageLayoutSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/CancelRecordPageLayoutSingleRecordAction';
|
||||
import { EditRecordPageLayoutSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/EditRecordPageLayoutSingleRecordAction';
|
||||
import { SaveRecordPageLayoutSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/components/SaveRecordPageLayoutSingleRecordAction';
|
||||
import { RecordPageLayoutSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/record-page-layout-actions/types/RecordPageLayoutSingleRecordActionKeys';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
@@ -36,9 +40,11 @@ import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
IconArrowMerge,
|
||||
IconBuildingSkyscraper,
|
||||
IconCancel,
|
||||
IconCheckbox,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconDeviceFloppy,
|
||||
IconEdit,
|
||||
IconEyeOff,
|
||||
IconFileExport,
|
||||
@@ -47,6 +53,7 @@ import {
|
||||
IconHeartOff,
|
||||
IconLayout,
|
||||
IconLayoutDashboard,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
IconRotate2,
|
||||
@@ -60,11 +67,13 @@ import {
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
| NoSelectionRecordActionKeys
|
||||
| SingleRecordActionKeys
|
||||
| MultipleRecordsActionKeys,
|
||||
| MultipleRecordsActionKeys
|
||||
| RecordPageLayoutSingleRecordActionKeys,
|
||||
ActionConfig
|
||||
> = {
|
||||
[SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: {
|
||||
@@ -741,4 +750,87 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
|
||||
),
|
||||
hotKeys: ['G', 'N'],
|
||||
},
|
||||
|
||||
[RecordPageLayoutSingleRecordActionKeys.EDIT_RECORD_PAGE_LAYOUT]: {
|
||||
key: RecordPageLayoutSingleRecordActionKeys.EDIT_RECORD_PAGE_LAYOUT,
|
||||
label: msg`Edit Page Layout`,
|
||||
shortLabel: msg`Edit Layout`,
|
||||
isPinned: true,
|
||||
position: 30,
|
||||
Icon: IconPencil,
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
requiredPermissionFlag: PermissionFlagType.LAYOUTS,
|
||||
shouldBeRegistered: ({
|
||||
selectedRecord,
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
isFeatureFlagEnabled,
|
||||
}) =>
|
||||
isFeatureFlagEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
) &&
|
||||
isDefined(selectedRecord) &&
|
||||
!selectedRecord?.isRemote &&
|
||||
!isDefined(selectedRecord?.deletedAt) &&
|
||||
objectPermissions.canUpdateObjectRecords &&
|
||||
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
component: <EditRecordPageLayoutSingleRecordAction />,
|
||||
},
|
||||
[RecordPageLayoutSingleRecordActionKeys.SAVE_RECORD_PAGE_LAYOUT]: {
|
||||
key: RecordPageLayoutSingleRecordActionKeys.SAVE_RECORD_PAGE_LAYOUT,
|
||||
label: msg`Save Page Layout`,
|
||||
shortLabel: msg`Save`,
|
||||
isPinned: true,
|
||||
isPrimaryCTA: true,
|
||||
position: 31,
|
||||
Icon: IconDeviceFloppy,
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
requiredPermissionFlag: PermissionFlagType.LAYOUTS,
|
||||
shouldBeRegistered: ({
|
||||
selectedRecord,
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
isFeatureFlagEnabled,
|
||||
}) =>
|
||||
isFeatureFlagEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
) &&
|
||||
isDefined(selectedRecord) &&
|
||||
!selectedRecord?.isRemote &&
|
||||
!isDefined(selectedRecord?.deletedAt) &&
|
||||
objectPermissions.canUpdateObjectRecords &&
|
||||
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard,
|
||||
availableOn: [ActionViewType.PAGE_EDIT_MODE],
|
||||
component: <SaveRecordPageLayoutSingleRecordAction />,
|
||||
},
|
||||
[RecordPageLayoutSingleRecordActionKeys.CANCEL_RECORD_PAGE_LAYOUT_EDITION]: {
|
||||
key: RecordPageLayoutSingleRecordActionKeys.CANCEL_RECORD_PAGE_LAYOUT_EDITION,
|
||||
label: msg`Cancel Edition`,
|
||||
shortLabel: msg`Cancel`,
|
||||
isPinned: true,
|
||||
position: 32,
|
||||
Icon: IconCancel,
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
requiredPermissionFlag: PermissionFlagType.LAYOUTS,
|
||||
shouldBeRegistered: ({
|
||||
selectedRecord,
|
||||
objectPermissions,
|
||||
objectMetadataItem,
|
||||
isFeatureFlagEnabled,
|
||||
}) =>
|
||||
isFeatureFlagEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED,
|
||||
) &&
|
||||
isDefined(selectedRecord) &&
|
||||
!selectedRecord?.isRemote &&
|
||||
!isDefined(selectedRecord?.deletedAt) &&
|
||||
objectPermissions.canUpdateObjectRecords &&
|
||||
objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Dashboard,
|
||||
availableOn: [ActionViewType.PAGE_EDIT_MODE],
|
||||
component: <CancelRecordPageLayoutSingleRecordAction />,
|
||||
},
|
||||
};
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
|
||||
export const CancelRecordPageLayoutSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { resetDraftPageLayoutToPersistedPageLayout } =
|
||||
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
|
||||
|
||||
const handleClick = () => {
|
||||
closeCommandMenu();
|
||||
|
||||
resetDraftPageLayoutToPersistedPageLayout();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
};
|
||||
|
||||
return <Action onClick={handleClick} />;
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useResetLocationHash } from 'twenty-ui/utilities';
|
||||
|
||||
export const EditRecordPageLayoutSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { resetLocationHash } = useResetLocationHash();
|
||||
|
||||
const handleClick = () => {
|
||||
setIsPageLayoutInEditMode(true);
|
||||
resetLocationHash();
|
||||
};
|
||||
|
||||
return <Action onClick={handleClick} />;
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
|
||||
export const SaveRecordPageLayoutSingleRecordAction = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
|
||||
id: recordId,
|
||||
targetObjectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const handleClick = async () => {
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
closeCommandMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
}
|
||||
};
|
||||
|
||||
return <Action onClick={handleClick} />;
|
||||
};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export enum RecordPageLayoutSingleRecordActionKeys {
|
||||
EDIT_RECORD_PAGE_LAYOUT = 'edit-record-page-layout-single-record',
|
||||
SAVE_RECORD_PAGE_LAYOUT = 'save-record-page-layout-single-record',
|
||||
CANCEL_RECORD_PAGE_LAYOUT_EDITION = 'cancel-record-page-layout-edition-single-record',
|
||||
}
|
||||
+2
@@ -4,6 +4,7 @@ import { type RecordFilter } from '@/object-record/record-filter/types/RecordFil
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { type FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export type ShouldBeRegisteredFunctionParams = {
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
@@ -28,4 +29,5 @@ export type ShouldBeRegisteredFunctionParams = {
|
||||
objectMetadataItemNameSingular: string,
|
||||
) => boolean;
|
||||
forceRegisteredActionsByKey: Record<string, boolean | undefined>;
|
||||
isFeatureFlagEnabled: (featureFlagKey: FeatureFlagKey) => boolean;
|
||||
};
|
||||
|
||||
+12
@@ -2,6 +2,7 @@ import { forceRegisteredActionsByKeyState } from '@/action-menu/actions/states/f
|
||||
import { type ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
|
||||
import { getActionViewType } from '@/action-menu/actions/utils/getActionViewType';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { objectPermissionsFamilySelector } from '@/auth/states/objectPermissionsFamilySelector';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
@@ -135,6 +136,16 @@ export const useShouldActionBeRegisteredParams = ({
|
||||
forceRegisteredActionsByKeyState,
|
||||
);
|
||||
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const isFeatureFlagEnabled = (featureFlagKey: FeatureFlagKey) => {
|
||||
const featureFlag = currentWorkspace?.featureFlags?.find(
|
||||
(flag) => flag.key === featureFlagKey,
|
||||
);
|
||||
|
||||
return featureFlag?.value === true;
|
||||
};
|
||||
|
||||
return {
|
||||
objectMetadataItem,
|
||||
isFavorite,
|
||||
@@ -150,5 +161,6 @@ export const useShouldActionBeRegisteredParams = ({
|
||||
getTargetObjectReadPermission: getObjectReadPermission,
|
||||
getTargetObjectWritePermission: getObjectWritePermission,
|
||||
forceRegisteredActionsByKey,
|
||||
isFeatureFlagEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user