nitin
2026-02-03 15:09:52 +00:00
committed by GitHub
parent a1f26cda0d
commit 19b56c5d3a
7 changed files with 281 additions and 10 deletions
@@ -4,26 +4,36 @@ import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { getFrontComponentWidgetTypeSelectItemId } from '@/command-menu/pages/page-layout/utils/getFrontComponentWidgetTypeSelectItemId';
import { isExistingWidgetMissingOrDifferentType } from '@/command-menu/pages/page-layout/utils/isExistingWidgetMissingOrDifferentType';
import { useOpportunityDefaultChartConfig } from '@/page-layout/hooks/useOpportunityDefaultChartConfig';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { FIND_MANY_FRONT_COMPONENTS } from '@/front-components/graphql/queries/findManyFrontComponents';
import { useCreatePageLayoutFrontComponentWidget } from '@/page-layout/hooks/useCreatePageLayoutFrontComponentWidget';
import { useCreatePageLayoutGraphWidget } from '@/page-layout/hooks/useCreatePageLayoutGraphWidget';
import { useCreatePageLayoutIframeWidget } from '@/page-layout/hooks/useCreatePageLayoutIframeWidget';
import { useCreatePageLayoutStandaloneRichTextWidget } from '@/page-layout/hooks/useCreatePageLayoutStandaloneRichTextWidget';
import { useOpportunityDefaultChartConfig } from '@/page-layout/hooks/useOpportunityDefaultChartConfig';
import { useRemovePageLayoutWidgetAndPreservePosition } from '@/page-layout/hooks/useRemovePageLayoutWidgetAndPreservePosition';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useQuery } from '@apollo/client';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
IconAlignBoxLeftTop,
IconApps,
IconChartPie,
IconFrame,
} from 'twenty-ui/display';
import { WidgetType } from '~/generated/graphql';
import {
FeatureFlagKey,
type FrontComponent,
WidgetType,
} from '~/generated/graphql';
export const CommandMenuPageLayoutWidgetTypeSelect = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -43,9 +53,31 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
const { createPageLayoutStandaloneRichTextWidget } =
useCreatePageLayoutStandaloneRichTextWidget(pageLayoutId);
const { createPageLayoutFrontComponentWidget } =
useCreatePageLayoutFrontComponentWidget(pageLayoutId);
const { removePageLayoutWidgetAndPreservePosition } =
useRemovePageLayoutWidgetAndPreservePosition(pageLayoutId);
const isApplicationEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_APPLICATION_ENABLED,
);
const { data: frontComponentsData } = useQuery<{
frontComponents: FrontComponent[];
}>(FIND_MANY_FRONT_COMPONENTS, {
skip: !isApplicationEnabled,
});
const frontComponents = frontComponentsData?.frontComponents ?? [];
const frontComponentsWithSelectItemId = frontComponents.map(
(frontComponent) => ({
frontComponent,
selectItemId: getFrontComponentWidgetTypeSelectItemId(frontComponent.id),
}),
);
const [pageLayoutEditingWidgetId, setPageLayoutEditingWidgetId] =
useRecoilComponentState(
pageLayoutEditingWidgetIdComponentState,
@@ -127,11 +159,36 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
closeCommandMenu();
};
const handleCreateFrontComponentWidget = (frontComponent: FrontComponent) => {
if (
isExistingWidgetMissingOrDifferentType(
existingWidget?.type,
WidgetType.FRONT_COMPONENT,
)
) {
if (isDefined(pageLayoutEditingWidgetId)) {
removePageLayoutWidgetAndPreservePosition(pageLayoutEditingWidgetId);
}
const newWidget = createPageLayoutFrontComponentWidget(
frontComponent.name,
frontComponent.id,
);
setPageLayoutEditingWidgetId(newWidget.id);
}
closeCommandMenu();
};
const selectableItemIds = [
'chart',
'iframe',
'rich-text',
...frontComponentsWithSelectItemId.map(({ selectItemId }) => selectItemId),
];
return (
<CommandMenuList
commandGroups={[]}
selectableItemIds={['chart', 'iframe', 'rich-text']}
>
<CommandMenuList commandGroups={[]} selectableItemIds={selectableItemIds}>
<CommandGroup heading={t`Widget type`}>
<SelectableListItem
itemId="chart"
@@ -168,6 +225,29 @@ export const CommandMenuPageLayoutWidgetTypeSelect = () => {
/>
</SelectableListItem>
</CommandGroup>
{isApplicationEnabled && frontComponentsWithSelectItemId.length > 0 && (
<CommandGroup heading={t`Front Components`}>
{frontComponentsWithSelectItemId.map(
({ frontComponent, selectItemId }) => (
<SelectableListItem
key={frontComponent.id}
itemId={selectItemId}
onEnter={() => handleCreateFrontComponentWidget(frontComponent)}
>
<CommandMenuItem
Icon={IconApps}
label={frontComponent.name}
id={selectItemId}
onClick={() =>
handleCreateFrontComponentWidget(frontComponent)
}
/>
</SelectableListItem>
),
)}
</CommandGroup>
)}
</CommandMenuList>
);
};
@@ -0,0 +1,3 @@
export const getFrontComponentWidgetTypeSelectItemId = (
frontComponentId: string,
): string => `front-component-${frontComponentId}`;
@@ -0,0 +1,11 @@
import gql from 'graphql-tag';
export const FIND_MANY_FRONT_COMPONENTS = gql`
query FindManyFrontComponents {
frontComponents {
id
name
applicationId
}
}
`;
@@ -10,4 +10,8 @@ export const WIDGET_SIZES: Partial<Record<WidgetType, WidgetSizeConfig>> = {
default: { w: 4, h: 4 },
minimum: { w: 2, h: 2 },
},
[WidgetType.FRONT_COMPONENT]: {
default: { w: 8, h: 8 },
minimum: { w: 4, h: 4 },
},
};
@@ -0,0 +1,125 @@
import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultFrontComponentWidget } from '@/page-layout/utils/createDefaultFrontComponentWidget';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { WidgetType } from '~/generated/graphql';
export const useCreatePageLayoutFrontComponentWidget = (
pageLayoutIdFromProps?: string,
) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const activeTabId = useRecoilComponentValue(
activeTabIdComponentState,
getTabListInstanceIdFromPageLayoutId(pageLayoutId),
);
const pageLayoutCurrentLayoutsState = useRecoilComponentCallbackState(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useRecoilComponentCallbackState(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const pageLayoutDraftState = useRecoilComponentCallbackState(
pageLayoutDraftComponentState,
pageLayoutId,
);
const createPageLayoutFrontComponentWidget = useRecoilCallback(
({ snapshot, set }) =>
(title: string, frontComponentId: string): PageLayoutWidget => {
const allTabLayouts = snapshot
.getLoadable(pageLayoutCurrentLayoutsState)
.getValue();
const pageLayoutDraggedArea = snapshot
.getLoadable(pageLayoutDraggedAreaState)
.getValue();
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new front component widget',
);
}
const widgetId = uuidv4();
const frontComponentSize = WIDGET_SIZES[WidgetType.FRONT_COMPONENT]!;
const defaultSize = frontComponentSize.default;
const minimumSize = frontComponentSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
minimumSize,
);
const newWidget = createDefaultFrontComponentWidget(
widgetId,
activeTabId,
title,
frontComponentId,
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
set(pageLayoutCurrentLayoutsState, updatedLayouts);
set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
set(pageLayoutDraggedAreaState, null);
return newWidget;
},
[
activeTabId,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutDraggedAreaState,
],
);
return { createPageLayoutFrontComponentWidget };
};
@@ -0,0 +1,32 @@
import {
type GridPosition,
WidgetConfigurationType,
WidgetType,
} from '~/generated/graphql';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
export const createDefaultFrontComponentWidget = (
id: string,
pageLayoutTabId: string,
title: string,
frontComponentId: string,
gridPosition: GridPosition,
): PageLayoutWidget => {
return {
__typename: 'PageLayoutWidget',
id,
pageLayoutTabId,
title,
type: WidgetType.FRONT_COMPONENT,
configuration: {
__typename: 'FrontComponentConfiguration',
configurationType: WidgetConfigurationType.FRONT_COMPONENT,
frontComponentId,
},
gridPosition,
objectMetadataId: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
};
@@ -1,10 +1,20 @@
import styled from '@emotion/styled';
import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
const StyledContainer = styled.div<{ isInEditMode: boolean }>`
height: 100%;
overflow: auto;
pointer-events: ${({ isInEditMode }) => (isInEditMode ? 'none' : 'auto')};
width: 100%;
`;
const FrontComponentRenderer = lazy(() =>
import('@/front-components/components/FrontComponentRenderer').then(
@@ -19,6 +29,10 @@ type FrontComponentWidgetRendererProps = {
export const FrontComponentWidgetRenderer = ({
widget,
}: FrontComponentWidgetRendererProps) => {
const isPageLayoutInEditMode = useRecoilComponentValue(
isPageLayoutInEditModeComponentState,
);
const configuration = widget.configuration;
if (
@@ -31,8 +45,10 @@ export const FrontComponentWidgetRenderer = ({
const frontComponentId = configuration.frontComponentId;
return (
<Suspense fallback={null}>
<FrontComponentRenderer frontComponentId={frontComponentId} />
</Suspense>
<StyledContainer isInEditMode={isPageLayoutInEditMode}>
<Suspense fallback={null}>
<FrontComponentRenderer frontComponentId={frontComponentId} />
</Suspense>
</StyledContainer>
);
};