[FRONT COMPONENTS] Frontend component widget creation (#17653)
closes https://github.com/twentyhq/core-team-issues/issues/2182 https://github.com/user-attachments/assets/babf154c-9cda-40ff-b2e8-5053e447c35f
This commit is contained in:
+87
-7
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const getFrontComponentWidgetTypeSelectItemId = (
|
||||
frontComponentId: string,
|
||||
): string => `front-component-${frontComponentId}`;
|
||||
+11
@@ -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 },
|
||||
},
|
||||
};
|
||||
|
||||
+125
@@ -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 };
|
||||
};
|
||||
+32
@@ -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,
|
||||
};
|
||||
};
|
||||
+19
-3
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user