Files
twenty/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx
T
Weiko 9e385e44f9 Fields widget draft view (#19562)
- Defer backend view creation for FIELDS widgets to prevent orphan views
when users cancel or leave before saving. Views are now created
optimistically in Jotai state (client-side UUID) and only persisted to
the database when the page layout is saved.
- Align the frontend's default field groups fallback with the backend
logic: split into General/Other groups, hide relation fields by default,
and filter invisible fields consistently across all code paths.
2026-04-12 20:26:47 +02:00

79 lines
2.7 KiB
TypeScript

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 { 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';
const StyledContainer = styled.div`
align-items: center;
background: ${themeCssVariables.color.blue};
box-sizing: border-box;
color: ${themeCssVariables.font.color.inverted};
display: flex;
justify-content: space-between;
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
width: 100%;
`;
const StyledTitle = styled.span`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
`;
const LayoutCustomizationBarContent = () => {
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const { save, isSaving } = useSaveLayoutCustomization();
const { cancel } = useCancelLayoutCustomization();
const { isDirty } = useIsLayoutCustomizationDirty();
return (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: theme.animation.duration.normal,
ease: 'easeInOut',
}}
>
<StyledContainer data-globally-prevent-click-outside="true">
<StyledTitle>
<IconPaint size={theme.icon.size.md} />
{t`Layout customization`}
</StyledTitle>
<SaveAndCancelButtons
onSave={save}
onCancel={cancel}
isSaveDisabled={!isDirty || isSaving}
isCancelDisabled={isSaving}
isLoading={isSaving}
inverted
saveIcon={IconCheck}
/>
</StyledContainer>
</motion.div>
);
};
export const LayoutCustomizationBar = () => {
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
);
return (
<AnimatePresence>
{isLayoutCustomizationModeEnabled && <LayoutCustomizationBarContent />}
</AnimatePresence>
);
};