Replace newFieldDefaultConfiguration with newFieldDefaultVisibility (#18539)

https://github.com/user-attachments/assets/365092cb-0fe1-44f7-9ae6-c6fc5edb98b2

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Baptiste Devessier
2026-03-11 13:14:30 +01:00
committed by GitHub
parent e4e7137660
commit ab5fb1f658
24 changed files with 843 additions and 1096 deletions
@@ -167,11 +167,8 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
... on FieldsConfiguration {
configurationType
viewId
newFieldDefaultVisibility
shouldAllowUserToSeeHiddenFields
newFieldDefaultConfiguration {
isVisible
viewFieldGroupId
}
}
... on FilesConfiguration {
configurationType
@@ -11,18 +11,15 @@ import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fiel
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { FieldsConfigurationGroupEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupEditor';
import { FieldsConfigurationUngroupedEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationUngroupedEditor';
import { NEW_FIELDS_INDICATOR_DRAGGABLE_ID } from '@/page-layout/widgets/fields/constants/NewFieldsIndicatorDraggableId';
import { useCreateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useCreateFieldsWidgetEditorGroup';
import { useDeleteFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useDeleteFieldsWidgetEditorGroup';
import { useFieldsWidgetEditorMode } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetEditorMode';
import { useGetNewFieldDefaultConfiguration } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultConfiguration';
import { useMoveFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveFieldInDraft';
import { useMoveUngroupedFieldInDraft } from '@/page-layout/widgets/fields/hooks/useMoveUngroupedFieldInDraft';
import { useReorderFieldsWidgetEditorGroups } from '@/page-layout/widgets/fields/hooks/useReorderFieldsWidgetEditorGroups';
import { useToggleFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleFieldVisibilityInDraft';
import { useToggleUngroupedFieldVisibilityInDraft } from '@/page-layout/widgets/fields/hooks/useToggleUngroupedFieldVisibilityInDraft';
import { useUpdateFieldsWidgetEditorGroup } from '@/page-layout/widgets/fields/hooks/useUpdateFieldsWidgetEditorGroup';
import { useUpdateNewFieldDefaultConfiguration } from '@/page-layout/widgets/fields/hooks/useUpdateNewFieldDefaultConfiguration';
import { getFieldsConfigurationGroupRenameDropdownId } from '@/page-layout/widgets/fields/utils/getFieldsConfigurationGroupRenameDropdownId';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -107,17 +104,6 @@ export const FieldsConfigurationEditor = ({
widgetId,
});
const { newFieldDefaultConfiguration } = useGetNewFieldDefaultConfiguration({
pageLayoutId,
widgetId,
});
const { updateNewFieldDefaultConfiguration } =
useUpdateNewFieldDefaultConfiguration({
pageLayoutId,
widgetId,
});
const { openDropdown } = useOpenDropdown();
const [renamingGroupValue, setRenamingGroupValue] = useState('');
@@ -141,7 +127,7 @@ export const FieldsConfigurationEditor = ({
};
const handleDragEnd = (result: DropResult) => {
const { source, destination, type, draggableId } = result;
const { source, destination, type } = result;
if (!destination) {
return;
@@ -154,17 +140,6 @@ export const FieldsConfigurationEditor = ({
return;
}
if (draggableId === NEW_FIELDS_INDICATOR_DRAGGABLE_ID) {
const cleanDestinationGroupId = destination.droppableId.replace(
'group-',
'',
);
updateNewFieldDefaultConfiguration({
viewFieldGroupId: cleanDestinationGroupId,
});
return;
}
if (type === 'GROUP') {
handleGroupReorder(source.index, destination.index);
} else if (type === 'FIELD') {
@@ -227,12 +202,6 @@ export const FieldsConfigurationEditor = ({
onMoveField={moveUngroupedField}
onToggleFieldVisibility={toggleUngroupedFieldVisibility}
onAddGroup={() => handleAddGroup({})}
newFieldsIsVisible={newFieldDefaultConfiguration.isVisible}
onToggleNewFieldsVisibility={() =>
updateNewFieldDefaultConfiguration({
isVisible: !newFieldDefaultConfiguration.isVisible,
})
}
/>
);
}
@@ -276,15 +245,6 @@ export const FieldsConfigurationEditor = ({
renamingGroupValue={renamingGroupValue}
onRenamingGroupValueChange={setRenamingGroupValue}
onStartRename={handleStartRename}
showNewFieldsItem={
group.id === newFieldDefaultConfiguration.viewFieldGroupId
}
newFieldsIsVisible={newFieldDefaultConfiguration.isVisible}
onToggleNewFieldsVisibility={() =>
updateNewFieldDefaultConfiguration({
isVisible: !newFieldDefaultConfiguration.isVisible,
})
}
/>
)}
</Draggable>
@@ -8,7 +8,6 @@ import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataI
import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationFieldEditor';
import { FieldsConfigurationGroupDropdown } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupDropdown';
import { FieldsConfigurationGroupRenameInput } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupRenameInput';
import { NEW_FIELDS_INDICATOR_DRAGGABLE_ID } from '@/page-layout/widgets/fields/constants/NewFieldsIndicatorDraggableId';
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { getFieldsConfigurationGroupRenameDropdownId } from '@/page-layout/widgets/fields/utils/getFieldsConfigurationGroupRenameDropdownId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@@ -16,13 +15,8 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import {
IconEye,
IconEyeOff,
IconNewSection,
IconPlaylistAdd,
} from 'twenty-ui/display';
import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
import { IconNewSection } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { FieldsConfigurationGroupDraggableHeader } from '@/page-layout/widgets/fields/components/FieldsConfigurationGroupDraggableHeader';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -89,9 +83,6 @@ type FieldsConfigurationGroupEditorProps = {
renamingGroupValue: string;
onRenamingGroupValueChange: (value: string) => void;
onStartRename: (params: { groupId: string; groupName: string }) => void;
showNewFieldsItem: boolean;
newFieldsIsVisible: boolean;
onToggleNewFieldsVisibility: () => void;
};
export const FieldsConfigurationGroupEditor = ({
@@ -105,9 +96,6 @@ export const FieldsConfigurationGroupEditor = ({
renamingGroupValue,
onRenamingGroupValueChange,
onStartRename,
showNewFieldsItem,
newFieldsIsVisible,
onToggleNewFieldsVisibility,
}: FieldsConfigurationGroupEditorProps) => {
const { t } = useLingui();
@@ -223,33 +211,6 @@ export const FieldsConfigurationGroupEditor = ({
/>
);
})}
{showNewFieldsItem && (
<DraggableItem
key={NEW_FIELDS_INDICATOR_DRAGGABLE_ID}
draggableId={NEW_FIELDS_INDICATOR_DRAGGABLE_ID}
index={sortedFields.length}
isInsideScrollableContainer
itemComponent={
<MenuItemDraggable
LeftIcon={IconPlaylistAdd}
text={t`New fields`}
contextualText={t`Default position/visibility for fields created in the future`}
gripMode="onHover"
withIconContainer
isIconDisplayedOnHoverOnly={false}
iconButtons={[
{
Icon: newFieldsIsVisible ? IconEye : IconEyeOff,
onClick: (e) => {
e.stopPropagation();
onToggleNewFieldsVisibility();
},
},
]}
/>
}
/>
)}
{droppableProvided.placeholder}
</StyledFieldsDroppable>
)}
@@ -6,13 +6,8 @@ import { FieldsConfigurationFieldEditor } from '@/page-layout/widgets/fields/com
import { type FieldsWidgetGroupField } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import {
IconEye,
IconEyeOff,
IconNewSection,
IconPlaylistAdd,
} from 'twenty-ui/display';
import { MenuItem, MenuItemDraggable } from 'twenty-ui/navigation';
import { IconNewSection } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
const StyledFieldsDroppable = styled.div`
display: flex;
@@ -25,8 +20,6 @@ type FieldsConfigurationUngroupedEditorProps = {
onMoveField: (sourceIndex: number, destinationIndex: number) => void;
onToggleFieldVisibility: (fieldMetadataId: string) => void;
onAddGroup: () => void;
newFieldsIsVisible: boolean;
onToggleNewFieldsVisibility: () => void;
};
export const FieldsConfigurationUngroupedEditor = ({
@@ -34,8 +27,6 @@ export const FieldsConfigurationUngroupedEditor = ({
onMoveField,
onToggleFieldVisibility,
onAddGroup,
newFieldsIsVisible,
onToggleNewFieldsVisibility,
}: FieldsConfigurationUngroupedEditorProps) => {
const { t } = useLingui();
@@ -89,25 +80,6 @@ export const FieldsConfigurationUngroupedEditor = ({
))}
{provided.placeholder}
<MenuItemDraggable
LeftIcon={IconPlaylistAdd}
text={t`New fields`}
contextualText={t`Default position/visibility for fields created in the future`}
gripMode="never"
isDragDisabled
withIconContainer
isIconDisplayedOnHoverOnly={false}
iconButtons={[
{
Icon: newFieldsIsVisible ? IconEye : IconEyeOff,
onClick: (e) => {
e.stopPropagation();
onToggleNewFieldsVisibility();
},
},
]}
/>
<MenuItem
LeftIcon={IconNewSection}
withIconContainer
@@ -1 +0,0 @@
export const NEW_FIELDS_INDICATOR_DRAGGABLE_ID = 'new-fields-indicator';
@@ -1,52 +0,0 @@
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { getLastGroupId } from '@/page-layout/widgets/fields/utils/getLastGroupId';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
import {
type FieldsConfiguration,
WidgetConfigurationType,
} from '~/generated-metadata/graphql';
type UseGetNewFieldDefaultConfigurationParams = {
pageLayoutId: string;
widgetId: string;
};
export const useGetNewFieldDefaultConfiguration = ({
pageLayoutId,
widgetId,
}: UseGetNewFieldDefaultConfigurationParams) => {
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const fieldsWidgetGroupsDraft = useAtomComponentStateValue(
fieldsWidgetGroupsDraftComponentState,
pageLayoutId,
);
const draftGroups = fieldsWidgetGroupsDraft[widgetId] ?? [];
const widget = pageLayoutDraft.tabs
.flatMap((tab) => tab.widgets)
.find((w) => w.id === widgetId);
const fieldsConfiguration =
isDefined(widget?.configuration) &&
widget.configuration.configurationType === WidgetConfigurationType.FIELDS
? (widget.configuration as FieldsConfiguration)
: null;
const lastGroupId = getLastGroupId(draftGroups);
const persisted = fieldsConfiguration?.newFieldDefaultConfiguration;
const newFieldDefaultConfiguration = {
isVisible: persisted?.isVisible ?? true,
viewFieldGroupId: persisted?.viewFieldGroupId ?? lastGroupId,
};
return { newFieldDefaultConfiguration, fieldsConfiguration };
};
@@ -0,0 +1,37 @@
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
import {
type FieldsConfiguration,
WidgetConfigurationType,
} from '~/generated-metadata/graphql';
type UseGetNewFieldDefaultVisibilityParams = {
pageLayoutId: string;
widgetId: string;
};
export const useGetNewFieldDefaultVisibility = ({
pageLayoutId,
widgetId,
}: UseGetNewFieldDefaultVisibilityParams) => {
const pageLayoutDraft = useAtomComponentStateValue(
pageLayoutDraftComponentState,
pageLayoutId,
);
const widget = pageLayoutDraft.tabs
.flatMap((tab) => tab.widgets)
.find((w) => w.id === widgetId);
const fieldsConfiguration =
isDefined(widget?.configuration) &&
widget.configuration.configurationType === WidgetConfigurationType.FIELDS
? (widget.configuration as FieldsConfiguration)
: null;
const newFieldDefaultVisibility =
fieldsConfiguration?.newFieldDefaultVisibility ?? true;
return { newFieldDefaultVisibility, fieldsConfiguration };
};
@@ -1,45 +0,0 @@
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { useGetNewFieldDefaultConfiguration } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultConfiguration';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
type UseUpdateNewFieldDefaultConfigurationParams = {
pageLayoutId: string;
widgetId: string;
};
export const useUpdateNewFieldDefaultConfiguration = ({
pageLayoutId,
widgetId,
}: UseUpdateNewFieldDefaultConfigurationParams) => {
const { newFieldDefaultConfiguration, fieldsConfiguration } =
useGetNewFieldDefaultConfiguration({ pageLayoutId, widgetId });
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
const updateNewFieldDefaultConfiguration = useCallback(
(updates: { isVisible?: boolean; viewFieldGroupId?: string | null }) => {
if (!isDefined(fieldsConfiguration)) {
return;
}
updatePageLayoutWidget(widgetId, {
configuration: {
...fieldsConfiguration,
newFieldDefaultConfiguration: {
...newFieldDefaultConfiguration,
...updates,
},
},
});
},
[
fieldsConfiguration,
newFieldDefaultConfiguration,
updatePageLayoutWidget,
widgetId,
],
);
return { updateNewFieldDefaultConfiguration };
};
@@ -0,0 +1,37 @@
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
import { useGetNewFieldDefaultVisibility } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultVisibility';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
type UseUpdateNewFieldDefaultVisibilityParams = {
pageLayoutId: string;
widgetId: string;
};
export const useUpdateNewFieldDefaultVisibility = ({
pageLayoutId,
widgetId,
}: UseUpdateNewFieldDefaultVisibilityParams) => {
const { newFieldDefaultVisibility, fieldsConfiguration } =
useGetNewFieldDefaultVisibility({ pageLayoutId, widgetId });
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
const updateNewFieldDefaultVisibility = useCallback(
(isVisible: boolean) => {
if (!isDefined(fieldsConfiguration)) {
return;
}
updatePageLayoutWidget(widgetId, {
configuration: {
...fieldsConfiguration,
newFieldDefaultVisibility: isVisible,
},
});
},
[fieldsConfiguration, updatePageLayoutWidget, widgetId],
);
return { updateNewFieldDefaultVisibility, newFieldDefaultVisibility };
};
@@ -1,46 +0,0 @@
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
import { getLastGroupId } from '@/page-layout/widgets/fields/utils/getLastGroupId';
const makeGroup = (
overrides: Partial<FieldsWidgetGroup> & { id: string },
): FieldsWidgetGroup => ({
name: 'Group',
position: 0,
isVisible: true,
fields: [],
...overrides,
});
describe('getLastGroupId', () => {
it('should return null for empty array', () => {
expect(getLastGroupId([])).toBeNull();
});
it('should return the id of a single group', () => {
const groups = [makeGroup({ id: 'g1', position: 0 })];
expect(getLastGroupId(groups)).toBe('g1');
});
it('should return the id of the group with the highest position', () => {
const groups = [
makeGroup({ id: 'g1', position: 0 }),
makeGroup({ id: 'g2', position: 2 }),
makeGroup({ id: 'g3', position: 1 }),
];
expect(getLastGroupId(groups)).toBe('g2');
});
it('should not mutate the original array', () => {
const groups = [
makeGroup({ id: 'g2', position: 2 }),
makeGroup({ id: 'g1', position: 0 }),
];
getLastGroupId(groups);
expect(groups[0].id).toBe('g2');
expect(groups[1].id).toBe('g1');
});
});
@@ -1,11 +0,0 @@
import { type FieldsWidgetGroup } from '@/page-layout/widgets/fields/types/FieldsWidgetGroup';
export const getLastGroupId = (groups: FieldsWidgetGroup[]): string | null => {
if (groups.length === 0) {
return null;
}
const sortedGroups = [...groups].sort((a, b) => a.position - b.position);
return sortedGroups[sortedGroups.length - 1].id;
};
@@ -0,0 +1,46 @@
import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItemToggle';
import { useGetNewFieldDefaultVisibility } from '@/page-layout/widgets/fields/hooks/useGetNewFieldDefaultVisibility';
import { useUpdateNewFieldDefaultVisibility } from '@/page-layout/widgets/fields/hooks/useUpdateNewFieldDefaultVisibility';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { useLingui } from '@lingui/react/macro';
import { IconEye } from 'twenty-ui/display';
export const NewFieldDefaultVisibilityToggle = ({
pageLayoutId,
widgetId,
}: {
pageLayoutId: string;
widgetId: string;
}) => {
const { t } = useLingui();
const { newFieldDefaultVisibility } = useGetNewFieldDefaultVisibility({
pageLayoutId,
widgetId,
});
const { updateNewFieldDefaultVisibility } =
useUpdateNewFieldDefaultVisibility({
pageLayoutId,
widgetId,
});
const handleToggle = () => {
updateNewFieldDefaultVisibility(!newFieldDefaultVisibility);
};
return (
<SelectableListItem
itemId="new-field-default-visibility"
onEnter={handleToggle}
>
<CommandMenuItemToggle
LeftIcon={IconEye}
text={t`Set fields created in the future as "visible"`}
id="new-field-default-visibility"
toggled={newFieldDefaultVisibility}
onToggleChange={handleToggle}
/>
</SelectableListItem>
);
};
@@ -3,6 +3,7 @@ import { CommandMenuItemToggle } from '@/command-menu/components/CommandMenuItem
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
import { SidePanelList } from '@/side-panel/components/SidePanelList';
import { NewFieldDefaultVisibilityToggle } from '@/side-panel/pages/page-layout/components/NewFieldDefaultVisibilityToggle';
import { WidgetSettingsFooter } from '@/side-panel/pages/page-layout/components/WidgetSettingsFooter';
import { useNavigatePageLayoutSidePanel } from '@/side-panel/pages/page-layout/hooks/useNavigatePageLayoutSidePanel';
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/side-panel/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
@@ -78,6 +79,7 @@ export const SidePanelPageLayoutFieldsSettings = () => {
const selectableItemIds = [
'layout',
'new-field-default-visibility',
'display-more-fields-button',
'action-button',
'move-down',
@@ -119,6 +121,10 @@ export const SidePanelPageLayoutFieldsSettings = () => {
onToggleChange={handleToggleShouldAllowUserToSeeHiddenFields}
/>
</SelectableListItem>
<NewFieldDefaultVisibilityToggle
pageLayoutId={pageLayoutId}
widgetId={widgetInEditMode.id}
/>
</SidePanelGroup>
</SidePanelList>
</StyledSidePanelContainer>