Add reset page layout in record page layout edit mode tab (#19800)

## Context
Adds a burger-menu dropdown on the layout customization bar exposing a
"Reset record page layout" action, so users can reset a record page
layout straight from the edit bar (previously only available in object
settings).

<img width="1512" height="849" alt="Screenshot 2026-04-17 at 18 03 19"
src="https://github.com/user-attachments/assets/145a77b8-6234-4987-ae31-38eccaa0548d"
/>
This commit is contained in:
Weiko
2026-04-17 19:46:42 +02:00
committed by GitHub
parent b8f8892b67
commit b320de966c
5 changed files with 180 additions and 16 deletions
@@ -1,15 +1,24 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { useAtomValue } from 'jotai';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconCheck, IconPaint } from 'twenty-ui/display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { LayoutCustomizationBarMenuDropdown } from '@/layout-customization/components/LayoutCustomizationBarMenuDropdown';
import { LayoutCustomizationBarResetConfirmationModal } from '@/layout-customization/components/LayoutCustomizationBarResetConfirmationModal';
import { useCancelLayoutCustomization } from '@/layout-customization/hooks/useCancelLayoutCustomization';
import { useIsLayoutCustomizationDirty } from '@/layout-customization/hooks/useIsLayoutCustomizationDirty';
import { useSaveLayoutCustomization } from '@/layout-customization/hooks/useSaveLayoutCustomization';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { useContext } from 'react';
import { IconCheck, IconPaint } from 'twenty-ui/display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { PageLayoutType } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
align-items: center;
@@ -22,10 +31,27 @@ const StyledContainer = styled.div`
width: 100%;
`;
const StyledLeftSection = styled.div`
align-items: center;
display: flex;
flex: 1;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledTitle = styled.span`
align-items: center;
display: flex;
flex: 1;
gap: ${themeCssVariables.spacing[2]};
justify-content: center;
text-align: center;
`;
const StyledRightSection = styled.div`
align-items: center;
display: flex;
flex: 1;
justify-content: flex-end;
`;
const LayoutCustomizationBarContent = () => {
@@ -36,6 +62,26 @@ const LayoutCustomizationBarContent = () => {
const { cancel } = useCancelLayoutCustomization();
const { isDirty } = useIsLayoutCustomizationDirty();
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
const persistedPageLayout = useAtomValue(
pageLayoutPersistedComponentState.atomFamily({
instanceId: currentPageLayoutId ?? '',
}),
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const recordPageLayoutObject =
isDefined(currentPageLayoutId) &&
persistedPageLayout?.type === PageLayoutType.RECORD_PAGE
? objectMetadataItems.find(
(item) => item.id === persistedPageLayout.objectMetadataId,
)
: undefined;
const title = isDefined(recordPageLayoutObject)
? t`${recordPageLayoutObject.labelPlural} layout edition`
: t`Layout customization`;
return (
<motion.div
initial={{ height: 0, opacity: 0 }}
@@ -47,20 +93,32 @@ const LayoutCustomizationBarContent = () => {
}}
>
<StyledContainer data-globally-prevent-click-outside="true">
<StyledLeftSection>
{isDefined(recordPageLayoutObject) && (
<LayoutCustomizationBarMenuDropdown />
)}
</StyledLeftSection>
<StyledTitle>
<IconPaint size={theme.icon.size.md} />
{t`Layout customization`}
{title}
</StyledTitle>
<SaveAndCancelButtons
onSave={save}
onCancel={cancel}
isSaveDisabled={!isDirty || isSaving}
isCancelDisabled={isSaving}
isLoading={isSaving}
inverted
saveIcon={IconCheck}
/>
<StyledRightSection>
<SaveAndCancelButtons
onSave={save}
onCancel={cancel}
isSaveDisabled={!isDirty || isSaving}
isCancelDisabled={isSaving}
isLoading={isSaving}
inverted
saveIcon={IconCheck}
/>
</StyledRightSection>
</StyledContainer>
{isDefined(recordPageLayoutObject) && isDefined(currentPageLayoutId) && (
<LayoutCustomizationBarResetConfirmationModal
pageLayoutId={currentPageLayoutId}
/>
)}
</motion.div>
);
};
@@ -0,0 +1,66 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { IconDotsVertical, IconReload } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { LAYOUT_CUSTOMIZATION_BAR_DROPDOWN_ID } from '@/layout-customization/constants/LayoutCustomizationBarDropdownId';
import { RESET_RECORD_PAGE_LAYOUT_MODAL_ID } from '@/layout-customization/constants/ResetRecordPageLayoutModalId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
const StyledInvertedIconButtonWrapper = styled.span`
align-items: center;
display: flex;
button {
color: ${themeCssVariables.font.color.inverted};
}
button:hover {
background: ${themeCssVariables.background.transparent.light};
}
`;
export const LayoutCustomizationBarMenuDropdown = () => {
const { t } = useLingui();
const { closeDropdown } = useCloseDropdown();
const { openModal } = useModal();
const handleResetClick = () => {
closeDropdown(LAYOUT_CUSTOMIZATION_BAR_DROPDOWN_ID);
openModal(RESET_RECORD_PAGE_LAYOUT_MODAL_ID);
};
return (
<Dropdown
dropdownId={LAYOUT_CUSTOMIZATION_BAR_DROPDOWN_ID}
dropdownPlacement="bottom-start"
clickableComponent={
<StyledInvertedIconButtonWrapper>
<LightIconButton
Icon={IconDotsVertical}
accent="tertiary"
aria-label={t`Layout customization menu`}
/>
</StyledInvertedIconButtonWrapper>
}
dropdownComponents={
<DropdownContent widthInPixels={GenericDropdownContentWidth.Large}>
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconReload}
text={t`Reset record page layout`}
onClick={handleResetClick}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
);
};
@@ -0,0 +1,36 @@
import { useLingui } from '@lingui/react/macro';
import { RESET_RECORD_PAGE_LAYOUT_MODAL_ID } from '@/layout-customization/constants/ResetRecordPageLayoutModalId';
import { useRefreshPageLayoutAfterReset } from '@/page-layout/hooks/useRefreshPageLayoutAfterReset';
import { useResetPageLayoutToDefault } from '@/page-layout/hooks/useResetPageLayoutToDefault';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
type LayoutCustomizationBarResetConfirmationModalProps = {
pageLayoutId: string;
};
export const LayoutCustomizationBarResetConfirmationModal = ({
pageLayoutId,
}: LayoutCustomizationBarResetConfirmationModalProps) => {
const { t } = useLingui();
const { resetPageLayoutToDefault } = useResetPageLayoutToDefault();
const { refreshPageLayoutAfterReset } =
useRefreshPageLayoutAfterReset(pageLayoutId);
const handleConfirmReset = async () => {
await resetPageLayoutToDefault({ pageLayoutId });
await refreshPageLayoutAfterReset();
};
return (
<ConfirmationModal
modalInstanceId={RESET_RECORD_PAGE_LAYOUT_MODAL_ID}
title={t`Reset to default`}
subtitle={t`This action cannot be undone.`}
onConfirmClick={handleConfirmReset}
confirmButtonText={t`Reset`}
confirmButtonAccent="danger"
/>
);
};
@@ -0,0 +1,2 @@
export const LAYOUT_CUSTOMIZATION_BAR_DROPDOWN_ID =
'layout-customization-bar-dropdown';
@@ -0,0 +1,2 @@
export const RESET_RECORD_PAGE_LAYOUT_MODAL_ID =
'reset-record-page-layout-modal';