diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuDashboardPageLayoutInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuDashboardPageLayoutInfo.tsx new file mode 100644 index 0000000000..195cf53a52 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuDashboardPageLayoutInfo.tsx @@ -0,0 +1,13 @@ +import { CommandMenuPageLayoutInfoContent } from '@/command-menu/components/CommandMenuPageLayoutInfoContent'; +import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; +import { isDefined } from 'twenty-shared/utils'; + +export const CommandMenuDashboardPageLayoutInfo = () => { + const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); + + if (!isDefined(pageLayoutId)) { + return null; + } + + return ; +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx index 72aa472f9f..531dea39bf 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageInfo.tsx @@ -55,6 +55,8 @@ export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => { CommandMenuPages.PageLayoutGraphFilter, CommandMenuPages.PageLayoutIframeSettings, CommandMenuPages.PageLayoutTabSettings, + CommandMenuPages.PageLayoutFieldsSettings, + CommandMenuPages.PageLayoutFieldsLayout, ].includes(pageChip.page?.page) : false; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx index d2b9c127f5..27964aae7e 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfo.tsx @@ -1,144 +1,30 @@ -import { usePageLayoutHeaderInfo } from '@/command-menu/components/hooks/usePageLayoutHeaderInfo'; -import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo'; -import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; -import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; -import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; -import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState'; -import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; -import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab'; -import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; -import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; -import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; -import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState'; -import { TitleInput } from '@/ui/input/components/TitleInput'; -import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { CommandMenuDashboardPageLayoutInfo } from '@/command-menu/components/CommandMenuDashboardPageLayoutInfo'; +import { CommandMenuRecordPageLayoutInfo } from '@/command-menu/components/CommandMenuRecordPageLayoutInfo'; +import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { useTheme } from '@emotion/react'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; -import { useIcons } from 'twenty-ui/display'; -import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout'; export const CommandMenuPageLayoutInfo = () => { - const theme = useTheme(); - const { getIcon } = useIcons(); - const commandMenuPage = useRecoilValue(commandMenuPageState); - const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); - const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); - - const [shouldFocusTitleInput, setShouldFocusTitleInput] = - useRecoilComponentState( - commandMenuShouldFocusTitleInputComponentState, - commandMenuPageInfo.instanceId, - ); - - const handleTitleInputOpen = () => { - setShouldFocusTitleInput(false); - }; - - const draftPageLayout = useRecoilComponentValue( - pageLayoutDraftComponentState, - pageLayoutId, + const objectMetadataId = useRecoilComponentValue( + contextStoreCurrentObjectMetadataItemIdComponentState, ); - const pageLayoutEditingWidgetId = useRecoilComponentValue( - pageLayoutEditingWidgetIdComponentState, - pageLayoutId, - ); - - const [openTabId] = useRecoilComponentState( - pageLayoutTabSettingsOpenTabIdComponentState, - pageLayoutId, - ); - - const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo(); - const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId); - const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId); - - const [editedTitle, setEditedTitle] = useState(null); - - const headerInfo = usePageLayoutHeaderInfo({ - commandMenuPage, - draftPageLayout, - pageLayoutEditingWidgetId, - openTabId, - editedTitle, - }); - - if (!headerInfo) { - return null; + if (!isDefined(objectMetadataId)) { + throw new Error('Object metadata ID is not defined'); } - const { - headerIcon, - headerIconColor, - headerType, - title, - isReadonly, - tab, - widgetInEditMode, - } = headerInfo; + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: objectMetadataId, + }); - const Icon = headerIcon ?? getIcon('IconDefault'); + const isDashboardContext = + objectMetadataItem.nameSingular === CoreObjectNameSingular.Dashboard; - const handleTitleChange = (newTitle: string) => { - setEditedTitle(newTitle); - }; + if (isDashboardContext) { + return ; + } - const saveTitle = async () => { - const finalTitle = editedTitle ?? title; - - if (!isNonEmptyString(finalTitle)) { - return; - } - - updateCommandMenuPageInfo({ - pageTitle: finalTitle, - pageIcon: Icon, - }); - - if ( - commandMenuPage === CommandMenuPages.PageLayoutTabSettings && - isDefined(tab) - ) { - updatePageLayoutTab(tab.id, { title: finalTitle }); - } else if (isDefined(widgetInEditMode)) { - updatePageLayoutWidget(widgetInEditMode.id, { - title: finalTitle, - }); - } - - setEditedTitle(null); - }; - - return ( - - ) : undefined - } - iconColor={headerIconColor} - title={ - setEditedTitle(null)} - onClickOutside={saveTitle} - onTab={saveTitle} - onShiftTab={saveTitle} - shouldFocus={shouldFocusTitleInput} - onFocus={handleTitleInputOpen} - /> - } - label={headerType} - /> - ); + return ; }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfoContent.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfoContent.tsx new file mode 100644 index 0000000000..569f4bc4a5 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuPageLayoutInfoContent.tsx @@ -0,0 +1,146 @@ +import { usePageLayoutHeaderInfo } from '@/command-menu/components/hooks/usePageLayoutHeaderInfo'; +import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo'; +import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState'; +import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; +import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab'; +import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget'; +import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState'; +import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; +import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState'; +import { TitleInput } from '@/ui/input/components/TitleInput'; +import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useTheme } from '@emotion/react'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; +import { CommandMenuPageInfoLayout } from './CommandMenuPageInfoLayout'; + +export const CommandMenuPageLayoutInfoContent = ({ + pageLayoutId, +}: { + pageLayoutId: string; +}) => { + const theme = useTheme(); + const { getIcon } = useIcons(); + const commandMenuPage = useRecoilValue(commandMenuPageState); + const commandMenuPageInfo = useRecoilValue(commandMenuPageInfoState); + + const [shouldFocusTitleInput, setShouldFocusTitleInput] = + useRecoilComponentState( + commandMenuShouldFocusTitleInputComponentState, + commandMenuPageInfo.instanceId, + ); + + const handleTitleInputOpen = () => { + setShouldFocusTitleInput(false); + }; + + const draftPageLayout = useRecoilComponentValue( + pageLayoutDraftComponentState, + pageLayoutId, + ); + + const pageLayoutEditingWidgetId = useRecoilComponentValue( + pageLayoutEditingWidgetIdComponentState, + pageLayoutId, + ); + + const [openTabId] = useRecoilComponentState( + pageLayoutTabSettingsOpenTabIdComponentState, + pageLayoutId, + ); + + const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo(); + const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId); + const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId); + + const [editedTitle, setEditedTitle] = useState(null); + + const headerInfo = usePageLayoutHeaderInfo({ + commandMenuPage, + draftPageLayout, + pageLayoutEditingWidgetId, + openTabId, + editedTitle, + }); + + if (!headerInfo) { + return null; + } + + const { + headerIcon, + headerIconColor, + headerType, + title, + isReadonly, + tab, + widgetInEditMode, + } = headerInfo; + + const Icon = headerIcon ?? getIcon('IconDefault'); + + const handleTitleChange = (newTitle: string) => { + setEditedTitle(newTitle); + }; + + const saveTitle = async () => { + const finalTitle = editedTitle ?? title; + + if (!isNonEmptyString(finalTitle)) { + return; + } + + updateCommandMenuPageInfo({ + pageTitle: finalTitle, + pageIcon: Icon, + }); + + if ( + commandMenuPage === CommandMenuPages.PageLayoutTabSettings && + isDefined(tab) + ) { + updatePageLayoutTab(tab.id, { title: finalTitle }); + } else if (isDefined(widgetInEditMode)) { + updatePageLayoutWidget(widgetInEditMode.id, { + title: finalTitle, + }); + } + + setEditedTitle(null); + }; + + return ( + + ) : undefined + } + iconColor={headerIconColor} + title={ + setEditedTitle(null)} + onClickOutside={saveTitle} + onTab={saveTitle} + onShiftTab={saveTitle} + shouldFocus={shouldFocusTitleInput} + onFocus={handleTitleInputOpen} + /> + } + label={headerType} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordPageLayoutInfo.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordPageLayoutInfo.tsx new file mode 100644 index 0000000000..6604f8f222 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuRecordPageLayoutInfo.tsx @@ -0,0 +1,9 @@ +import { CommandMenuPageLayoutInfoContent } from '@/command-menu/components/CommandMenuPageLayoutInfoContent'; +import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord'; + +export const CommandMenuRecordPageLayoutInfo = () => { + const { pageLayoutId } = + usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord(); + + return ; +}; 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 2776e6f188..3a8df0a0a1 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 @@ -10,6 +10,7 @@ import { isDefined } from 'twenty-shared/utils'; import { IconAppWindow, IconFrame, + IconList, IconPlus, type IconComponent, } from 'twenty-ui/display'; @@ -149,6 +150,37 @@ export const usePageLayoutHeaderInfo = ({ }; } + case CommandMenuPages.PageLayoutFieldsSettings: + case CommandMenuPages.PageLayoutFieldsLayout: { + if (!isDefined(pageLayoutEditingWidgetId)) { + return null; + } + + const widgetInEditMode = draftPageLayout.tabs + .flatMap((tab) => tab.widgets) + .find((widget) => widget.id === pageLayoutEditingWidgetId); + + if (!isDefined(widgetInEditMode)) { + return null; + } + + const title = isDefined(editedTitle) + ? editedTitle + : isDefined(widgetInEditMode.title) && widgetInEditMode.title !== '' + ? widgetInEditMode.title + : ''; + + return { + headerIcon: IconList, + headerIconColor: iconColor, + headerType: t`Fields Widget`, + title, + isReadonly: false, + tab: undefined, + widgetInEditMode, + }; + } + case CommandMenuPages.PageLayoutWidgetTypeSelect: { return { headerIcon: IconPlus, diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx index 8f55e9528e..0fb940a277 100644 --- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuPagesConfig.tsx @@ -5,6 +5,8 @@ import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-even import { CommandMenuFrontComponentPage } from '@/command-menu/pages/front-component/components/CommandMenuFrontComponentPage'; import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage'; import { CommandMenuPageLayoutChartSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings'; +import { CommandMenuPageLayoutFieldsLayout } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout'; +import { CommandMenuPageLayoutFieldsSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings'; import { CommandMenuPageLayoutGraphFilter } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutGraphFilter'; import { CommandMenuPageLayoutIframeSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutIframeSettings'; import { CommandMenuPageLayoutTabSettings } from '@/command-menu/pages/page-layout/components/CommandMenuPageLayoutTabSettings'; @@ -65,5 +67,13 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map< CommandMenuPages.PageLayoutTabSettings, , ], + [ + CommandMenuPages.PageLayoutFieldsSettings, + , + ], + [ + CommandMenuPages.PageLayoutFieldsLayout, + , + ], [CommandMenuPages.ViewFrontComponent, ], ]); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx index 491cb25feb..817d9e3626 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutChartSettings.tsx @@ -42,7 +42,7 @@ export const CommandMenuPageLayoutChartSettings = () => { value={{ instanceId: widgetInEditMode.id }} > - + ); diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx new file mode 100644 index 0000000000..0fafed4278 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsLayout.tsx @@ -0,0 +1,65 @@ +import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory'; +import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/components/SidePanelSubPageNavigationHeader'; +import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord'; +import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode'; +import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration'; +import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration'; +import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor'; +import styled from '@emotion/styled'; +import { t } from '@lingui/core/macro'; +import { useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; + +const StyledOuterContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; +`; + +const StyledContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(3)}; + padding: ${({ theme }) => theme.spacing(2)}; + overflow-y: auto; +`; + +export const CommandMenuPageLayoutFieldsLayout = () => { + const { goBackFromCommandMenu } = useCommandMenuHistory(); + + const { pageLayoutId, objectNameSingular } = + usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord(); + + const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId); + const defaultFieldsConfiguration = + useTemporaryFieldsConfiguration(objectNameSingular); + const [fieldsConfiguration, setFieldsConfiguration] = + useState(defaultFieldsConfiguration); + + if (!isDefined(widgetInEditMode)) { + return null; + } + + const handleConfigurationChange = ( + updatedConfiguration: FieldsConfiguration, + ) => { + // TODO: replace with a call to updatePageLayoutWidget + setFieldsConfiguration(updatedConfiguration); + }; + + return ( + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx new file mode 100644 index 0000000000..2ad8f8c293 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/CommandMenuPageLayoutFieldsSettings.tsx @@ -0,0 +1,94 @@ +import { CommandGroup } from '@/command-menu/components/CommandGroup'; +import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; +import { CommandMenuList } from '@/command-menu/components/CommandMenuList'; +import { WidgetSettingsFooter } from '@/command-menu/pages/page-layout/components/WidgetSettingsFooter'; +import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu'; +import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord'; +import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode'; +import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages'; +import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration'; +import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; +import styled from '@emotion/styled'; +import { useLingui } from '@lingui/react/macro'; +import { isDefined } from 'twenty-shared/utils'; +import { IconLayoutSidebarRight } from 'twenty-ui/display'; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + height: 100%; +`; + +const StyledCommandMenuContainer = styled.div` + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +`; + +export const CommandMenuPageLayoutFieldsSettings = () => { + const { t } = useLingui(); + const { navigatePageLayoutCommandMenu } = useNavigatePageLayoutCommandMenu(); + const { pageLayoutId, objectNameSingular } = + usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord(); + + const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId); + const fieldsConfiguration = + useTemporaryFieldsConfiguration(objectNameSingular); + + if (!isDefined(widgetInEditMode)) { + return null; + } + + const totalFieldsCount = fieldsConfiguration.sections.reduce( + (count, section) => count + section.fields.length, + 0, + ); + + const handleNavigateToLayout = () => { + navigatePageLayoutCommandMenu({ + commandMenuPage: CommandMenuPages.PageLayoutFieldsLayout, + }); + }; + + const selectableItemIds = [ + 'layout', + 'display-more-fields-button', + 'action-button', + 'move-down', + 'move-up', + 'move-to-tab', + 'add-widget-above', + 'add-widget-below', + 'delete', + ]; + + return ( + + + + + + + + + + + + + ); +}; 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 b7753d147f..0e3cba156f 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 @@ -93,7 +93,7 @@ export const CommandMenuPageLayoutIframeSettings = () => { error={urlError} /> - + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx index b8d3a63744..aa14fb739a 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/components/WidgetSettingsFooter.tsx @@ -1,4 +1,3 @@ -import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord'; import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget'; import { useDuplicatePageLayoutWidget } from '@/page-layout/hooks/useDuplicatePageLayoutWidget'; import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState'; @@ -14,12 +13,14 @@ import { isDefined } from 'twenty-shared/utils'; import { IconCopyPlus, IconTrash } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; -export const WidgetSettingsFooter = () => { +export const WidgetSettingsFooter = ({ + pageLayoutId, +}: { + pageLayoutId: string; +}) => { const dropdownId = useId(); const { t } = useLingui(); const { closeDropdown } = useCloseDropdown(); - - const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord(); const { duplicateWidget } = useDuplicatePageLayoutWidget(pageLayoutId); const { deletePageLayoutWidget } = useDeletePageLayoutWidget(pageLayoutId); const editingWidgetId = useRecoilComponentValue( diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts new file mode 100644 index 0000000000..5ed470a0a9 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord.ts @@ -0,0 +1,46 @@ +import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { isDefined } from 'twenty-shared/utils'; + +export const usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord = + () => { + const targetedRecordsRule = useRecoilComponentValue( + contextStoreTargetedRecordsRuleComponentState, + ); + + const objectMetadataId = useRecoilComponentValue( + contextStoreCurrentObjectMetadataItemIdComponentState, + ); + + if (!isDefined(objectMetadataId)) { + throw new Error('Object metadata ID is not defined'); + } + + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: objectMetadataId ?? undefined, + }); + + if ( + !( + targetedRecordsRule.mode === 'selection' && + targetedRecordsRule.selectedRecordIds.length === 1 + ) + ) { + throw new Error('Only one record should be selected'); + } + + const recordId: string = targetedRecordsRule.selectedRecordIds[0]; + + const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({ + id: recordId, + targetObjectNameSingular: objectMetadataItem.nameSingular, + }); + + return { + pageLayoutId, + objectNameSingular: objectMetadataItem.nameSingular, + }; + }; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts index 0000c13dcb..c0c0c5e7d3 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/types/PageLayoutCommandMenuPage.ts @@ -5,4 +5,6 @@ export type PageLayoutCommandMenuPage = | CommandMenuPages.PageLayoutGraphTypeSelect | CommandMenuPages.PageLayoutIframeSettings | CommandMenuPages.PageLayoutGraphFilter - | CommandMenuPages.PageLayoutTabSettings; + | CommandMenuPages.PageLayoutTabSettings + | CommandMenuPages.PageLayoutFieldsSettings + | CommandMenuPages.PageLayoutFieldsLayout; diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts index 358aca3c91..73789d114e 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutIcon.ts @@ -6,6 +6,8 @@ import { IconChartPie, IconFilter, IconFrame, + IconLayoutSidebarRight, + IconList, } from 'twenty-ui/display'; export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => { @@ -20,6 +22,10 @@ export const getPageLayoutIcon = (page: PageLayoutCommandMenuPage) => { return IconFilter; case CommandMenuPages.PageLayoutTabSettings: return IconAppWindow; + case CommandMenuPages.PageLayoutFieldsSettings: + return IconList; + case CommandMenuPages.PageLayoutFieldsLayout: + return IconLayoutSidebarRight; default: assertUnreachable(page); } diff --git a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts index 441d9f3acf..97d9d21852 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts +++ b/packages/twenty-front/src/modules/command-menu/pages/page-layout/utils/getPageLayoutPageTitle.ts @@ -15,6 +15,10 @@ export const getPageLayoutPageTitle = (page: PageLayoutCommandMenuPage) => { return t`Configure filters`; case CommandMenuPages.PageLayoutTabSettings: return t`Tab Settings`; + case CommandMenuPages.PageLayoutFieldsSettings: + return t`Fields Settings`; + case CommandMenuPages.PageLayoutFieldsLayout: + return t`Layout`; default: assertUnreachable(page); } diff --git a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts index a8250f1ff6..1af134d3f7 100644 --- a/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts +++ b/packages/twenty-front/src/modules/command-menu/types/CommandMenuPages.ts @@ -21,5 +21,7 @@ export enum CommandMenuPages { PageLayoutGraphFilter = 'page-layout-graph-filter', PageLayoutIframeSettings = 'page-layout-iframe-settings', PageLayoutTabSettings = 'page-layout-tab-settings', + PageLayoutFieldsSettings = 'page-layout-fields-settings', + PageLayoutFieldsLayout = 'page-layout-fields-layout', ViewFrontComponent = 'view-front-component', } diff --git a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts index 2656da741c..680dab921c 100644 --- a/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts +++ b/packages/twenty-front/src/modules/page-layout/hooks/useEditPageLayoutWidget.ts @@ -56,6 +56,15 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => { return; } + if (widgetType === WidgetType.FIELDS) { + navigatePageLayoutCommandMenu({ + commandMenuPage: CommandMenuPages.PageLayoutFieldsSettings, + pageTitle: t`Edit Fields`, + resetNavigationStack: true, + }); + return; + } + setCommandMenuPage(CommandMenuPages.Root); closeCommandMenu(); }, diff --git a/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts index 1f70d2aa7c..079c75340b 100644 --- a/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts +++ b/packages/twenty-front/src/modules/page-layout/types/FieldsConfiguration.ts @@ -4,6 +4,8 @@ export type FieldsConfigurationFieldItem = { fieldMetadataId: string; position: number; conditionalDisplay?: RulesLogic; + // TODO: This property will be removed. Instead, we will only store the fields we want to display in FieldsConfiguration + isVisible?: boolean; }; export type FieldsConfigurationSection = { diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx new file mode 100644 index 0000000000..08501f2236 --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationEditor.tsx @@ -0,0 +1,56 @@ +import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; +import { + type FieldsConfiguration, + type FieldsConfigurationSection, +} from '@/page-layout/types/FieldsConfiguration'; +import { FieldsConfigurationSectionEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor'; + +type FieldsConfigurationEditorProps = { + configuration: FieldsConfiguration; + onChange: (configuration: FieldsConfiguration) => void; +}; + +export const FieldsConfigurationEditor = ({ + configuration, + onChange, +}: FieldsConfigurationEditorProps) => { + const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow(); + + const handleSectionChange = ( + sectionId: string, + updatedSection: FieldsConfigurationSection, + ) => { + const updatedSections = configuration.sections.map((section) => + section.id === sectionId ? updatedSection : section, + ); + + onChange({ + ...configuration, + sections: updatedSections, + }); + }; + + const sortedSections = [...configuration.sections].sort( + (a, b) => a.position - b.position, + ); + + if (sortedSections.length === 0) { + return null; + } + + return ( + <> + {sortedSections.map((section, index) => ( + + handleSectionChange(section.id, updatedSection) + } + /> + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx new file mode 100644 index 0000000000..f51f37d10c --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor.tsx @@ -0,0 +1,39 @@ +import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { type FieldsConfigurationFieldItem } from '@/page-layout/types/FieldsConfiguration'; +import { IconEye, IconEyeOff, useIcons } from 'twenty-ui/display'; +import { MenuItemDraggable } from 'twenty-ui/navigation'; + +type FieldsConfigurationFieldEditorProps = { + field: FieldsConfigurationFieldItem; + fieldMetadata: FieldMetadataItem; + index: number; + onToggleVisibility: () => void; +}; + +export const FieldsConfigurationFieldEditor = ({ + field, + fieldMetadata, + onToggleVisibility, +}: FieldsConfigurationFieldEditorProps) => { + const { getIcon } = useIcons(); + const isVisible = field.isVisible !== false; + const FieldIcon = getIcon(fieldMetadata.icon); + + return ( + { + e.stopPropagation(); + onToggleVisibility(); + }, + }, + ]} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx new file mode 100644 index 0000000000..d1e97ff48b --- /dev/null +++ b/packages/twenty-front/src/modules/page-layout/widgets/fields/components/FieldsConfigurationSectionEditor.tsx @@ -0,0 +1,112 @@ +import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { + type FieldsConfigurationFieldItem, + type FieldsConfigurationSection, +} from '@/page-layout/types/FieldsConfiguration'; +import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { IconDotsVertical } from 'twenty-ui/display'; +import { MenuItem } from 'twenty-ui/navigation'; + +const StyledFieldsContainer = styled.div` + display: flex; + flex-direction: column; +`; + +type FieldsConfigurationSectionEditorProps = { + section: FieldsConfigurationSection; + index: number; + objectMetadataItem: ObjectMetadataItem; + onSectionChange: (section: FieldsConfigurationSection) => void; +}; + +export const FieldsConfigurationSectionEditor = ({ + section, + objectMetadataItem, + onSectionChange, +}: FieldsConfigurationSectionEditorProps) => { + const [isExpanded, setIsExpanded] = useState(true); + + const handleFieldChange = ( + fieldMetadataId: string, + updatedField: FieldsConfigurationFieldItem, + ) => { + const updatedFields = section.fields.map((field) => + field.fieldMetadataId === fieldMetadataId ? updatedField : field, + ); + + onSectionChange({ + ...section, + fields: updatedFields, + }); + }; + + const handleToggleFieldVisibility = (fieldMetadataId: string) => { + const field = section.fields.find( + (f) => f.fieldMetadataId === fieldMetadataId, + ); + + if (!field) { + return; + } + + const currentlyVisible = field.isVisible !== false; + + const updatedField: FieldsConfigurationFieldItem = { + ...field, + isVisible: !currentlyVisible, + }; + + handleFieldChange(fieldMetadataId, updatedField); + }; + + const sortedFields = [...section.fields].sort( + (a, b) => a.position - b.position, + ); + + return ( + <> + { + e.stopPropagation(); + // TODO: Add section menu + }, + }, + ]} + onClick={() => setIsExpanded(!isExpanded)} + hasSubMenu + isSubMenuOpened={isExpanded} + /> + {isExpanded && ( + + {sortedFields.map((field, fieldIndex) => { + const fieldMetadata = objectMetadataItem.fields.find( + (f) => f.id === field.fieldMetadataId, + ); + + if (!fieldMetadata) { + return null; + } + + return ( + + handleToggleFieldVisibility(field.fieldMetadataId) + } + /> + ); + })} + + )} + + ); +}; 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 index eecf7c4293..0221c67d80 100644 --- 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 @@ -45,6 +45,12 @@ export const filterAndOrderFieldsFromConfiguration = ({ return false; } + // Check user-controlled visibility flag + // TODO: This will be replaced by proper conditional display later + if (fieldConfig.isVisible === false) { + return false; + } + return evaluateWidgetVisibility({ conditionalDisplay: fieldConfig.conditionalDisplay, context,