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
@@ -1417,6 +1417,7 @@ export enum FeatureFlagKey {
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
|
||||
@@ -1384,6 +1384,7 @@ export enum FeatureFlagKey {
|
||||
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
|
||||
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
|
||||
|
||||
+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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,12 +3,13 @@ import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLay
|
||||
import { PageLayoutVerticalListEditor } from '@/page-layout/components/PageLayoutVerticalListEditor';
|
||||
import { PageLayoutVerticalListViewer } from '@/page-layout/components/PageLayoutVerticalListViewer';
|
||||
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
|
||||
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
|
||||
import { usePageLayoutTabWithVisibleWidgetsOrThrow } from '@/page-layout/hooks/usePageLayoutTabWithVisibleWidgetsOrThrow';
|
||||
import { useReorderPageLayoutWidgets } from '@/page-layout/hooks/useReorderPageLayoutWidgets';
|
||||
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { FeatureFlagKey, PageLayoutType } from '~/generated/graphql';
|
||||
|
||||
export const PageLayoutContent = () => {
|
||||
const isRecordPageEnabled = useIsFeatureEnabled(
|
||||
@@ -27,6 +28,11 @@ export const PageLayoutContent = () => {
|
||||
|
||||
const { layoutMode } = usePageLayoutContentContext();
|
||||
|
||||
const { currentPageLayout } = useCurrentPageLayoutOrThrow();
|
||||
|
||||
const isRecordPageLayout =
|
||||
currentPageLayout.type === PageLayoutType.RECORD_PAGE;
|
||||
|
||||
const isCanvasLayout = isRecordPageEnabled && layoutMode === 'canvas';
|
||||
const isVerticalList = isRecordPageEnabled && layoutMode === 'vertical-list';
|
||||
|
||||
@@ -39,6 +45,7 @@ export const PageLayoutContent = () => {
|
||||
<PageLayoutVerticalListEditor
|
||||
widgets={activeTab.widgets}
|
||||
onReorder={reorderWidgets}
|
||||
isReorderEnabled={!isRecordPageLayout}
|
||||
/>
|
||||
) : (
|
||||
<PageLayoutVerticalListViewer widgets={activeTab.widgets} />
|
||||
|
||||
+28
-5
@@ -1,7 +1,10 @@
|
||||
import { usePageLayoutShouldUseWhiteBackground } from '@/page-layout/hooks/usePageLayoutShouldUseWhiteBackground';
|
||||
import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant';
|
||||
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
|
||||
import { type PageLayoutVerticalListViewerVariant } from '@/page-layout/types/PageLayoutVerticalListViewerVariant';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
|
||||
import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
@@ -11,8 +14,10 @@ import {
|
||||
type DropResult,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useId } from 'react';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledVerticalListContainer = styled.div<{
|
||||
variant: PageLayoutVerticalListViewerVariant;
|
||||
shouldUseWhiteBackground: boolean;
|
||||
}>`
|
||||
background: ${({ theme, shouldUseWhiteBackground }) =>
|
||||
@@ -21,7 +26,9 @@ const StyledVerticalListContainer = styled.div<{
|
||||
: theme.background.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
padding: ${({ theme, variant }) =>
|
||||
variant === 'side-column' ? theme.spacing(1) : theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDraggableWrapper = styled.div<{ isDragging: boolean }>`
|
||||
@@ -34,15 +41,25 @@ const StyledDraggableWrapper = styled.div<{ isDragging: boolean }>`
|
||||
type PageLayoutVerticalListEditorProps = {
|
||||
widgets: PageLayoutWidget[];
|
||||
onReorder: (result: DropResult) => void;
|
||||
isReorderEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const PageLayoutVerticalListEditor = ({
|
||||
widgets,
|
||||
onReorder,
|
||||
isReorderEnabled = true,
|
||||
}: PageLayoutVerticalListEditorProps) => {
|
||||
const droppableId = `page-layout-vertical-list-${useId()}`;
|
||||
|
||||
const { shouldUseWhiteBackground } = usePageLayoutShouldUseWhiteBackground();
|
||||
const { isInRightDrawer } = useLayoutRenderingContext();
|
||||
const isMobile = useIsMobile();
|
||||
const { isInPinnedTab } = useIsInPinnedTab();
|
||||
|
||||
const variant = getPageLayoutVerticalListViewerVariant({
|
||||
isInPinnedTab,
|
||||
isMobile,
|
||||
isInRightDrawer,
|
||||
});
|
||||
|
||||
const setDraggingWidgetId = useSetRecoilComponentState(
|
||||
pageLayoutDraggingWidgetIdComponentState,
|
||||
@@ -62,12 +79,18 @@ export const PageLayoutVerticalListEditor = ({
|
||||
{(provided) => (
|
||||
<StyledVerticalListContainer
|
||||
ref={provided.innerRef}
|
||||
shouldUseWhiteBackground={shouldUseWhiteBackground}
|
||||
variant={variant}
|
||||
shouldUseWhiteBackground={isMobile || isInRightDrawer}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{widgets.map((widget, index) => (
|
||||
<Draggable key={widget.id} draggableId={widget.id} index={index}>
|
||||
<Draggable
|
||||
key={widget.id}
|
||||
draggableId={widget.id}
|
||||
index={index}
|
||||
isDragDisabled={!isReorderEnabled}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
<StyledDraggableWrapper
|
||||
ref={provided.innerRef}
|
||||
|
||||
+4
-4
@@ -2,7 +2,6 @@ import styled from '@emotion/styled';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant';
|
||||
import { usePageLayoutShouldUseWhiteBackground } from '@/page-layout/hooks/usePageLayoutShouldUseWhiteBackground';
|
||||
import { type PageLayoutVerticalListViewerVariant } from '@/page-layout/types/PageLayoutVerticalListViewerVariant';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
|
||||
@@ -10,8 +9,8 @@ import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
|
||||
const StyledVerticalListContainer = styled.div<{
|
||||
shouldUseWhiteBackground: boolean;
|
||||
variant: PageLayoutVerticalListViewerVariant;
|
||||
shouldUseWhiteBackground: boolean;
|
||||
}>`
|
||||
background: ${({ theme, shouldUseWhiteBackground }) =>
|
||||
shouldUseWhiteBackground
|
||||
@@ -21,6 +20,8 @@ const StyledVerticalListContainer = styled.div<{
|
||||
flex-direction: column;
|
||||
gap: ${({ theme, variant }) =>
|
||||
variant === 'side-column' ? 0 : theme.spacing(2)};
|
||||
padding: ${({ theme, variant }) =>
|
||||
variant === 'side-column' ? 0 : theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type PageLayoutVerticalListViewerProps = {
|
||||
@@ -30,7 +31,6 @@ type PageLayoutVerticalListViewerProps = {
|
||||
export const PageLayoutVerticalListViewer = ({
|
||||
widgets,
|
||||
}: PageLayoutVerticalListViewerProps) => {
|
||||
const { shouldUseWhiteBackground } = usePageLayoutShouldUseWhiteBackground();
|
||||
const { isInRightDrawer } = useLayoutRenderingContext();
|
||||
const isMobile = useIsMobile();
|
||||
const { isInPinnedTab } = useIsInPinnedTab();
|
||||
@@ -43,8 +43,8 @@ export const PageLayoutVerticalListViewer = ({
|
||||
|
||||
return (
|
||||
<StyledVerticalListContainer
|
||||
shouldUseWhiteBackground={shouldUseWhiteBackground}
|
||||
variant={variant}
|
||||
shouldUseWhiteBackground={isMobile || isInRightDrawer}
|
||||
>
|
||||
{widgets.map((widget) => (
|
||||
<div key={widget.id}>
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { PageLayoutType } from '~/generated/graphql';
|
||||
|
||||
export const usePageLayoutShouldUseWhiteBackground = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { isInRightDrawer, layoutType } = useLayoutRenderingContext();
|
||||
|
||||
const shouldUseWhiteBackground =
|
||||
layoutType === PageLayoutType.RECORD_PAGE && (isMobile || isInRightDrawer);
|
||||
|
||||
return { shouldUseWhiteBackground };
|
||||
};
|
||||
@@ -1,30 +1,7 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId';
|
||||
import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId';
|
||||
import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId';
|
||||
import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId';
|
||||
import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
|
||||
import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
|
||||
import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const OBJECT_NAME_TO_DEFAULT_LAYOUT_ID: Record<string, string> = {
|
||||
[CoreObjectNameSingular.Company]: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Person]: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Opportunity]:
|
||||
DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Note]: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Task]: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Workflow]: DEFAULT_WORKFLOW_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.WorkflowVersion]:
|
||||
DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.WorkflowRun]: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID,
|
||||
};
|
||||
|
||||
export const useRecordPageLayoutId = ({
|
||||
id,
|
||||
@@ -37,23 +14,12 @@ export const useRecordPageLayoutId = ({
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDefined(record)) {
|
||||
return {
|
||||
pageLayoutId: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (isDefined(record.pageLayoutId)) {
|
||||
return {
|
||||
pageLayoutId: record.pageLayoutId,
|
||||
};
|
||||
}
|
||||
|
||||
const defaultLayoutId =
|
||||
OBJECT_NAME_TO_DEFAULT_LAYOUT_ID[targetObjectNameSingular] ??
|
||||
DEFAULT_RECORD_PAGE_LAYOUT_ID;
|
||||
const pageLayoutId = getRecordPageLayoutId({
|
||||
record,
|
||||
targetObjectNameSingular,
|
||||
});
|
||||
|
||||
return {
|
||||
pageLayoutId: defaultLayoutId,
|
||||
pageLayoutId,
|
||||
};
|
||||
};
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { getRecordPageLayoutId } from '@/page-layout/utils/getRecordPageLayoutId';
|
||||
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useRecordPageLayoutIdFromRecordStoreOrThrow = ({
|
||||
id,
|
||||
targetObjectNameSingular,
|
||||
}: TargetRecordIdentifier) => {
|
||||
const record = useRecoilValue(recordStoreFamilyState(id));
|
||||
|
||||
if (!isDefined(record)) {
|
||||
throw new Error(`Record with id ${id} not found in record store`);
|
||||
}
|
||||
|
||||
const pageLayoutId = getRecordPageLayoutId({
|
||||
record,
|
||||
targetObjectNameSingular,
|
||||
});
|
||||
|
||||
if (!isDefined(pageLayoutId)) {
|
||||
throw new Error(`Page layout id not found for record with id ${id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
pageLayoutId,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultCompanyRecordPageLayoutId';
|
||||
import { DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultNoteRecordPageLayoutId';
|
||||
import { DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultOpportunityRecordPageLayoutId';
|
||||
import { DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPersonRecordPageLayoutId';
|
||||
import { DEFAULT_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultRecordPageLayoutId';
|
||||
import { DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultTaskRecordPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowRunPageLayoutId';
|
||||
import { DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultWorkflowVersionPageLayoutId';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const OBJECT_NAME_TO_DEFAULT_LAYOUT_ID: Record<string, string> = {
|
||||
[CoreObjectNameSingular.Company]: DEFAULT_COMPANY_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Person]: DEFAULT_PERSON_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Opportunity]:
|
||||
DEFAULT_OPPORTUNITY_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Note]: DEFAULT_NOTE_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Task]: DEFAULT_TASK_RECORD_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.Workflow]: DEFAULT_WORKFLOW_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.WorkflowVersion]:
|
||||
DEFAULT_WORKFLOW_VERSION_PAGE_LAYOUT_ID,
|
||||
[CoreObjectNameSingular.WorkflowRun]: DEFAULT_WORKFLOW_RUN_PAGE_LAYOUT_ID,
|
||||
};
|
||||
|
||||
export const getRecordPageLayoutId = ({
|
||||
record,
|
||||
targetObjectNameSingular,
|
||||
}: {
|
||||
record: ObjectRecord | null | undefined;
|
||||
targetObjectNameSingular: string;
|
||||
}): string | null => {
|
||||
if (!isDefined(record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDefined(record.pageLayoutId)) {
|
||||
return record.pageLayoutId;
|
||||
}
|
||||
|
||||
const defaultLayoutId =
|
||||
OBJECT_NAME_TO_DEFAULT_LAYOUT_ID[targetObjectNameSingular] ??
|
||||
DEFAULT_RECORD_PAGE_LAYOUT_ID;
|
||||
|
||||
return defaultLayoutId;
|
||||
};
|
||||
+10
-2
@@ -29,7 +29,7 @@ import styled from '@emotion/styled';
|
||||
import { type MouseEvent } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
import { WidgetType } from '~/generated/graphql';
|
||||
import { PageLayoutType, WidgetType } from '~/generated/graphql';
|
||||
|
||||
const StyledNoAccessContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -79,6 +79,9 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
|
||||
const isLastWidget = useIsCurrentWidgetLastOfTab(widget.id);
|
||||
|
||||
const isReorderEnabled =
|
||||
currentPageLayout.type !== PageLayoutType.RECORD_PAGE;
|
||||
|
||||
// TODO: when we have more widgets without headers, we should use a more generic approach to hide the header
|
||||
// each widget type could have metadata (e.g., hasHeader: boolean or headerMode: 'always' | 'editOnly' | 'never')
|
||||
const isRichTextWidget = widget.type === WidgetType.STANDALONE_RICH_TEXT;
|
||||
@@ -144,6 +147,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
variant={variant}
|
||||
isInEditMode={isPageLayoutInEditMode}
|
||||
isResizing={isResizing}
|
||||
isReorderEnabled={isReorderEnabled}
|
||||
title={widget.title}
|
||||
onRemove={handleRemove}
|
||||
actions={actions}
|
||||
@@ -158,7 +162,11 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<WidgetCardContent variant={variant} hasHeader={showHeader}>
|
||||
<WidgetCardContent
|
||||
variant={variant}
|
||||
hasHeader={showHeader}
|
||||
isEditable={isPageLayoutInEditMode}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={PageLayoutWidgetInvalidConfigDisplay}
|
||||
|
||||
+12
-10
@@ -87,12 +87,23 @@ const StyledWidgetCard = styled.div<{
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === 'record-page' && !isEditable) {
|
||||
return css`
|
||||
background: ${theme.background.primary};
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${theme.border.radius.md};
|
||||
padding: ${theme.spacing(2)};
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
(variant === 'side-column' && isEditable) ||
|
||||
(variant === 'record-page' && isEditable)
|
||||
) {
|
||||
return css`
|
||||
background: ${theme.background.primary};
|
||||
background: ${variant === 'side-column'
|
||||
? theme.background.secondary
|
||||
: theme.background.primary};
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${theme.border.radius.md};
|
||||
padding: ${theme.spacing(2)};
|
||||
@@ -125,15 +136,6 @@ const StyledWidgetCard = styled.div<{
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === 'record-page' && !isEditable) {
|
||||
return css`
|
||||
background: ${theme.background.primary};
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${theme.border.radius.md};
|
||||
padding: ${theme.spacing(2)};
|
||||
`;
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
|
||||
+6
-2
@@ -5,6 +5,7 @@ import { type WidgetCardVariant } from '~/modules/page-layout/widgets/types/Widg
|
||||
const StyledWidgetCardContent = styled.div<{
|
||||
variant: WidgetCardVariant;
|
||||
hasHeader: boolean;
|
||||
isEditable: boolean;
|
||||
}>`
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
@@ -20,14 +21,17 @@ const StyledWidgetCardContent = styled.div<{
|
||||
}
|
||||
`}
|
||||
|
||||
${({ theme, variant }) => {
|
||||
${({ theme, variant, isEditable }) => {
|
||||
if (variant === 'dashboard') {
|
||||
return css`
|
||||
padding: ${theme.spacing(2)};
|
||||
`;
|
||||
}
|
||||
|
||||
if (variant === 'record-page') {
|
||||
if (
|
||||
variant === 'record-page' ||
|
||||
(variant === 'side-column' && isEditable)
|
||||
) {
|
||||
return css`
|
||||
border: 1px solid ${theme.border.color.medium};
|
||||
border-radius: ${theme.border.radius.md};
|
||||
|
||||
+3
-1
@@ -25,6 +25,7 @@ export type WidgetCardHeaderProps = {
|
||||
actions?: WidgetAction[];
|
||||
className?: string;
|
||||
isResizing?: boolean;
|
||||
isReorderEnabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledWidgetCardHeader = styled.div`
|
||||
@@ -78,6 +79,7 @@ export const WidgetCardHeader = ({
|
||||
isEmpty = false,
|
||||
isInEditMode = false,
|
||||
isResizing = false,
|
||||
isReorderEnabled = true,
|
||||
title,
|
||||
onRemove,
|
||||
forbiddenDisplay,
|
||||
@@ -94,7 +96,7 @@ export const WidgetCardHeader = ({
|
||||
return (
|
||||
<StyledWidgetCardHeader className={className}>
|
||||
<AnimatePresence initial={false}>
|
||||
{!isEmpty && isInEditMode && (
|
||||
{!isEmpty && isInEditMode && isReorderEnabled && (
|
||||
<WidgetGrip
|
||||
className="drag-handle"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export enum FeatureFlagKey {
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_DASHBOARD_V2_ENABLED = 'IS_DASHBOARD_V2_ENABLED',
|
||||
|
||||
+1
@@ -226,6 +226,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED: false,
|
||||
IS_FILES_FIELD_ENABLED: false,
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED: false,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false,
|
||||
},
|
||||
userWorkspaceRoleMap: {},
|
||||
eventEmitterService: {
|
||||
|
||||
Reference in New Issue
Block a user