Scaffold Fields widget edition (#17548)
https://github.com/user-attachments/assets/960b8b0d-10e8-42a1-bf61-e875359ea302 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Devessier <29370468+Devessier@users.noreply.github.com> Co-authored-by: Devessier <baptiste@devessier.fr>
This commit is contained in:
+13
@@ -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 <CommandMenuPageLayoutInfoContent pageLayoutId={pageLayoutId} />;
|
||||
};
|
||||
@@ -55,6 +55,8 @@ export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
|
||||
CommandMenuPages.PageLayoutGraphFilter,
|
||||
CommandMenuPages.PageLayoutIframeSettings,
|
||||
CommandMenuPages.PageLayoutTabSettings,
|
||||
CommandMenuPages.PageLayoutFieldsSettings,
|
||||
CommandMenuPages.PageLayoutFieldsLayout,
|
||||
].includes(pageChip.page?.page)
|
||||
: false;
|
||||
|
||||
|
||||
+18
-132
@@ -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<string | null>(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 <CommandMenuDashboardPageLayoutInfo />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
isDefined(headerIcon) ? (
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
) : undefined
|
||||
}
|
||||
iconColor={headerIconColor}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`page-layout-title-${commandMenuPage}-${pageLayoutId}`}
|
||||
disabled={isReadonly}
|
||||
sizeVariant="sm"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={headerType}
|
||||
onEnter={saveTitle}
|
||||
onEscape={() => setEditedTitle(null)}
|
||||
onClickOutside={saveTitle}
|
||||
onTab={saveTitle}
|
||||
onShiftTab={saveTitle}
|
||||
shouldFocus={shouldFocusTitleInput}
|
||||
onFocus={handleTitleInputOpen}
|
||||
/>
|
||||
}
|
||||
label={headerType}
|
||||
/>
|
||||
);
|
||||
return <CommandMenuRecordPageLayoutInfo />;
|
||||
};
|
||||
|
||||
+146
@@ -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<string | null>(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 (
|
||||
<CommandMenuPageInfoLayout
|
||||
icon={
|
||||
isDefined(headerIcon) ? (
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
) : undefined
|
||||
}
|
||||
iconColor={headerIconColor}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`page-layout-title-${commandMenuPage}-${pageLayoutId}`}
|
||||
disabled={isReadonly}
|
||||
sizeVariant="sm"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={headerType}
|
||||
onEnter={saveTitle}
|
||||
onEscape={() => setEditedTitle(null)}
|
||||
onClickOutside={saveTitle}
|
||||
onTab={saveTitle}
|
||||
onShiftTab={saveTitle}
|
||||
shouldFocus={shouldFocusTitleInput}
|
||||
onFocus={handleTitleInputOpen}
|
||||
/>
|
||||
}
|
||||
label={headerType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+9
@@ -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 <CommandMenuPageLayoutInfoContent pageLayoutId={pageLayoutId} />;
|
||||
};
|
||||
+32
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
<CommandMenuPageLayoutTabSettings />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutFieldsSettings,
|
||||
<CommandMenuPageLayoutFieldsSettings />,
|
||||
],
|
||||
[
|
||||
CommandMenuPages.PageLayoutFieldsLayout,
|
||||
<CommandMenuPageLayoutFieldsLayout />,
|
||||
],
|
||||
[CommandMenuPages.ViewFrontComponent, <CommandMenuFrontComponentPage />],
|
||||
]);
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export const CommandMenuPageLayoutChartSettings = () => {
|
||||
value={{ instanceId: widgetInEditMode.id }}
|
||||
>
|
||||
<ChartSettings widget={widgetInEditMode} />
|
||||
<WidgetSettingsFooter />
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
</WidgetComponentInstanceContext.Provider>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
+65
@@ -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<FieldsConfiguration>(defaultFieldsConfiguration);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleConfigurationChange = (
|
||||
updatedConfiguration: FieldsConfiguration,
|
||||
) => {
|
||||
// TODO: replace with a call to updatePageLayoutWidget
|
||||
setFieldsConfiguration(updatedConfiguration);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledOuterContainer>
|
||||
<SidePanelSubPageNavigationHeader
|
||||
title={t`Layout`}
|
||||
onBackClick={goBackFromCommandMenu}
|
||||
/>
|
||||
<StyledContainer>
|
||||
<FieldsConfigurationEditor
|
||||
configuration={fieldsConfiguration}
|
||||
onChange={handleConfigurationChange}
|
||||
/>
|
||||
</StyledContainer>
|
||||
</StyledOuterContainer>
|
||||
);
|
||||
};
|
||||
+94
@@ -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 (
|
||||
<StyledContainer>
|
||||
<StyledCommandMenuContainer>
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={selectableItemIds}
|
||||
>
|
||||
<CommandGroup heading={t`Customize`}>
|
||||
<SelectableListItem
|
||||
itemId="layout"
|
||||
onEnter={handleNavigateToLayout}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id="layout"
|
||||
label={t`Layout`}
|
||||
Icon={IconLayoutSidebarRight}
|
||||
hasSubMenu
|
||||
onClick={handleNavigateToLayout}
|
||||
description={t`${totalFieldsCount} fields`}
|
||||
contextualTextPosition="right"
|
||||
/>
|
||||
</SelectableListItem>
|
||||
</CommandGroup>
|
||||
</CommandMenuList>
|
||||
</StyledCommandMenuContainer>
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -93,7 +93,7 @@ export const CommandMenuPageLayoutIframeSettings = () => {
|
||||
error={urlError}
|
||||
/>
|
||||
</StyledContainer>
|
||||
<WidgetSettingsFooter />
|
||||
<WidgetSettingsFooter pageLayoutId={pageLayoutId} />
|
||||
</StyledOuterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-4
@@ -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(
|
||||
|
||||
+46
@@ -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,
|
||||
};
|
||||
};
|
||||
+3
-1
@@ -5,4 +5,6 @@ export type PageLayoutCommandMenuPage =
|
||||
| CommandMenuPages.PageLayoutGraphTypeSelect
|
||||
| CommandMenuPages.PageLayoutIframeSettings
|
||||
| CommandMenuPages.PageLayoutGraphFilter
|
||||
| CommandMenuPages.PageLayoutTabSettings;
|
||||
| CommandMenuPages.PageLayoutTabSettings
|
||||
| CommandMenuPages.PageLayoutFieldsSettings
|
||||
| CommandMenuPages.PageLayoutFieldsLayout;
|
||||
|
||||
+6
@@ -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);
|
||||
}
|
||||
|
||||
+4
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+56
@@ -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) => (
|
||||
<FieldsConfigurationSectionEditor
|
||||
key={section.id}
|
||||
section={section}
|
||||
index={index}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
onSectionChange={(updatedSection) =>
|
||||
handleSectionChange(section.id, updatedSection)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+39
@@ -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 (
|
||||
<MenuItemDraggable
|
||||
LeftIcon={FieldIcon}
|
||||
text={fieldMetadata.label}
|
||||
showGrip
|
||||
isIconDisplayedOnHoverOnly={false}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: isVisible ? IconEye : IconEyeOff,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
onToggleVisibility();
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+112
@@ -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 (
|
||||
<>
|
||||
<MenuItem
|
||||
text={section.title}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconDotsVertical,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
// TODO: Add section menu
|
||||
},
|
||||
},
|
||||
]}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
hasSubMenu
|
||||
isSubMenuOpened={isExpanded}
|
||||
/>
|
||||
{isExpanded && (
|
||||
<StyledFieldsContainer>
|
||||
{sortedFields.map((field, fieldIndex) => {
|
||||
const fieldMetadata = objectMetadataItem.fields.find(
|
||||
(f) => f.id === field.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!fieldMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldsConfigurationFieldEditor
|
||||
key={field.fieldMetadataId}
|
||||
field={field}
|
||||
fieldMetadata={fieldMetadata}
|
||||
index={fieldIndex}
|
||||
onToggleVisibility={() =>
|
||||
handleToggleFieldVisibility(field.fieldMetadataId)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledFieldsContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+6
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user