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:
+74
-16
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+66
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+36
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const LAYOUT_CUSTOMIZATION_BAR_DROPDOWN_ID =
|
||||
'layout-customization-bar-dropdown';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RESET_RECORD_PAGE_LAYOUT_MODAL_ID =
|
||||
'reset-record-page-layout-modal';
|
||||
Reference in New Issue
Block a user