9e385e44f9
- 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.
79 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
};
|