Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
343843d
feat: wip
Devessier Feb 13, 2026
1f4a966
Merge remote-tracking branch 'origin/main' into wire-fields-widget-to…
Devessier Feb 13, 2026
76cfdb1
Merge remote-tracking branch 'origin/main' into wire-fields-widget-to…
Devessier Feb 13, 2026
023b8f7
fix: prevent issue with css selector starting with a number
Devessier Feb 13, 2026
1d3f845
feat: read record page layout from state
Devessier Feb 13, 2026
adcb30b
fix: filter out some fields
Devessier Feb 17, 2026
afc8962
fix: types
Devessier Feb 17, 2026
f35bf63
Merge remote-tracking branch 'origin/main' into wire-fields-widget-to…
Devessier Feb 17, 2026
bbd5516
lint: fix
Devessier Feb 17, 2026
78e835c
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 17, 2026
0be84c1
fix: convert fields widgets view types
Devessier Feb 17, 2026
477dff6
feat: custom query to fetch fields widgets views
Devessier Feb 17, 2026
2df9520
Merge remote-tracking branch 'origin/main' into wire-fields-widget-to…
Devessier Feb 17, 2026
0d51eb1
fix: rely on widget configuration when available
Devessier Feb 18, 2026
1eefd5a
feat: wip draft mode for views
Devessier Feb 18, 2026
9d0ecfe
feat: do not save dynamic field widgets
Devessier Feb 18, 2026
d49b8ed
feat: sync live updates between command menu and fields widget
Devessier Feb 18, 2026
9b7e174
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 18, 2026
cac3ba8
feat: add validation on update
Devessier Feb 18, 2026
37443a2
fix: fix backend
Devessier Feb 18, 2026
890ef15
fix: fix type import
Devessier Feb 18, 2026
146bfef
fix: fetch all required info after update
Devessier Feb 18, 2026
510986c
feat: refetch views after update
Devessier Feb 18, 2026
1b7008a
Merge remote-tracking branch 'origin/main' into wire-fields-widget-to…
Devessier Feb 19, 2026
26d37ed
Merge remote-tracking branch 'origin/main' into wire-fields-widget-to…
Devessier Feb 19, 2026
578b49d
test: add tests
Devessier Feb 19, 2026
0bddde5
lint: fix
Devessier Feb 19, 2026
eb228b1
refactor: remove useRef
Devessier Feb 19, 2026
1ddb37d
lint: fix
Devessier Feb 19, 2026
18321fa
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 19, 2026
0bb8610
lint: fix
Devessier Feb 19, 2026
07eb836
test: update snapshots
Devessier Feb 19, 2026
3da374a
refactor: small cleans
Devessier Feb 19, 2026
18ac71a
refactor: extract to effect component
Devessier Feb 19, 2026
0811f3c
fix: explicitly dissociate record page layouts and dashboards
Devessier Feb 19, 2026
1a687ee
refactor: clean
Devessier Feb 19, 2026
23b02b7
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 19, 2026
3bde720
feat: mock general group for view fields without view group
Devessier Feb 19, 2026
6d7f9fd
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 20, 2026
55c681b
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 20, 2026
c2d1dd9
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 20, 2026
cbd435d
Merge branch 'main' into wire-fields-widget-to-backend
Devessier Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 250 additions & 28 deletions packages/twenty-front/src/generated-metadata/graphql.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';

export const CancelRecordPageLayoutSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();

const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();

const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
id: recordId,
targetObjectNameSingular: objectMetadataItem.nameSingular,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { useResetLocationHash } from 'twenty-ui/utilities';

export const EditRecordPageLayoutSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();

const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();

const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
id: recordId,
targetObjectNameSingular: objectMetadataItem.nameSingular,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { Action } from '@/action-menu/actions/components/Action';
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';

export const SaveRecordPageLayoutSingleRecordAction = () => {
const recordId = useSelectedRecordIdOrThrow();

const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();

const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
id: recordId,
targetObjectNameSingular: objectMetadataItem.nameSingular,
});

const { savePageLayout } = useSavePageLayout(pageLayoutId);
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups({
pageLayoutId,
});

const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
Expand All @@ -27,6 +27,8 @@ export const SaveRecordPageLayoutSingleRecordAction = () => {
const result = await savePageLayout();

if (result.status === 'successful') {
await saveFieldsWidgetGroups();

closeCommandMenu();
setIsPageLayoutInEditMode(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRecoilCallback } from 'recoil';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { hasInitializedFieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/hasInitializedFieldsWidgetGroupsDraftComponentState';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
Expand Down Expand Up @@ -69,6 +70,13 @@ export const useExecuteTasksOnAnyLocationChange = () => {
false,
);

set(
hasInitializedFieldsWidgetGroupsDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
{},
);

set(currentPageLayoutIdState, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/co
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
import { FieldsConfigurationEditor } from '@/page-layout/widgets/fields/components/FieldsConfigurationEditor';
import { FieldsWidgetGroupsDraftInitializationEffect } from '@/page-layout/widgets/fields/components/FieldsWidgetGroupsDraftInitializationEffect';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import {
type FieldsConfiguration,
WidgetConfigurationType,
} from '~/generated-metadata/graphql';

const StyledOuterContainer = styled.div`
display: flex;
Expand All @@ -28,25 +31,23 @@ const StyledContainer = styled.div`
export const CommandMenuPageLayoutFieldsLayout = () => {
const { goBackFromCommandMenu } = useCommandMenuHistory();

const { pageLayoutId, objectNameSingular } =
const { pageLayoutId } =
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();

const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const defaultFieldsConfiguration =
useTemporaryFieldsConfiguration(objectNameSingular);
const [fieldsConfiguration, setFieldsConfiguration] =
useState<FieldsConfiguration>(defaultFieldsConfiguration);
const temporaryFieldsConfiguration = useTemporaryFieldsConfiguration();

if (!isDefined(widgetInEditMode)) {
return null;
}

const handleConfigurationChange = (
updatedConfiguration: FieldsConfiguration,
) => {
// TODO: replace with a call to updatePageLayoutWidget
setFieldsConfiguration(updatedConfiguration);
};
const widgetConfiguration = widgetInEditMode.configuration;

const fieldsConfiguration: FieldsConfiguration =
isDefined(widgetConfiguration) &&
widgetConfiguration.configurationType === WidgetConfigurationType.FIELDS
? (widgetConfiguration as FieldsConfiguration)
: temporaryFieldsConfiguration;

return (
<StyledOuterContainer>
Expand All @@ -55,9 +56,14 @@ export const CommandMenuPageLayoutFieldsLayout = () => {
onBackClick={goBackFromCommandMenu}
/>
<StyledContainer>
<FieldsWidgetGroupsDraftInitializationEffect
viewId={fieldsConfiguration.viewId ?? null}
pageLayoutId={pageLayoutId}
widgetId={widgetInEditMode.id}
/>
<FieldsConfigurationEditor
configuration={fieldsConfiguration}
onChange={handleConfigurationChange}
pageLayoutId={pageLayoutId}
widgetId={widgetInEditMode.id}
/>
</StyledContainer>
</StyledOuterContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layo
import { usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord';
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useTemporaryFieldsConfiguration } from '@/page-layout/hooks/useTemporaryFieldsConfiguration';
import { useFieldsWidgetGroups } from '@/page-layout/widgets/fields/hooks/useFieldsWidgetGroups';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconLayoutSidebarRight } from 'twenty-ui/display';
import { type FieldsConfiguration } from '~/generated-metadata/graphql';

const StyledContainer = styled.div`
display: flex;
Expand All @@ -33,15 +34,22 @@ export const CommandMenuPageLayoutFieldsSettings = () => {
usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord();

const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
const fieldsConfiguration =
useTemporaryFieldsConfiguration(objectNameSingular);

const fieldsConfiguration = widgetInEditMode?.configuration as
| FieldsConfiguration
| undefined;

const { groups } = useFieldsWidgetGroups({
viewId: fieldsConfiguration?.viewId ?? null,
objectNameSingular,
});

if (!isDefined(widgetInEditMode)) {
return null;
}

const totalFieldsCount = fieldsConfiguration.sections.reduce(
(count, section) => count + section.fields.length,
const totalFieldsCount = groups.reduce(
(count, group) => count + group.fields.length,
0,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/
import { getDateGranularityLabel } from '@/command-menu/pages/page-layout/utils/getDateGranularityLabel';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponentInstanceContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
Expand All @@ -17,7 +16,10 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/jotai/hooks/useR
import { ObjectRecordGroupByDateGranularity } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type WidgetConfiguration } from '~/generated-metadata/graphql';
import {
type FieldsConfiguration,
type WidgetConfiguration,
} from '~/generated-metadata/graphql';

type ChartDateGranularitySelectionDropdownContentProps = {
axis?: 'primary' | 'secondary';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ export const usePageLayoutIdForRecordPageLayoutFromContextStoreTargetedRecord =
throw new Error('Only one record should be selected');
}

const recordId: string = targetedRecordsRule.selectedRecordIds[0];

const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
id: recordId,
targetObjectNameSingular: objectMetadataItem.nameSingular,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { type ChartWidget } from '@/command-menu/pages/page-layout/types/ChartWidget';
import { isWidgetConfigurationOfTypeGraph } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfTypeGraph';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { type WidgetConfiguration } from '~/generated-metadata/graphql';

export const isChartWidget = (
pageLayoutWidget: PageLayoutWidget,
): pageLayoutWidget is ChartWidget => {
return isWidgetConfigurationOfTypeGraph(
// TODO: Remove this cast when we FieldsConfiguration and FieldConfiguration are in the backend
pageLayoutWidget.configuration as WidgetConfiguration,
);
return isWidgetConfigurationOfTypeGraph(pageLayoutWidget.configuration);
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
import {
type AggregateChartConfiguration,
type BarChartConfiguration,
type CalendarConfiguration,
type EmailsConfiguration,
type FieldRichTextConfiguration,
type FieldsConfiguration,
type FilesConfiguration,
type FrontComponentConfiguration,
type GaugeChartConfiguration,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { type ChartConfiguration } from '@/command-menu/pages/page-layout/types/ChartConfiguration';
import { isWidgetConfigurationOfType } from '@/command-menu/pages/page-layout/utils/isWidgetConfigurationOfType';
import { type FieldConfiguration } from '@/page-layout/types/FieldConfiguration';
import { type FieldsConfiguration } from '@/page-layout/types/FieldsConfiguration';
import { type WidgetConfiguration } from '~/generated-metadata/graphql';
import {
type FieldsConfiguration,
type WidgetConfiguration,
} from '~/generated-metadata/graphql';

export const isWidgetConfigurationOfTypeGraph = (
configuration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@ const renderHooks = ({
isCompact: false,
openRecordIn: ViewOpenRecordIn.SIDE_PANEL,
viewFields: [],
viewFieldGroups: [],
viewGroups: [],
viewSorts: [],
viewFilters: [],
viewFilterGroups: [],
kanbanAggregateOperation: AggregateOperations.COUNT,
icon: '',
kanbanAggregateOperationFieldMetadataId: '',
position: 0,
viewFilters: [],
visibility: ViewVisibility.WORKSPACE,
createdByUserWorkspaceId: null,
shouldHideEmptyGroups: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
updateCurrentViewParams.mainGroupByFieldMetadataId = null;
return await updateCurrentView(updateCurrentViewParams);
}
case ViewType.FieldsWidget: {
return;
}
default: {
return assertUnreachable(viewType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-r
import { RecordShowEffect } from '@/object-record/record-show/components/RecordShowEffect';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { PageLayoutRenderer } from '@/page-layout/components/PageLayoutRenderer';
import { useRecordPageLayoutId } from '@/page-layout/hooks/useRecordPageLayoutId';
import { usePageLayoutIdForRecord } from '@/page-layout/hooks/usePageLayoutIdForRecord';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { type TargetRecordIdentifier } from '@/ui/layout/contexts/TargetRecordIdentifier';
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
Expand Down Expand Up @@ -50,7 +50,7 @@ export const PageLayoutRecordPageRenderer = ({
}),
);

const { pageLayoutId } = useRecordPageLayoutId({
const { pageLayoutId } = usePageLayoutIdForRecord({
id: targetRecordIdentifier.id,
targetObjectNameSingular: targetRecordIdentifier.targetObjectNameSingular,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getPageLayoutVerticalListViewerVariant } from '@/page-layout/components/utils/getPageLayoutVerticalListViewerVariant';

describe('getPageLayoutVerticalListViewerVariant', () => {
it('should return side-column when isInPinnedTab is true', () => {
expect(
getPageLayoutVerticalListViewerVariant({
isInPinnedTab: true,
isMobile: false,
isInRightDrawer: false,
}),
).toBe('side-column');
});

it('should return side-column when isMobile is true', () => {
expect(
getPageLayoutVerticalListViewerVariant({
isInPinnedTab: false,
isMobile: true,
isInRightDrawer: false,
}),
).toBe('side-column');
});

it('should return side-column when isInRightDrawer is true', () => {
expect(
getPageLayoutVerticalListViewerVariant({
isInPinnedTab: false,
isMobile: false,
isInRightDrawer: true,
}),
).toBe('side-column');
});

it('should return default when none of the conditions are true', () => {
expect(
getPageLayoutVerticalListViewerVariant({
isInPinnedTab: false,
isMobile: false,
isInRightDrawer: false,
}),
).toBe('default');
});
});
Loading
Loading