diff --git a/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts b/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts index 54381e385d..4ec05e0f94 100644 --- a/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts +++ b/packages/twenty-front/src/modules/command-menu/components/hooks/usePageLayoutHeaderInfo.ts @@ -2,6 +2,7 @@ import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constan import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget'; import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useTheme } from '@emotion/react'; import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; @@ -11,7 +12,6 @@ import { IconPlus, type IconComponent, } from 'twenty-ui/display'; -import { type PageLayoutWidget } from '~/generated/graphql'; type PageLayoutHeaderInfo = { headerIcon: IconComponent | undefined; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx index 3e31e844ca..c973599086 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/ChartSettings.tsx @@ -10,7 +10,6 @@ import { useChartSettingsValues } from '@/command-menu/pages/page-layout/hooks/u import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig'; import { useGetConfigToUpdateAfterGraphTypeChange } from '@/command-menu/pages/page-layout/hooks/useUpdateGraphTypeConfig'; -import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; import { CHART_CONFIGURATION_SETTING_IDS } from '@/command-menu/pages/page-layout/types/ChartConfigurationSettingIds'; import { shouldHideChartSetting } from '@/command-menu/pages/page-layout/utils/shouldHideChartSetting'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; @@ -20,7 +19,9 @@ import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { isFieldMetadataDateKind } from 'twenty-shared/utils'; -import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; +import { assertChartWidgetOrThrow } from '@/command-menu/pages/page-layout/utils/assertChartWidgetOrThrow'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { GraphType } from '~/generated/graphql'; const StyledCommandMenuContainer = styled.div` display: flex; @@ -36,11 +37,9 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => { useUpdateCurrentWidgetConfig(pageLayoutId); const { objectMetadataItems } = useObjectMetadataItems(); - if (widget.configuration?.__typename === 'IframeConfiguration') { - throw new Error(t`IframeConfiguration is not supported`); - } + assertChartWidgetOrThrow(widget); - const configuration = widget.configuration as ChartConfiguration; + const configuration = widget.configuration; const currentGraphType = configuration?.graphType; const { getChartSettingsValues } = useChartSettingsValues({ diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx index 956483e781..8d99583633 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings.tsx @@ -26,9 +26,11 @@ export const CommandMenuPageLayoutIframeSettings = () => { throw new Error('Widget ID must be present while editing the widget'); } + const widgetConfiguration = widgetInEditMode.configuration; + const configUrl = - widgetInEditMode.configuration && 'url' in widgetInEditMode.configuration - ? widgetInEditMode.configuration.url + widgetConfiguration && 'url' in widgetConfiguration + ? widgetConfiguration.url : null; const [url, setUrl] = useState( @@ -56,15 +58,18 @@ export const CommandMenuPageLayoutIframeSettings = () => { const handleUrlChange = (value: string) => { setUrl(value); - if (validateUrl(value)) { - const trimmedValue = value.trim(); - updatePageLayoutWidget(widgetInEditMode.id, { - configuration: { - ...widgetInEditMode.configuration, - url: trimmedValue || null, - }, - }); + if (!validateUrl(value)) { + return; } + + const trimmedValue = value.trim(); + + updatePageLayoutWidget(widgetInEditMode.id, { + configuration: { + __typename: 'IframeConfiguration', + url: isNonEmptyString(trimmedValue) ? trimmedValue : null, + }, + }); }; return ( diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig.ts index 6b5e065d2f..4c20fc28f6 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig.ts @@ -1,9 +1,9 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import type { PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; 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 type { PageLayoutWidget } from '~/generated/graphql'; export const useUpdateCurrentWidgetConfig = (pageLayoutIdFromProps: string) => { const pageLayoutDraftCallbackState = useRecoilComponentCallbackState( diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateGraphTypeConfig.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateGraphTypeConfig.ts index ae9e0399ee..cea3bd8578 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateGraphTypeConfig.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/useUpdateGraphTypeConfig.ts @@ -7,13 +7,14 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId'; import { updateWidgetMinimumSizeForGraphType } from '@/page-layout/utils/updateWidgetMinimumSizeForGraphType'; import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; +import { GraphType } from '~/generated/graphql'; export const useGetConfigToUpdateAfterGraphTypeChange = ({ pageLayoutId, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartWidget.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartWidget.ts index e8e087852b..f84c9d6b74 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartWidget.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/ChartWidget.ts @@ -1,7 +1,8 @@ import { type ChartFilters } from '@/command-menu/pages/page-layout/types/ChartFilters'; import { type ChartWidgetConfiguration } from '@/command-menu/pages/page-layout/types/ChartWidgetConfiguration'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { type ModifiedProperties } from 'twenty-shared/types'; -import { type PageLayoutWidget, type WidgetType } from '~/generated/graphql'; +import { type WidgetType } from '~/generated/graphql'; export type ChartWidget = ModifiedProperties< PageLayoutWidget, diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/assertChartWidgetOrThrow.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/assertChartWidgetOrThrow.ts new file mode 100644 index 0000000000..01f5eeca27 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/assertChartWidgetOrThrow.ts @@ -0,0 +1,44 @@ +import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { isNonEmptyString } from '@sniptt/guards'; +import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; + +type AssertChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => asserts widget is PageLayoutWidget & { + objectMetadataId: string; + configuration: ChartConfiguration; +}; + +const VALID_CHART_TYPES: ReadonlyArray = [ + 'BarChartConfiguration', + 'LineChartConfiguration', + 'PieChartConfiguration', + 'AggregateChartConfiguration', + 'GaugeChartConfiguration', +] as const; + +export const assertChartWidgetOrThrow: AssertChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => { + assertIsDefinedOrThrow( + widget.objectMetadataId, + new Error('Widget objectMetadataId is required'), + ); + + assertIsDefinedOrThrow( + widget.configuration, + new Error('Widget configuration is required'), + ); + + if ( + !isNonEmptyString(widget.configuration.__typename) || + !VALID_CHART_TYPES.includes( + widget.configuration.__typename as ChartConfiguration['__typename'], + ) + ) { + throw new Error( + `Expected chart configuration but got ${widget.configuration.__typename}`, + ); + } +}; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutCanvasViewer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutCanvasViewer.tsx index 0840a58c76..7e8be5375d 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutCanvasViewer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutCanvasViewer.tsx @@ -1,7 +1,7 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; import styled from '@emotion/styled'; import { isDefined } from 'twenty-shared/utils'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledCanvasContainer = styled.div` display: grid; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx index b6f9f22997..e633420aaa 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListEditor.tsx @@ -1,4 +1,5 @@ import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { WidgetRenderer } from '@/page-layout/widgets/components/WidgetRenderer'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import styled from '@emotion/styled'; @@ -9,7 +10,6 @@ import { type DropResult, } from '@hello-pangea/dnd'; import { useId } from 'react'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledVerticalListContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListViewer.tsx b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListViewer.tsx index 4b6589d66f..96ee60aa8e 100644 --- a/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListViewer.tsx +++ b/packages/twenty-front/src/modules/page-layout/components/PageLayoutVerticalListViewer.tsx @@ -1,6 +1,6 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; 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; diff --git a/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts b/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts index 0e7ab61295..c0e04d5759 100644 --- a/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts +++ b/packages/twenty-front/src/modules/page-layout/constants/DefaultNoteRecordPageLayout.ts @@ -38,6 +38,9 @@ export const DEFAULT_NOTE_RECORD_PAGE_LAYOUT: PageLayout = { rowSpan: 12, columnSpan: 12, }, + // Note: Configuration is null by default. For testing purposes, + // use useTempNoteFieldsConfiguration() hook at runtime to get + // a configuration with actual field metadata IDs from the backend. configuration: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts index b41740e0a7..ad29f1b73e 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutGraphWidget.ts @@ -4,6 +4,7 @@ import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pag import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState'; import { type GraphWidgetFieldSelection } from '@/page-layout/types/GraphWidgetFieldSelection'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; import { createDefaultGraphWidget } from '@/page-layout/utils/createDefaultGraphWidget'; import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition'; @@ -18,7 +19,7 @@ import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 as uuidv4 } from 'uuid'; import { type GraphType } from '~/generated-metadata/graphql'; -import { WidgetType, type PageLayoutWidget } from '~/generated/graphql'; +import { WidgetType } from '~/generated/graphql'; export const useCreatePageLayoutGraphWidget = ( pageLayoutIdFromProps?: string, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts index 33c3c80d4b..1eed19d7c5 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useCreatePageLayoutIframeWidget.ts @@ -3,6 +3,7 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context 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 { createDefaultIframeWidget } from '@/page-layout/utils/createDefaultIframeWidget'; import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition'; @@ -15,7 +16,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 as uuidv4 } from 'uuid'; -import { type PageLayoutWidget, WidgetType } from '~/generated/graphql'; +import { WidgetType } from '~/generated/graphql'; export const useCreatePageLayoutIframeWidget = ( pageLayoutIdFromProps?: string, diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts index 9a5020a55c..33900f2443 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useDuplicatePageLayoutWidget.ts @@ -2,6 +2,7 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/context import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab'; import { getScrollWrapperInstanceIdFromPageLayoutId } from '@/page-layout/utils/getScrollWrapperInstanceIdFromPageLayoutId'; import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts'; @@ -12,7 +13,6 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { v4 as uuidv4 } from 'uuid'; -import { type PageLayoutWidget } from '~/generated/graphql'; import { generateDuplicatedTimestamps } from '../utils/generateDuplicatedTimestamps'; import { getDuplicatedTitle } from '../utils/getDuplicatedTitle'; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useTemporaryFieldsConfiguration.ts b/packages/twenty-front/src/modules/page-layout/hooks/useTemporaryFieldsConfiguration.ts new file mode 100644 index 0000000000..78714c2f15 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/hooks/useTemporaryFieldsConfiguration.ts @@ -0,0 +1,49 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; +import { useMemo } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const useTemporaryFieldsConfiguration = ( + objectNameSingular: string, +): FieldsConfiguration | null => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const configuration = useMemo(() => { + if (!isDefined(objectMetadataItem)) { + return null; + } + + const fieldsToDisplay = objectMetadataItem.fields.filter( + (field) => + field.type !== FieldMetadataType.RELATION && + field.type !== FieldMetadataType.MORPH_RELATION && + field.type !== FieldMetadataType.RICH_TEXT_V2, + ); + + const fields = fieldsToDisplay.map((field, index) => ({ + fieldMetadataId: field.id, + position: index, + })); + + if (fields.length === 0) { + return null; + } + + return { + __typename: 'FieldsConfiguration', + sections: [ + { + id: `${objectNameSingular}-section-general`, + title: 'General', + position: 0, + fields, + }, + ], + }; + }, [objectMetadataItem, objectNameSingular]); + + return configuration; +}; diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts index 4f9750bcfc..acf0d1bccd 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useUpdatePageLayoutWidget.ts @@ -1,6 +1,6 @@ import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext'; import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; -import { type PageLayoutWidget } from '~/generated/graphql'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState'; import { useRecoilCallback } from 'recoil'; diff --git a/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts new file mode 100644 index 0000000000..c5396e96b0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts @@ -0,0 +1,19 @@ +import { type RulesLogic } from 'json-logic-js'; + +export type FieldsConfigurationFieldItem = { + fieldMetadataId: string; + position: number; + conditionalDisplay?: RulesLogic; +}; + +export type FieldsConfigurationSection = { + id: string; + title: string; + position: number; + fields: FieldsConfigurationFieldItem[]; +}; + +export type FieldsConfiguration = { + __typename: 'FieldsConfiguration'; + sections: FieldsConfigurationSection[]; +}; diff --git a/packages/twenty-front/src/modules/page-layout/types/GridLayoutItem.ts b/packages/twenty-front/src/modules/page-layout/types/GridLayoutItem.ts index 5b2d8d1a79..e76a368d0f 100644 --- a/packages/twenty-front/src/modules/page-layout/types/GridLayoutItem.ts +++ b/packages/twenty-front/src/modules/page-layout/types/GridLayoutItem.ts @@ -1,4 +1,4 @@ -import { type PageLayoutWidget } from '~/generated/graphql'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; export type GridLayoutWidgetItem = { id: string; diff --git a/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts index 4334ac3e96..a8d36d321a 100644 --- a/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayoutTab.ts @@ -1,19 +1,9 @@ import { type PageLayoutTabLayoutMode } from '@/page-layout/types/PageLayoutTabLayoutMode'; -import { type RulesLogic } from 'json-logic-js'; -import { type ModifiedProperties, type Nullable } from 'twenty-shared/types'; -import { - type PageLayoutTab as PageLayoutTabGenerated, - type PageLayoutWidget, -} from '~/generated/graphql'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { type PageLayoutTab as PageLayoutTabGenerated } from '~/generated/graphql'; export type PageLayoutTab = Omit & { - widgets: ModifiedProperties< - PageLayoutWidget, - { - objectMetadataId?: Nullable; - conditionalDisplay?: RulesLogic; - } - >[]; + widgets: PageLayoutWidget[]; /** * Only available behind IS_RECORD_PAGE_LAYOUT_ENABLED for now. */ diff --git a/packages/twenty-front/src/modules/page-layout/types/PageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/types/PageLayoutWidget.ts new file mode 100644 index 0000000000..1dec182eb1 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/types/PageLayoutWidget.ts @@ -0,0 +1,16 @@ +import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; +import { type RulesLogic } from 'json-logic-js'; +import { type Nullable } from 'twenty-shared/types'; +import { + type PageLayoutWidget as PageLayoutWidgetGenerated, + type WidgetConfiguration, +} from '~/generated/graphql'; + +export type PageLayoutWidget = Omit< + PageLayoutWidgetGenerated, + 'objectMetadataId' | 'configuration' +> & { + objectMetadataId?: Nullable; + conditionalDisplay?: RulesLogic; + configuration?: WidgetConfiguration | FieldsConfiguration | null; +}; diff --git a/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts b/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts index f2236527bb..1c71806a05 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/addWidgetToTab.ts @@ -1,5 +1,5 @@ import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab'; -import { type PageLayoutWidget } from '~/generated/graphql'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; export const addWidgetToTab = ( tabs: PageLayoutTab[], diff --git a/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts b/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts index ddd053b04b..90ea442d1f 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/convertLayoutsToWidgets.ts @@ -1,4 +1,4 @@ -import { type PageLayoutWidget } from '~/generated/graphql'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { type Layouts } from 'react-grid-layout'; export const convertLayoutsToWidgets = ( diff --git a/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts b/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts index 2f2de974ad..7615b73a8a 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/extractFieldMetadataIdsFromWidget.ts @@ -1,8 +1,6 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { isDefined } from 'twenty-shared/utils'; -import { - type PageLayoutWidget, - WidgetType, -} from '~/generated-metadata/graphql'; +import { WidgetType } from '~/generated-metadata/graphql'; export const extractFieldMetadataIdsFromWidget = ( widget: PageLayoutWidget, diff --git a/packages/twenty-front/src/modules/page-layout/utils/prepareGridLayoutItemsWithPlaceholders.ts b/packages/twenty-front/src/modules/page-layout/utils/prepareGridLayoutItemsWithPlaceholders.ts index 641fcc4182..2c3aaa52a5 100644 --- a/packages/twenty-front/src/modules/page-layout/utils/prepareGridLayoutItemsWithPlaceholders.ts +++ b/packages/twenty-front/src/modules/page-layout/utils/prepareGridLayoutItemsWithPlaceholders.ts @@ -1,7 +1,7 @@ import { PENDING_WIDGET_PLACEHOLDER_LAYOUT_KEY } from '@/page-layout/constants/PendingWidgetPlaceholderLayoutKey'; import { type GridLayoutItem } from '@/page-layout/types/GridLayoutItem'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { isDefined } from 'twenty-shared/utils'; -import { type PageLayoutWidget } from '~/generated/graphql'; export const prepareGridLayoutItemsWithPlaceholders = ( widgets: PageLayoutWidget[] | undefined, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/calendar/components/CalendarWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/calendar/components/CalendarWidget.tsx index 14ac302aa9..c8607c5d87 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/calendar/components/CalendarWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/calendar/components/CalendarWidget.tsx @@ -1,8 +1,8 @@ import { CalendarEventsCard } from '@/activities/calendar/components/CalendarEventsCard'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx index 2d13b3953e..68d0737606 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetContentRenderer.tsx @@ -1,3 +1,4 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { CalendarWidget } from '@/page-layout/widgets/calendar/components/CalendarWidget'; import { EmailWidget } from '@/page-layout/widgets/emails/components/EmailWidget'; import { FieldRichTextWidget } from '@/page-layout/widgets/field-rich-text/components/FieldRichTextWidget'; @@ -11,10 +12,7 @@ import { TimelineWidget } from '@/page-layout/widgets/timeline/components/Timeli import { WorkflowRunWidget } from '@/page-layout/widgets/workflow/components/WorkflowRunWidget'; import { WorkflowVersionWidget } from '@/page-layout/widgets/workflow/components/WorkflowVersionWidget'; import { WorkflowWidget } from '@/page-layout/widgets/workflow/components/WorkflowWidget'; -import { - type PageLayoutWidget, - WidgetType, -} from '~/generated-metadata/graphql'; +import { WidgetType } from '~/generated-metadata/graphql'; type WidgetContentRendererProps = { widget: PageLayoutWidget; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx index 4c6765e38e..20115d03b8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/components/WidgetRenderer.tsx @@ -6,6 +6,7 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPag import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; import { pageLayoutResizingWidgetIdComponentState } from '@/page-layout/states/pageLayoutResizingWidgetIdComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { PageLayoutWidgetForbiddenDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetForbiddenDisplay'; import { WidgetContentRenderer } from '@/page-layout/widgets/components/WidgetContentRenderer'; import { useIsInPinnedTab } from '@/page-layout/widgets/hooks/useIsInPinnedTab'; @@ -20,7 +21,6 @@ import { useSetRecoilComponentFamilyState } from '@/ui/utilities/state/component import { useTheme } from '@emotion/react'; import { type MouseEvent } from 'react'; import { IconLock } from 'twenty-ui/display'; -import { type PageLayoutWidget } from '~/generated/graphql'; type WidgetRendererProps = { widget: PageLayoutWidget; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/emails/components/EmailWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/emails/components/EmailWidget.tsx index 936131d868..36d336beca 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/emails/components/EmailWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/emails/components/EmailWidget.tsx @@ -1,8 +1,8 @@ import { EmailsCard } from '@/activities/emails/components/EmailsCard'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/field-rich-text/components/FieldRichTextWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/field-rich-text/components/FieldRichTextWidget.tsx index 0bd40e4676..21fca49cc2 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/field-rich-text/components/FieldRichTextWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/field-rich-text/components/FieldRichTextWidget.tsx @@ -1,6 +1,6 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { FieldRichTextCard } from '@/ui/layout/show-page/components/FieldRichTextCard'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx index 3a0c0d1df2..3e27152972 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidget.tsx @@ -1,9 +1,38 @@ -import { RecordFieldList } from '@/object-record/record-field-list/components/RecordFieldList'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly'; +import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly'; +import { RecordDetailSectionContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailSectionContainer'; +import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext'; +import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState'; +import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; +import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; +import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; +import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader'; +import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions'; +import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; +import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { FieldsWidgetCellEditModePortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal'; +import { FieldsWidgetCellHoveredPortal } from '@/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal'; +import { useFieldsWidgetSectionsWithIndices } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices'; +import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord'; import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; +import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; +import { t } from '@lingui/core/macro'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, +} from 'twenty-ui/layout'; const StyledContainer = styled.div` display: flex; @@ -19,17 +48,148 @@ export const FieldsWidget = ({ widget: _widget }: FieldsWidgetProps) => { const targetRecord = useTargetRecord(); const { isInRightDrawer } = useLayoutRenderingContext(); + const instanceId = `fields-widget-${targetRecord.id}-${isInRightDrawer ? 'right-drawer' : ''}`; + + const { recordLoading, isPrefetchLoading } = useRecordShowContainerData({ + objectRecordId: targetRecord.id, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: targetRecord.targetObjectNameSingular, + }); + + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + + const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ + objectNameSingular: targetRecord.targetObjectNameSingular, + objectRecordId: targetRecord.id, + }); + + const isRecordReadOnly = useIsRecordReadOnly({ + recordId: targetRecord.id, + objectMetadataId: objectMetadataItem.id, + }); + + const setRecordFieldListHoverPosition = useSetRecoilComponentState( + recordFieldListHoverPositionComponentState, + instanceId, + ); + + const { sectionsWithFieldIndices } = useFieldsWidgetSectionsWithIndices( + targetRecord.targetObjectNameSingular, + ); + + if (sectionsWithFieldIndices.length === 0) { + return ( + + + + + + + {t`No fields to display`} + + + {t`Configure this widget to display fields`} + + + + + + ); + } + return ( - - - + + {sectionsWithFieldIndices.map((section) => ( + + + {isPrefetchLoading ? ( + + ) : ( + <> + {section.fields.map( + ({ field: fieldMetadataItem, globalIndex }) => { + return ( + + setRecordFieldListHoverPosition(globalIndex), + anchorId: `${getRecordFieldInputInstanceId({ + recordId: targetRecord.id, + fieldName: fieldMetadataItem.name, + prefix: instanceId, + })}`, + }} + > + + + + + ); + }, + )} + + )} + + + ))} + + - - + + + ); }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal.tsx new file mode 100644 index 0000000000..e1e2cf32e8 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellEditModePortal.tsx @@ -0,0 +1,56 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider'; +import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext'; +import { recordFieldListCellEditModePositionComponentState } from '@/object-record/record-field-list/states/recordFieldListCellEditModePositionComponentState'; +import { FieldInput } from '@/object-record/record-field/ui/components/FieldInput'; +import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal'; +import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode'; +import { useFieldsWidgetFlattenedFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { isDefined } from 'twenty-shared/utils'; + +type FieldsWidgetCellEditModePortalProps = { + objectMetadataItem: ObjectMetadataItem; + recordId: string; +}; + +export const FieldsWidgetCellEditModePortal = ({ + objectMetadataItem, + recordId, +}: FieldsWidgetCellEditModePortalProps) => { + const instanceId = useAvailableComponentInstanceIdOrThrow( + RecordFieldListComponentInstanceContext, + ); + + const editModePosition = useRecoilComponentValue( + recordFieldListCellEditModePositionComponentState, + ); + + const { flattenedFieldMetadataItems } = useFieldsWidgetFlattenedFields( + objectMetadataItem.nameSingular, + ); + + const editedFieldMetadataItem = isDefined(editModePosition) + ? flattenedFieldMetadataItems.at(editModePosition) + : undefined; + + if (!isDefined(editModePosition) || !isDefined(editedFieldMetadataItem)) { + return null; + } + + return ( + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal.tsx new file mode 100644 index 0000000000..056735d25b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsWidgetCellHoveredPortal.tsx @@ -0,0 +1,53 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordFieldListCellHoveredPortalContent } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListCellHoveredPortalContent'; +import { RecordFieldListInputContextProvider } from '@/object-record/record-field-list/anchored-portal/components/RecordFieldListInputContextProvider'; +import { RecordFieldListComponentInstanceContext } from '@/object-record/record-field-list/states/contexts/RecordFieldListComponentInstanceContext'; +import { recordFieldListHoverPositionComponentState } from '@/object-record/record-field-list/states/recordFieldListHoverPositionComponentState'; +import { RecordInlineCellAnchoredPortal } from '@/object-record/record-inline-cell/components/RecordInlineCellAnchoredPortal'; +import { useFieldsWidgetFlattenedFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { isDefined } from 'twenty-shared/utils'; + +type FieldsWidgetCellHoveredPortalProps = { + objectMetadataItem: ObjectMetadataItem; + recordId: string; +}; + +export const FieldsWidgetCellHoveredPortal = ({ + objectMetadataItem, + recordId, +}: FieldsWidgetCellHoveredPortalProps) => { + const instanceId = useAvailableComponentInstanceIdOrThrow( + RecordFieldListComponentInstanceContext, + ); + + const hoverPosition = useRecoilComponentValue( + recordFieldListHoverPositionComponentState, + ); + + const { flattenedFieldMetadataItems } = useFieldsWidgetFlattenedFields( + objectMetadataItem.nameSingular, + ); + + const hoveredFieldMetadataItem = isDefined(hoverPosition) + ? flattenedFieldMetadataItems.at(hoverPosition) + : undefined; + + if (!isDefined(hoverPosition) || !isDefined(hoveredFieldMetadataItem)) { + return null; + } + + return ( + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems.ts new file mode 100644 index 0000000000..d3a30f7cd0 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems.ts @@ -0,0 +1,29 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; +import { FieldMetadataType } from 'twenty-shared/types'; + +type UseFieldsWidgetFieldMetadataItemsProps = { + objectNameSingular: string; +}; + +export const useFieldsWidgetFieldMetadataItems = ({ + objectNameSingular, +}: UseFieldsWidgetFieldMetadataItemsProps): FieldMetadataItem[] => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { objectMetadataItems } = useObjectMetadataItems(); + + const fieldMetadataItems = objectMetadataItem.readableFields.filter( + (fieldMetadataItem) => + isFieldCellSupported(fieldMetadataItem, objectMetadataItems) && + fieldMetadataItem.type !== FieldMetadataType.RELATION && + fieldMetadataItem.type !== FieldMetadataType.MORPH_RELATION && + fieldMetadataItem.type !== FieldMetadataType.RICH_TEXT_V2, + ); + + return fieldMetadataItems; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields.ts new file mode 100644 index 0000000000..76475773b5 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetFlattenedFields.ts @@ -0,0 +1,12 @@ +import { useFieldsWidgetSectionsWithFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields'; + +export const useFieldsWidgetFlattenedFields = (objectNameSingular: string) => { + const { sectionsWithFields } = + useFieldsWidgetSectionsWithFields(objectNameSingular); + + const flattenedFieldMetadataItems = sectionsWithFields.flatMap( + (section) => section.fields, + ); + + return { flattenedFieldMetadataItems }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields.ts new file mode 100644 index 0000000000..d9d3c09419 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields.ts @@ -0,0 +1,31 @@ +import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration'; +import { buildWidgetVisibilityContext } from '@/page-layout/utils/buildWidgetVisibilityContext'; +import { useFieldsWidgetFieldMetadataItems } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetFieldMetadataItems'; +import { filterAndOrderFieldsFromConfiguration } from '@/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration'; +import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; + +export const useFieldsWidgetSectionsWithFields = ( + objectNameSingular: string, +) => { + const isMobile = useIsMobile(); + const { isInRightDrawer } = useLayoutRenderingContext(); + const fieldMetadataItems = useFieldsWidgetFieldMetadataItems({ + objectNameSingular, + }); + const temporaryConfiguration = + useTemporaryFieldsConfiguration(objectNameSingular); + + const context = buildWidgetVisibilityContext({ isMobile, isInRightDrawer }); + + const sectionsWithFields = filterAndOrderFieldsFromConfiguration({ + configuration: temporaryConfiguration ?? { + __typename: 'FieldsConfiguration', + sections: [], + }, + availableFieldMetadataItems: fieldMetadataItems, + context, + }); + + return { sectionsWithFields }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices.ts new file mode 100644 index 0000000000..6322fc4e8b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithIndices.ts @@ -0,0 +1,26 @@ +import { useFieldsWidgetSectionsWithFields } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetSectionsWithFields'; + +export const useFieldsWidgetSectionsWithIndices = ( + objectNameSingular: string, +) => { + const { sectionsWithFields } = + useFieldsWidgetSectionsWithFields(objectNameSingular); + + const sectionsWithFieldIndices = sectionsWithFields.map( + (section, sectionIndex) => { + const startIndex = sectionsWithFields + .slice(0, sectionIndex) + .reduce((sum, s) => sum + s.fields.length, 0); + + return { + ...section, + fields: section.fields.map((field, fieldIndex) => ({ + field, + globalIndex: startIndex + fieldIndex, + })), + }; + }, + ); + + return { sectionsWithFieldIndices }; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration.ts b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration.ts new file mode 100644 index 0000000000..e838baba95 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/utils/filterAndOrderFieldsFromConfiguration.ts @@ -0,0 +1,76 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; +import { type WidgetVisibilityContext } from '@/page-layout/types/WidgetVisibilityContext'; +import { evaluateWidgetVisibility } from '@/page-layout/utils/evaluateWidgetVisibility'; +import { isDefined } from 'twenty-shared/utils'; + +export type FieldsConfigurationSectionWithFields = { + id: string; + title: string; + position: number; + fields: FieldMetadataItem[]; +}; + +type FilterAndOrderFieldsFromConfigurationParams = { + configuration: FieldsConfiguration; + availableFieldMetadataItems: FieldMetadataItem[]; + context: WidgetVisibilityContext; +}; + +export const filterAndOrderFieldsFromConfiguration = ({ + configuration, + availableFieldMetadataItems, + context, +}: FilterAndOrderFieldsFromConfigurationParams): FieldsConfigurationSectionWithFields[] => { + const fieldMetadataItemsMap = new Map( + availableFieldMetadataItems.map((field) => [field.id, field]), + ); + + const sortedSections = [...configuration.sections].sort( + (a, b) => a.position - b.position, + ); + + const sectionsWithFields = sortedSections + .map((section) => { + const sortedFields = [...section.fields].sort( + (a, b) => a.position - b.position, + ); + + const visibleFields = sortedFields + .map((fieldConfig) => { + const fieldMetadataItem = fieldMetadataItemsMap.get( + fieldConfig.fieldMetadataId, + ); + + if (!isDefined(fieldMetadataItem)) { + return null; + } + + const isVisible = evaluateWidgetVisibility({ + conditionalDisplay: fieldConfig.conditionalDisplay, + context, + }); + + if (!isVisible) { + return null; + } + + return fieldMetadataItem; + }) + .filter(isDefined); + + if (visibleFields.length === 0) { + return null; + } + + return { + id: section.id, + title: section.title, + position: section.position, + fields: visibleFields, + }; + }) + .filter(isDefined); + + return sectionsWithFields; +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/files/components/FileWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/files/components/FileWidget.tsx index 0940fca7e7..eb08134f4b 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/files/components/FileWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/files/components/FileWidget.tsx @@ -1,8 +1,8 @@ import { FilesCard } from '@/activities/files/components/FilesCard'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx index cd87c59112..fce44edd1d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidget.tsx @@ -1,4 +1,5 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { getDefaultWidgetData } from '@/page-layout/utils/getDefaultWidgetData'; import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay'; import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; @@ -8,7 +9,7 @@ import { GraphWidgetLineChartRenderer } from '@/page-layout/widgets/graph/graphW import { GraphWidgetPieChartRenderer } from '@/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer'; import { areChartConfigurationFieldsValidForQuery } from '@/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery'; import { lazy, Suspense } from 'react'; -import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; +import { GraphType } from '~/generated/graphql'; const GraphWidgetGaugeChart = lazy(() => import( diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx index e9ddb1ba8b..636017f5d7 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/components/GraphWidgetRenderer.tsx @@ -1,8 +1,9 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay'; import { GraphWidget } from '@/page-layout/widgets/graph/components/GraphWidget'; import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext'; import { isDefined } from 'twenty-shared/utils'; -import { GraphType, type PageLayoutWidget } from '~/generated/graphql'; +import { GraphType } from '~/generated/graphql'; type GraphWidgetRendererProps = { widget: PageLayoutWidget; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetAggregateChart/components/GraphWidgetAggregateChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetAggregateChart/components/GraphWidgetAggregateChartRenderer.tsx index 4acde13449..ac9f70844e 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetAggregateChart/components/GraphWidgetAggregateChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetAggregateChart/components/GraphWidgetAggregateChartRenderer.tsx @@ -1,10 +1,8 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; import { useGraphWidgetAggregateQuery } from '@/page-layout/widgets/graph/hooks/useGraphWidgetAggregateQuery'; +import { assertAggregateChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertAggregateChartWidget'; import { lazy, Suspense } from 'react'; -import { - type AggregateChartConfiguration, - type PageLayoutWidget, -} from '~/generated/graphql'; const GraphWidgetAggregateChart = lazy(() => import( @@ -19,11 +17,11 @@ export const GraphWidgetAggregateChartRenderer = ({ }: { widget: PageLayoutWidget; }) => { - const configuration = widget.configuration as AggregateChartConfiguration; + assertAggregateChartWidgetOrThrow(widget); const { value, loading } = useGraphWidgetAggregateQuery({ objectMetadataItemId: widget.objectMetadataId, - configuration, + configuration: widget.configuration, }); if (loading) { @@ -34,8 +32,8 @@ export const GraphWidgetAggregateChartRenderer = ({ }> ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx index 6685afd390..c2f0bbbac1 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetBarChart/components/GraphWidgetBarChartRenderer.tsx @@ -1,8 +1,10 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect'; import { useGraphBarChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetBarChart/hooks/useGraphBarChartWidgetData'; import { getEffectiveGroupMode } from '@/page-layout/widgets/graph/graphWidgetBarChart/utils/getEffectiveGroupMode'; +import { assertBarChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertBarChartWidget'; import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams'; import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -13,10 +15,6 @@ import { useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import { AppPath } from 'twenty-shared/types'; import { getAppPath, isDefined } from 'twenty-shared/utils'; -import { - type BarChartConfiguration, - type PageLayoutWidget, -} from '~/generated/graphql'; const GraphWidgetBarChart = lazy(() => import( @@ -31,6 +29,8 @@ export const GraphWidgetBarChartRenderer = ({ }: { widget: PageLayoutWidget; }) => { + assertBarChartWidgetOrThrow(widget); + const { data, indexBy, @@ -47,11 +47,11 @@ export const GraphWidgetBarChartRenderer = ({ objectMetadataItem, } = useGraphBarChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, - configuration: widget.configuration as BarChartConfiguration, + configuration: widget.configuration, }); const navigate = useNavigate(); - const configuration = widget.configuration as BarChartConfiguration; + const configuration = widget.configuration; const isPageLayoutInEditMode = useRecoilComponentValue( isPageLayoutInEditModeComponentState, ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx index 016cdf317c..fed5f1e756 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetLineChart/components/GraphWidgetLineChartRenderer.tsx @@ -1,9 +1,11 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect'; import { LINE_CHART_IS_STACKED_DEFAULT } from '@/page-layout/widgets/graph/graphWidgetLineChart/constants/LineChartIsStackedDefault'; import { useGraphLineChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetLineChart/hooks/useGraphLineChartWidgetData'; import { type LineChartDataPoint } from '@/page-layout/widgets/graph/graphWidgetLineChart/types/LineChartDataPoint'; +import { assertLineChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertLineChartWidget'; import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams'; import { generateChartAggregateFilterKey } from '@/page-layout/widgets/graph/utils/generateChartAggregateFilterKey'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -14,10 +16,6 @@ import { useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import { AppPath } from 'twenty-shared/types'; import { getAppPath, isDefined } from 'twenty-shared/utils'; -import { - type LineChartConfiguration, - type PageLayoutWidget, -} from '~/generated/graphql'; const GraphWidgetLineChart = lazy(() => import( @@ -32,6 +30,8 @@ export const GraphWidgetLineChartRenderer = ({ }: { widget: PageLayoutWidget; }) => { + assertLineChartWidgetOrThrow(widget); + const { series, xAxisLabel, @@ -44,11 +44,11 @@ export const GraphWidgetLineChartRenderer = ({ objectMetadataItem, } = useGraphLineChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, - configuration: widget.configuration as LineChartConfiguration, + configuration: widget.configuration, }); const navigate = useNavigate(); - const configuration = widget.configuration as LineChartConfiguration; + const configuration = widget.configuration; const isPageLayoutInEditMode = useRecoilComponentValue( isPageLayoutInEditModeComponentState, ); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx index ef6925e637..453c8575fe 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/graphWidgetPieChart/components/GraphWidgetPieChartRenderer.tsx @@ -1,8 +1,10 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; import { GraphWidgetChartHasTooManyGroupsEffect } from '@/page-layout/widgets/graph/components/GraphWidgetChartHasTooManyGroupsEffect'; import { useGraphPieChartWidgetData } from '@/page-layout/widgets/graph/graphWidgetPieChart/hooks/useGraphPieChartWidgetData'; import { type PieChartDataItem } from '@/page-layout/widgets/graph/graphWidgetPieChart/types/PieChartDataItem'; +import { assertPieChartWidgetOrThrow } from '@/page-layout/widgets/graph/utils/assertPieChartWidget'; import { buildChartDrilldownQueryParams } from '@/page-layout/widgets/graph/utils/buildChartDrilldownQueryParams'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector'; @@ -11,10 +13,6 @@ import { useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import { AppPath } from 'twenty-shared/types'; import { getAppPath } from 'twenty-shared/utils'; -import { - type PageLayoutWidget, - type PieChartConfiguration, -} from '~/generated/graphql'; const GraphWidgetPieChart = lazy(() => import( @@ -29,6 +27,8 @@ export const GraphWidgetPieChartRenderer = ({ }: { widget: PageLayoutWidget; }) => { + assertPieChartWidgetOrThrow(widget); + const { data, loading, @@ -40,11 +40,10 @@ export const GraphWidgetPieChartRenderer = ({ formattedToRawLookup, } = useGraphPieChartWidgetData({ objectMetadataItemId: widget.objectMetadataId, - configuration: widget.configuration as PieChartConfiguration, + configuration: widget.configuration, }); const navigate = useNavigate(); - const configuration = widget.configuration as PieChartConfiguration; const isPageLayoutInEditMode = useRecoilComponentValue( isPageLayoutInEditModeComponentState, @@ -60,12 +59,12 @@ export const GraphWidgetPieChartRenderer = ({ const drilldownQueryParams = buildChartDrilldownQueryParams({ objectMetadataItem, - configuration, + configuration: widget.configuration, clickedData: { primaryBucketRawValue: rawValue, }, viewId: indexViewId, - timezone: configuration.timezone ?? undefined, + timezone: widget.configuration.timezone ?? undefined, }); const url = getAppPath( @@ -92,7 +91,7 @@ export const GraphWidgetPieChartRenderer = ({ data={data} id={widget.id} objectMetadataItemId={widget.objectMetadataId} - configuration={configuration} + configuration={widget.configuration} showLegend={showLegend} displayType="shortNumber" onSliceClick={isPageLayoutInEditMode ? undefined : handleSliceClick} diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts index c13f2196cb..da38ff2250 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/areChartConfigurationFieldsValidForQuery.ts @@ -1,10 +1,8 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { isDefined } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { - type PageLayoutWidget, - type RatioAggregateConfig, -} from '~/generated/graphql'; +import { type RatioAggregateConfig } from '~/generated/graphql'; const fieldExists = ( fieldId: string | undefined | null, diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertAggregateChartWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertAggregateChartWidget.ts new file mode 100644 index 0000000000..fb8f7a11c2 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertAggregateChartWidget.ts @@ -0,0 +1,24 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { type AggregateChartConfiguration } from '~/generated/graphql'; + +type AssertAggregateChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => asserts widget is PageLayoutWidget & { + objectMetadataId: string; + configuration: AggregateChartConfiguration; +}; + +export const assertAggregateChartWidgetOrThrow: AssertAggregateChartWidgetOrThrow = + (widget: PageLayoutWidget) => { + assertIsDefinedOrThrow( + widget.objectMetadataId, + new Error('Widget objectMetadataId is required'), + ); + + if (widget.configuration?.__typename !== 'AggregateChartConfiguration') { + throw new Error( + `Expected AggregateChartConfiguration but got ${widget.configuration?.__typename}`, + ); + } + }; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertBarChartWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertBarChartWidget.ts new file mode 100644 index 0000000000..b10b373077 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertBarChartWidget.ts @@ -0,0 +1,25 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { type BarChartConfiguration } from '~/generated/graphql'; + +type AssertBarChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => asserts widget is PageLayoutWidget & { + objectMetadataId: string; + configuration: BarChartConfiguration; +}; + +export const assertBarChartWidgetOrThrow: AssertBarChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => { + assertIsDefinedOrThrow( + widget.objectMetadataId, + new Error('Widget objectMetadataId is required'), + ); + + if (widget.configuration?.__typename !== 'BarChartConfiguration') { + throw new Error( + `Expected BarChartConfiguration but got ${widget.configuration?.__typename}`, + ); + } +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertLineChartWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertLineChartWidget.ts new file mode 100644 index 0000000000..3f09325cd9 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertLineChartWidget.ts @@ -0,0 +1,25 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { type LineChartConfiguration } from '~/generated/graphql'; + +type AssertLineChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => asserts widget is PageLayoutWidget & { + objectMetadataId: string; + configuration: LineChartConfiguration; +}; + +export const assertLineChartWidgetOrThrow: AssertLineChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => { + assertIsDefinedOrThrow( + widget.objectMetadataId, + new Error('Widget objectMetadataId is required'), + ); + + if (widget.configuration?.__typename !== 'LineChartConfiguration') { + throw new Error( + `Expected LineChartConfiguration but got ${widget.configuration?.__typename}`, + ); + } +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertPieChartWidget.ts b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertPieChartWidget.ts new file mode 100644 index 0000000000..95053b2457 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/graph/utils/assertPieChartWidget.ts @@ -0,0 +1,25 @@ +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; +import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { type PieChartConfiguration } from '~/generated/graphql'; + +type AssertPieChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => asserts widget is PageLayoutWidget & { + objectMetadataId: string; + configuration: PieChartConfiguration; +}; + +export const assertPieChartWidgetOrThrow: AssertPieChartWidgetOrThrow = ( + widget: PageLayoutWidget, +) => { + assertIsDefinedOrThrow( + widget.objectMetadataId, + new Error('Widget objectMetadataId is required'), + ); + + if (widget.configuration?.__typename !== 'PieChartConfiguration') { + throw new Error( + `Expected PieChartConfiguration but got ${widget.configuration?.__typename}`, + ); + } +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetPermissions.ts b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetPermissions.ts index 30b93c46af..646a77563d 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetPermissions.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/hooks/useWidgetPermissions.ts @@ -1,11 +1,11 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { checkFieldPermissions } from '@/page-layout/utils/checkFieldPermissions'; import { extractFieldMetadataIdsFromWidget } from '@/page-layout/utils/extractFieldMetadataIdsFromWidget'; import { type WidgetAccessDenialInfo } from '@/page-layout/widgets/types/WidgetAccessDenialInfo'; import { isDefined } from 'twenty-shared/utils'; -import { type PageLayoutWidget } from '~/generated-metadata/graphql'; export type UseWidgetPermissionsReturn = { hasAccess: boolean; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx index 47ce490be5..02f55ed278 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/iframe/components/IframeWidget.tsx @@ -1,11 +1,11 @@ import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { PageLayoutWidgetNoDataDisplay } from '@/page-layout/widgets/components/PageLayoutWidgetNoDataDisplay'; import { ChartSkeletonLoader } from '@/page-layout/widgets/graph/components/ChartSkeletonLoader'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import styled from '@emotion/styled'; import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; -import { type PageLayoutWidget } from '~/generated-metadata/graphql'; const StyledContainer = styled.div<{ $isEditMode: boolean }>` border-radius: ${({ theme }) => theme.border.radius.md}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/notes/components/NoteWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/notes/components/NoteWidget.tsx index 8f821f1154..adc179f0d8 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/notes/components/NoteWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/notes/components/NoteWidget.tsx @@ -1,8 +1,8 @@ import { NotesCard } from '@/activities/notes/components/NotesCard'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/tasks/components/TaskWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/tasks/components/TaskWidget.tsx index 8bc00a81ad..ddaa24d209 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/tasks/components/TaskWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/tasks/components/TaskWidget.tsx @@ -1,8 +1,8 @@ import { TasksCard } from '@/activities/tasks/components/TasksCard'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/timeline/components/TimelineWidget.tsx b/packages/twenty-front/src/modules/page-layout/widgets/timeline/components/TimelineWidget.tsx index 353c0fe757..74b873224b 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/timeline/components/TimelineWidget.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/timeline/components/TimelineWidget.tsx @@ -1,8 +1,8 @@ import { TimelineCard } from '@/activities/timeline-activities/components/TimelineCard'; +import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget'; import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext'; import { RightDrawerProvider } from '@/ui/layout/right-drawer/contexts/RightDrawerContext'; import styled from '@emotion/styled'; -import { type PageLayoutWidget } from '~/generated/graphql'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx index d1365bd69a..22d3d0d399 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx +++ b/packages/twenty-front/src/modules/page-layout/widgets/widget-card/components/WidgetCardHeader.tsx @@ -32,7 +32,7 @@ const StyledWidgetCardHeader = styled.div` const StyledTitleContainer = styled.div` color: ${({ theme }) => theme.font.color.primary}; flex: 1; - font-size: ${({ theme }) => theme.font.size.sm}; + font-size: ${({ theme }) => theme.font.size.md}; padding-inline: ${({ theme }) => theme.spacing(1)}; font-weight: ${({ theme }) => theme.font.weight.medium}; user-select: none;