Implement basic edition for record page layouts (#15237)
1. Add actions for record page layouts to side panel 2. Support drag-and-drop of widgets https://github.com/user-attachments/assets/c4cef7c5-08a8-4999-b8a5-e1831cc66709
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
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 { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { inheritActionsFromDefaultConfig } from '@/action-menu/actions/record-actions/utils/inheritActionsFromDefaultConfig';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { PageLayoutSingleRecordActionKeys } from '@/page-layout/actions/PageLayoutSingleRecordActionKeys';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconDeviceFloppy, IconPencil, IconX } from 'twenty-ui/display';
|
||||
|
||||
export const RECORD_PAGE_LAYOUT_ACTIONS_CONFIG =
|
||||
inheritActionsFromDefaultConfig({
|
||||
config: {
|
||||
[PageLayoutSingleRecordActionKeys.EDIT_LAYOUT]: {
|
||||
key: PageLayoutSingleRecordActionKeys.EDIT_LAYOUT,
|
||||
label: msg`Edit Layout`,
|
||||
shortLabel: msg`Edit`,
|
||||
isPinned: true,
|
||||
position: 0,
|
||||
Icon: IconPencil,
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
shouldBeRegistered: ({ selectedRecord }) =>
|
||||
isDefined(selectedRecord) &&
|
||||
!selectedRecord?.isRemote &&
|
||||
!isDefined(selectedRecord?.deletedAt),
|
||||
// TODO: Once backend is ready, uncomment the line below
|
||||
// isDefined(selectedRecord?.pageLayoutId),
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
component: <EditRecordPageLayoutSingleRecordAction />,
|
||||
},
|
||||
[PageLayoutSingleRecordActionKeys.SAVE_LAYOUT]: {
|
||||
key: PageLayoutSingleRecordActionKeys.SAVE_LAYOUT,
|
||||
label: msg`Save Layout`,
|
||||
shortLabel: msg`Save`,
|
||||
isPinned: true,
|
||||
position: 1,
|
||||
Icon: IconDeviceFloppy,
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
shouldBeRegistered: ({ selectedRecord }) =>
|
||||
isDefined(selectedRecord) &&
|
||||
!selectedRecord?.isRemote &&
|
||||
!isDefined(selectedRecord?.deletedAt),
|
||||
// TODO: Once backend is ready, uncomment the line below
|
||||
// isDefined(selectedRecord?.pageLayoutId),
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
component: <SaveRecordPageLayoutSingleRecordAction />,
|
||||
},
|
||||
[PageLayoutSingleRecordActionKeys.CANCEL_LAYOUT_EDITION]: {
|
||||
key: PageLayoutSingleRecordActionKeys.CANCEL_LAYOUT_EDITION,
|
||||
label: msg`Cancel Edition`,
|
||||
shortLabel: msg`Cancel`,
|
||||
isPinned: true,
|
||||
position: 2,
|
||||
Icon: IconX,
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
shouldBeRegistered: ({ selectedRecord }) =>
|
||||
isDefined(selectedRecord) &&
|
||||
!selectedRecord?.isRemote &&
|
||||
!isDefined(selectedRecord?.deletedAt),
|
||||
// TODO: Once backend is ready, uncomment the line below
|
||||
// isDefined(selectedRecord?.pageLayoutId),
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
component: <CancelRecordPageLayoutSingleRecordAction />,
|
||||
},
|
||||
},
|
||||
actionKeys: [
|
||||
SingleRecordActionKeys.ADD_TO_FAVORITES,
|
||||
SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
|
||||
SingleRecordActionKeys.DELETE,
|
||||
SingleRecordActionKeys.DESTROY,
|
||||
SingleRecordActionKeys.RESTORE,
|
||||
SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW,
|
||||
SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
|
||||
SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
|
||||
],
|
||||
propertiesToOverwrite: {
|
||||
[SingleRecordActionKeys.ADD_TO_FAVORITES]: {
|
||||
position: 3,
|
||||
},
|
||||
[SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
|
||||
position: 4,
|
||||
},
|
||||
[SingleRecordActionKeys.DELETE]: {
|
||||
position: 5,
|
||||
label: msg`Delete record`,
|
||||
},
|
||||
[SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW]: {
|
||||
position: 6,
|
||||
label: msg`Export record`,
|
||||
},
|
||||
[SingleRecordActionKeys.DESTROY]: {
|
||||
position: 7,
|
||||
label: msg`Permanently destroy record`,
|
||||
},
|
||||
[SingleRecordActionKeys.RESTORE]: {
|
||||
position: 8,
|
||||
label: msg`Restore record`,
|
||||
},
|
||||
[SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD]: {
|
||||
position: 9,
|
||||
label: msg`Navigate to previous record`,
|
||||
},
|
||||
[SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: {
|
||||
position: 10,
|
||||
label: msg`Navigate to next record`,
|
||||
},
|
||||
},
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { DEFAULT_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPageLayoutId';
|
||||
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const CancelRecordPageLayoutSingleRecordAction = () => {
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
// For COMPANY objects, use DEFAULT_PAGE_LAYOUT_ID
|
||||
// For other objects, use the record's pageLayoutId (when backend is ready)
|
||||
const pageLayoutId =
|
||||
objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
|
||||
? DEFAULT_PAGE_LAYOUT_ID
|
||||
: selectedRecord?.pageLayoutId;
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const { resetDraftPageLayoutToPersistedPageLayout } =
|
||||
useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
|
||||
|
||||
const handleClick = () => {
|
||||
resetDraftPageLayoutToPersistedPageLayout();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
};
|
||||
|
||||
return <Action onClick={handleClick} />;
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { DEFAULT_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPageLayoutId';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const EditRecordPageLayoutSingleRecordAction = () => {
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
// For COMPANY objects, use DEFAULT_PAGE_LAYOUT_ID
|
||||
// For other objects, use the record's pageLayoutId (when backend is ready)
|
||||
const pageLayoutId =
|
||||
objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
|
||||
? DEFAULT_PAGE_LAYOUT_ID
|
||||
: selectedRecord?.pageLayoutId;
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsPageLayoutInEditMode(true);
|
||||
};
|
||||
|
||||
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 { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { DEFAULT_PAGE_LAYOUT_ID } from '@/page-layout/constants/DefaultPageLayoutId';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const SaveRecordPageLayoutSingleRecordAction = () => {
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
// For COMPANY objects, use DEFAULT_PAGE_LAYOUT_ID
|
||||
// For other objects, use the record's pageLayoutId (when backend is ready)
|
||||
const pageLayoutId =
|
||||
objectMetadataItem.nameSingular === CoreObjectNameSingular.Company
|
||||
? DEFAULT_PAGE_LAYOUT_ID
|
||||
: selectedRecord?.pageLayoutId;
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
const handleClick = async () => {
|
||||
await savePageLayout();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
};
|
||||
|
||||
return <Action onClick={handleClick} />;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DASHBOARD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/DashboardActionsConfig';
|
||||
import { DEFAULT_RECORD_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/DefaultRecordActionsConfig';
|
||||
import { RECORD_PAGE_LAYOUT_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/RecordPageLayoutActionsConfig';
|
||||
import { WORKFLOW_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowActionsConfig';
|
||||
import { WORKFLOW_RUNS_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig';
|
||||
import { WORKFLOW_VERSIONS_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowVersionsActionsConfig';
|
||||
@@ -10,8 +11,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getActionConfig = ({
|
||||
objectMetadataItem,
|
||||
isRecordPageLayoutEnabled,
|
||||
}: {
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
isRecordPageLayoutEnabled: boolean;
|
||||
}): Record<string, ActionConfig> => {
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return {};
|
||||
@@ -30,6 +33,13 @@ export const getActionConfig = ({
|
||||
case CoreObjectNameSingular.WorkflowRun: {
|
||||
return WORKFLOW_RUNS_ACTIONS_CONFIG;
|
||||
}
|
||||
case CoreObjectNameSingular.Company: {
|
||||
if (isRecordPageLayoutEnabled) {
|
||||
return RECORD_PAGE_LAYOUT_ACTIONS_CONFIG;
|
||||
}
|
||||
|
||||
return DEFAULT_RECORD_ACTIONS_CONFIG;
|
||||
}
|
||||
default: {
|
||||
return DEFAULT_RECORD_ACTIONS_CONFIG;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import { contextStoreCurrentViewTypeComponentState } from '@/context-store/state
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const useRegisteredActions = (
|
||||
shouldBeRegisteredParams: ShouldBeRegisteredFunctionParams,
|
||||
@@ -27,6 +29,10 @@ export const useRegisteredActions = (
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
);
|
||||
|
||||
const isRecordPageLayoutEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
|
||||
);
|
||||
|
||||
const viewType = getActionViewType(
|
||||
contextStoreCurrentViewType,
|
||||
contextStoreTargetedRecordsRule,
|
||||
@@ -34,6 +40,7 @@ export const useRegisteredActions = (
|
||||
|
||||
const recordActionConfig = getActionConfig({
|
||||
objectMetadataItem,
|
||||
isRecordPageLayoutEnabled,
|
||||
});
|
||||
|
||||
const relatedRecordActionConfig = useRelatedRecordActions({
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
|
||||
import { PageLayoutVerticalListEditor } from '@/page-layout/components/PageLayoutVerticalListEditor';
|
||||
import { PageLayoutVerticalListViewer } from '@/page-layout/components/PageLayoutVerticalListViewer';
|
||||
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
|
||||
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 styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type PageLayoutContentProps = {
|
||||
tabId: string;
|
||||
};
|
||||
|
||||
export const PageLayoutContent = ({ tabId }: PageLayoutContentProps) => {
|
||||
const isRecordPageEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_ENABLED,
|
||||
);
|
||||
|
||||
const isPageLayoutInEditMode = useRecoilComponentValue(
|
||||
isPageLayoutInEditModeComponentState,
|
||||
);
|
||||
|
||||
const { currentPageLayout } = useCurrentPageLayout();
|
||||
const { reorderWidgets } = useReorderPageLayoutWidgets(tabId);
|
||||
|
||||
const activeTab = currentPageLayout?.tabs.find((tab) => tab.id === tabId);
|
||||
|
||||
if (!isDefined(currentPageLayout) || !isDefined(activeTab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isVerticalList =
|
||||
isRecordPageEnabled && activeTab.layoutMode === 'vertical-list';
|
||||
|
||||
if (isVerticalList) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{isPageLayoutInEditMode ? (
|
||||
<PageLayoutVerticalListEditor
|
||||
widgets={activeTab.widgets}
|
||||
onReorder={reorderWidgets}
|
||||
/>
|
||||
) : (
|
||||
<PageLayoutVerticalListViewer widgets={activeTab.widgets} />
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageLayoutGridLayout tabId={tabId} />;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SummaryCard } from '@/object-record/record-show/components/SummaryCard';
|
||||
import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
|
||||
import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent';
|
||||
import { useCurrentPageLayout } from '@/page-layout/hooks/useCurrentPageLayout';
|
||||
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
@@ -29,7 +29,7 @@ export const PageLayoutLeftPanel = ({
|
||||
isInRightDrawer={isInRightDrawer}
|
||||
/>
|
||||
|
||||
<PageLayoutGridLayout tabId={pinnedLeftTabId} />
|
||||
<PageLayoutContent tabId={pinnedLeftTabId} />
|
||||
</ShowPageLeftContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { PageLayoutGridLayout } from '@/page-layout/components/PageLayoutGridLayout';
|
||||
import { PageLayoutContent } from '@/page-layout/components/PageLayoutContent';
|
||||
import { PageLayoutLeftPanel } from '@/page-layout/components/PageLayoutLeftPanel';
|
||||
import { PageLayoutTabHeader } from '@/page-layout/components/PageLayoutTabHeader';
|
||||
import { useCreatePageLayoutTab } from '@/page-layout/hooks/useCreatePageLayoutTab';
|
||||
@@ -93,7 +93,7 @@ export const PageLayoutRendererContent = () => {
|
||||
defaultEnableXScroll={false}
|
||||
>
|
||||
{isDefined(activeTabId) && (
|
||||
<PageLayoutGridLayout tabId={activeTabId} />
|
||||
<PageLayoutContent tabId={activeTabId} />
|
||||
)}
|
||||
</StyledScrollWrapper>
|
||||
</StyledTabsAndDashboardContainer>
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
type DropResult,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useId } from 'react';
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
const StyledVerticalListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDraggableWrapper = styled.div<{ isDragging: boolean }>`
|
||||
background: ${({ theme, isDragging }) =>
|
||||
isDragging ? theme.background.transparent.light : 'transparent'};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
transition: background 0.1s ease;
|
||||
`;
|
||||
|
||||
type PageLayoutVerticalListEditorProps = {
|
||||
widgets: PageLayoutWidget[];
|
||||
onReorder: (result: DropResult) => void;
|
||||
};
|
||||
|
||||
export const PageLayoutVerticalListEditor = ({
|
||||
widgets,
|
||||
onReorder,
|
||||
}: PageLayoutVerticalListEditorProps) => {
|
||||
const droppableId = `page-layout-vertical-list-${useId()}`;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onReorder}>
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided) => (
|
||||
<StyledVerticalListContainer
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{widgets.map((widget, index) => (
|
||||
<Draggable key={widget.id} draggableId={widget.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<StyledDraggableWrapper
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.draggableProps}
|
||||
isDragging={snapshot.isDragging}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<div {...provided.dragHandleProps}>
|
||||
<WidgetRenderer widget={widget} />
|
||||
</div>
|
||||
</StyledDraggableWrapper>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</StyledVerticalListContainer>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer';
|
||||
import styled from '@emotion/styled';
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
const StyledVerticalListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type PageLayoutVerticalListViewerProps = {
|
||||
widgets: PageLayoutWidget[];
|
||||
};
|
||||
|
||||
export const PageLayoutVerticalListViewer = ({
|
||||
widgets,
|
||||
}: PageLayoutVerticalListViewerProps) => {
|
||||
return (
|
||||
<StyledVerticalListContainer>
|
||||
{widgets.map((widget) => (
|
||||
<div key={widget.id}>
|
||||
<WidgetRenderer widget={widget} />
|
||||
</div>
|
||||
))}
|
||||
</StyledVerticalListContainer>
|
||||
);
|
||||
};
|
||||
@@ -115,6 +115,25 @@ export const DEFAULT_PAGE_LAYOUT: PageLayout = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
__typename: 'PageLayoutWidget',
|
||||
id: 'default-widget-tasks-2',
|
||||
pageLayoutTabId: 'default-tab-tasks',
|
||||
title: 'Tasks',
|
||||
type: WidgetType.TASKS,
|
||||
objectMetadataId: null,
|
||||
gridPosition: {
|
||||
__typename: 'GridPosition',
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 6,
|
||||
columnSpan: 12,
|
||||
},
|
||||
configuration: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Notes tab (position 400)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useReorderPageLayoutWidgets = (
|
||||
tabId: string,
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useRecoilComponentCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const reorderWidgets = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
set(pageLayoutDraftState, (prev) => {
|
||||
const tab = prev.tabs.find((t) => t.id === tabId);
|
||||
if (!isDefined(tab)) return prev;
|
||||
|
||||
const newWidgets = Array.from(tab.widgets ?? []);
|
||||
const [removed] = newWidgets.splice(result.source.index, 1);
|
||||
newWidgets.splice(result.destination!.index, 0, removed);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
tabs: prev.tabs.map((t) =>
|
||||
t.id === tabId ? { ...t, widgets: newWidgets } : t,
|
||||
),
|
||||
};
|
||||
});
|
||||
},
|
||||
[tabId, pageLayoutDraftState],
|
||||
);
|
||||
|
||||
return { reorderWidgets };
|
||||
};
|
||||
Reference in New Issue
Block a user