[DASHBOARDS] Widget duplication (#15979)

## Widget duplication

- Created a shared component for the option menu shared by the record
page, the dashboards and the workflows
- Fix arrow selection in the option menu in workflows, which wasn't
working
- Scroll to the new widget after creation and open the new widget
settings


https://github.com/user-attachments/assets/47b8dade-44bd-4ba2-a81c-01b09e8718d3
This commit is contained in:
Raphaël Bosi
2025-11-24 14:38:18 +01:00
committed by GitHub
parent 8923d2fa0a
commit 2b80d9e015
18 changed files with 526 additions and 217 deletions
@@ -33,6 +33,13 @@ import {
type PageLayoutWidget,
} from '~/generated/graphql';
const StyledCommandMenuContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
`;
const StyledSidePanelInformationBanner = styled(SidePanelInformationBanner)`
margin-top: ${({ theme }) => theme.spacing(2)};
`;
@@ -137,78 +144,80 @@ export const ChartSettings = ({ widget }: { widget: PageLayoutWidget }) => {
primaryAxisField?.type === FieldMetadataType.DATE_TIME;
return (
<CommandMenuList commandGroups={[]} selectableItemIds={visibleItemIds}>
<ChartTypeSelectionSection
currentGraphType={currentGraphType}
setCurrentGraphType={handleGraphTypeChange}
/>
{hasWidgetTooManyGroups && (
<StyledSidePanelInformationBanner
message={
currentGraphType === GraphType.LINE
? t`Undisplayed data: max ${LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS} data points per chart.`
: t`Undisplayed data: max ${BAR_CHART_MAXIMUM_NUMBER_OF_BARS} bars per chart.`
}
tooltipMessage={
isPrimaryAxisDate
? t`Consider adding a filter or changing the date granularity to display more data.`
: t`Consider adding a filter to display more data.`
}
variant="warning"
<StyledCommandMenuContainer>
<CommandMenuList commandGroups={[]} selectableItemIds={visibleItemIds}>
<ChartTypeSelectionSection
currentGraphType={currentGraphType}
setCurrentGraphType={handleGraphTypeChange}
/>
)}
{chartSettings.map((group) => {
const visibleItems = group.items.filter(
(item) =>
!shouldHideChartSetting(
item,
widget.objectMetadataId,
isGroupByEnabled as boolean,
configuration,
objectMetadataItem,
),
);
{hasWidgetTooManyGroups && (
<StyledSidePanelInformationBanner
message={
currentGraphType === GraphType.LINE
? t`Undisplayed data: max ${LINE_CHART_MAXIMUM_NUMBER_OF_DATA_POINTS} data points per chart.`
: t`Undisplayed data: max ${BAR_CHART_MAXIMUM_NUMBER_OF_BARS} bars per chart.`
}
tooltipMessage={
isPrimaryAxisDate
? t`Consider adding a filter or changing the date granularity to display more data.`
: t`Consider adding a filter to display more data.`
}
variant="warning"
/>
)}
{chartSettings.map((group) => {
const visibleItems = group.items.filter(
(item) =>
!shouldHideChartSetting(
item,
widget.objectMetadataId,
isGroupByEnabled as boolean,
configuration,
objectMetadataItem,
),
);
return (
<CommandGroup key={group.heading} heading={group.heading}>
{visibleItems.map((item) => {
const handleItemToggleChange = () => {
setSelectedItemId(item.id);
updateChartSettingToggle(item.id);
};
return (
<CommandGroup key={group.heading} heading={group.heading}>
{visibleItems.map((item) => {
const handleItemToggleChange = () => {
setSelectedItemId(item.id);
updateChartSettingToggle(item.id);
};
const handleItemInputChange = (value: number | null) => {
updateChartSettingInput(item.id, value);
};
const handleItemInputChange = (value: number | null) => {
updateChartSettingInput(item.id, value);
};
const handleItemDropdownOpen = () => {
openDropdown({
dropdownComponentInstanceIdFromProps: item.id,
});
};
const handleItemDropdownOpen = () => {
openDropdown({
dropdownComponentInstanceIdFromProps: item.id,
});
};
const handleFilterClick = () => {
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutGraphFilter,
});
};
const handleFilterClick = () => {
navigatePageLayoutCommandMenu({
commandMenuPage: CommandMenuPages.PageLayoutGraphFilter,
});
};
return (
<ChartSettingItem
key={item.id}
item={item}
configuration={configuration}
getChartSettingsValues={getChartSettingsValues}
onToggleChange={handleItemToggleChange}
onInputChange={handleItemInputChange}
onDropdownOpen={handleItemDropdownOpen}
onFilterClick={handleFilterClick}
/>
);
})}
</CommandGroup>
);
})}
</CommandMenuList>
return (
<ChartSettingItem
key={item.id}
item={item}
configuration={configuration}
getChartSettingsValues={getChartSettingsValues}
onToggleChange={handleItemToggleChange}
onInputChange={handleItemInputChange}
onDropdownOpen={handleItemDropdownOpen}
onFilterClick={handleFilterClick}
/>
);
})}
</CommandGroup>
);
})}
</CommandMenuList>
</StyledCommandMenuContainer>
);
};
@@ -1,11 +1,19 @@
import { ChartSettings } from '@/command-menu/pages/page-layout/components/ChartSettings';
import { WidgetSettingsFooter } from '@/command-menu/pages/page-layout/components/WidgetSettingsFooter';
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
export const CommandMenuPageLayoutGraphTypeSelect = () => {
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
@@ -41,10 +49,13 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
}
return (
<GraphWidgetComponentInstanceContext.Provider
value={{ instanceId: widgetInEditMode.id }}
>
<ChartSettings widget={widgetInEditMode} />
</GraphWidgetComponentInstanceContext.Provider>
<StyledContainer>
<GraphWidgetComponentInstanceContext.Provider
value={{ instanceId: widgetInEditMode.id }}
>
<ChartSettings widget={widgetInEditMode} />
<WidgetSettingsFooter />
</GraphWidgetComponentInstanceContext.Provider>
</StyledContainer>
);
};
@@ -0,0 +1,65 @@
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
import { useDuplicatePageLayoutWidget } from '@/page-layout/hooks/useDuplicatePageLayoutWidget';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { OptionsDropdownMenu } from '@/ui/layout/dropdown/components/OptionsDropdownMenu';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useLingui } from '@lingui/react/macro';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconCopyPlus } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
export const WidgetSettingsFooter = () => {
const dropdownId = useId();
const { t } = useLingui();
const { closeDropdown } = useCloseDropdown();
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
const { duplicateWidget } = useDuplicatePageLayoutWidget(pageLayoutId);
const editingWidgetId = useRecoilComponentValue(
pageLayoutEditingWidgetIdComponentState,
pageLayoutId,
);
const handleDuplicateWidget = () => {
if (isDefined(editingWidgetId)) {
duplicateWidget(editingWidgetId);
}
closeDropdown(dropdownId);
};
const selectedItemId = useRecoilComponentValue(
selectedItemIdComponentState,
dropdownId,
);
return (
<RightDrawerFooter
actions={[
<OptionsDropdownMenu
key="options"
dropdownId={dropdownId}
selectableListId={dropdownId}
selectableItemIdArray={['duplicate-widget']}
>
<SelectableListItem
itemId="duplicate-widget"
onEnter={handleDuplicateWidget}
>
<MenuItem
focused={selectedItemId === 'duplicate-widget'}
onClick={handleDuplicateWidget}
text={t`Duplicate widget`}
LeftIcon={IconCopyPlus}
/>
</SelectableListItem>
</OptionsDropdownMenu>,
]}
/>
);
};