diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
index 277d2c05c9..4853cdf37e 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
@@ -837,6 +837,7 @@ enum ViewType {
TABLE
KANBAN
CALENDAR
+ LIST
FIELDS_WIDGET
TABLE_WIDGET
KANBAN_WIDGET
@@ -1805,6 +1806,7 @@ enum FeatureFlagKey {
IS_CALENDAR_WEEK_VIEW_ENABLED
IS_EMAIL_GROUP_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
+ IS_LIST_VIEW_ENABLED
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED
IS_SETTINGS_DISCOVERY_HERO_ENABLED
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
index 7d8181496f..cd418a4803 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
@@ -585,7 +585,7 @@ export interface View {
__typename: 'View'
}
-export type ViewType = 'TABLE' | 'KANBAN' | 'CALENDAR' | 'FIELDS_WIDGET' | 'TABLE_WIDGET' | 'KANBAN_WIDGET' | 'CALENDAR_WIDGET'
+export type ViewType = 'TABLE' | 'KANBAN' | 'CALENDAR' | 'LIST' | 'FIELDS_WIDGET' | 'TABLE_WIDGET' | 'KANBAN_WIDGET' | 'CALENDAR_WIDGET'
export type ViewKey = 'INDEX'
@@ -1446,7 +1446,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
-export type FeatureFlagKey = 'IS_APP_CLAIMING_ENABLED' | 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_CALENDAR_WEEK_VIEW_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_WORKFLOW_VERSION_IN_CORE_ENABLED' | 'IS_WORKFLOW_DISPATCH_FROM_CORE_ENABLED'
+export type FeatureFlagKey = 'IS_APP_CLAIMING_ENABLED' | 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_CALENDAR_WEEK_VIEW_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_LIST_VIEW_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_WORKFLOW_VERSION_IN_CORE_ENABLED' | 'IS_WORKFLOW_DISPATCH_FROM_CORE_ENABLED'
export interface WorkspaceUrls {
customUrl?: Scalars['String']
@@ -9440,6 +9440,7 @@ export const enumViewType = {
TABLE: 'TABLE' as const,
KANBAN: 'KANBAN' as const,
CALENDAR: 'CALENDAR' as const,
+ LIST: 'LIST' as const,
FIELDS_WIDGET: 'FIELDS_WIDGET' as const,
TABLE_WIDGET: 'TABLE_WIDGET' as const,
KANBAN_WIDGET: 'KANBAN_WIDGET' as const,
@@ -9680,6 +9681,7 @@ export const enumFeatureFlagKey = {
IS_CALENDAR_WEEK_VIEW_ENABLED: 'IS_CALENDAR_WEEK_VIEW_ENABLED' as const,
IS_EMAIL_GROUP_ENABLED: 'IS_EMAIL_GROUP_ENABLED' as const,
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
+ IS_LIST_VIEW_ENABLED: 'IS_LIST_VIEW_ENABLED' as const,
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const,
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' as const,
IS_SETTINGS_DISCOVERY_HERO_ENABLED: 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' as const,
diff --git a/packages/twenty-docs/developers/extend/apps/layout/views.mdx b/packages/twenty-docs/developers/extend/apps/layout/views.mdx
index a24aa06329..5f034c3f2f 100644
--- a/packages/twenty-docs/developers/extend/apps/layout/views.mdx
+++ b/packages/twenty-docs/developers/extend/apps/layout/views.mdx
@@ -43,7 +43,7 @@ export default defineView({
| Property | Values | Description |
|----------|--------|-------------|
-| `type` | `ViewType.TABLE` (default), `ViewType.KANBAN`, `ViewType.CALENDAR` | How records are laid out. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, and `CALENDAR_WIDGET` also exist but are used internally by page-layout widgets.) |
+| `type` | `ViewType.TABLE` (default), `ViewType.LIST`, `ViewType.KANBAN`, `ViewType.CALENDAR` | How records are laid out. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, and `CALENDAR_WIDGET` also exist but are used internally by page-layout widgets.) |
| `visibility` | `ViewVisibility.WORKSPACE` (default), `ViewVisibility.UNLISTED` | Whether the view is listed for the whole workspace or hidden from pickers. |
| `openRecordIn` | deprecated | No longer read: where records open is now a property of the [object](/developers/extend/apps/data/objects) (`openRecordIn` on `defineObject()`), falling back to each member's own preference. |
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Default sort order. |
diff --git a/packages/twenty-front/src/generated-admin/graphql.ts b/packages/twenty-front/src/generated-admin/graphql.ts
index e72a841bc1..115f44fb70 100644
--- a/packages/twenty-front/src/generated-admin/graphql.ts
+++ b/packages/twenty-front/src/generated-admin/graphql.ts
@@ -373,6 +373,7 @@ export enum FeatureFlagKey {
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
+ IS_LIST_VIEW_ENABLED = 'IS_LIST_VIEW_ENABLED',
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 6a3d069019..073c484220 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1819,6 +1819,7 @@ export enum FeatureFlagKey {
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
+ IS_LIST_VIEW_ENABLED = 'IS_LIST_VIEW_ENABLED',
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
@@ -6486,6 +6487,7 @@ export enum ViewType {
FIELDS_WIDGET = 'FIELDS_WIDGET',
KANBAN = 'KANBAN',
KANBAN_WIDGET = 'KANBAN_WIDGET',
+ LIST = 'LIST',
TABLE = 'TABLE',
TABLE_WIDGET = 'TABLE_WIDGET'
}
diff --git a/packages/twenty-front/src/modules/command-menu-item/utils/getCommandMenuItemLabel.ts b/packages/twenty-front/src/modules/command-menu-item/utils/getCommandMenuItemLabel.ts
index b432217c4f..92ad843f16 100644
--- a/packages/twenty-front/src/modules/command-menu-item/utils/getCommandMenuItemLabel.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/utils/getCommandMenuItemLabel.ts
@@ -9,5 +9,11 @@ export const getCommandMenuItemLabel = (
return '';
}
- return i18n._(label);
+ if (typeof label === 'string') {
+ return i18n._(label);
+ }
+
+ const { id, values, ...options } = label;
+
+ return i18n._(id, values, options);
};
diff --git a/packages/twenty-front/src/modules/dropdown-context-state-management/hooks/useDropdownContextStateManagement.ts b/packages/twenty-front/src/modules/dropdown-context-state-management/hooks/useDropdownContextStateManagement.ts
index 659084a37d..7010b6e11e 100644
--- a/packages/twenty-front/src/modules/dropdown-context-state-management/hooks/useDropdownContextStateManagement.ts
+++ b/packages/twenty-front/src/modules/dropdown-context-state-management/hooks/useDropdownContextStateManagement.ts
@@ -1,6 +1,6 @@
import { isDefined } from 'twenty-shared/utils';
import { type ObjectOptionsDropdownContextValue } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
-import { type RecordBoardColumnHeaderAggregateDropdownContextValue } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
+import { type RecordGroupAggregateDropdownContextValue } from '@/object-record/record-group/types/RecordGroupAggregateDropdownContextValue';
import { type RecordTableColumnAggregateFooterDropdownContextValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { type Context, useCallback, useContext } from 'react';
@@ -13,7 +13,7 @@ import { type Context, useCallback, useContext } from 'react';
*/
export const useDropdownContextStateManagement = <
T extends
- | RecordBoardColumnHeaderAggregateDropdownContextValue
+ | RecordGroupAggregateDropdownContextValue
| RecordTableColumnAggregateFooterDropdownContextValue
| ObjectOptionsDropdownContextValue,
>({
diff --git a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts
index ad41cff66a..0dbbe8fe32 100644
--- a/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts
+++ b/packages/twenty-front/src/modules/metadata-store/hooks/useLoadStaleMetadataEntities.ts
@@ -43,7 +43,12 @@ const PAGE_LAYOUTS_GROUP_KEYS: MetadataEntityKey[] = [
'pageLayoutWidgets',
];
-const INDEX_VIEW_TYPES = [ViewType.TABLE, ViewType.KANBAN, ViewType.CALENDAR];
+const INDEX_VIEW_TYPES = [
+ ViewType.TABLE,
+ ViewType.KANBAN,
+ ViewType.CALENDAR,
+ ViewType.LIST,
+];
const FIELDS_WIDGET_VIEW_TYPES = [ViewType.FIELDS_WIDGET];
const WIDGET_VIEW_TYPES = [
ViewType.TABLE_WIDGET,
diff --git a/packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx b/packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx
index 52f23dcdd8..03f54f30a0 100644
--- a/packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx
+++ b/packages/twenty-front/src/modules/object-record/components/RecordComponentInstanceContextsWrapper.tsx
@@ -1,5 +1,6 @@
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
+import { RecordListComponentInstanceContext } from '@/object-record/record-list/states/contexts/RecordListComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
@@ -29,7 +30,11 @@ export const RecordComponentInstanceContextsWrapper = ({
- {children}
+
+ {children}
+
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx
index c1c2b67b27..a79333fdac 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent.tsx
@@ -67,6 +67,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
const recordIndexCalendarLayout = useAtomComponentStateValue(
recordIndexCalendarLayoutComponentState,
);
+ const isListViewEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_LIST_VIEW_ENABLED,
+ );
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
);
@@ -124,8 +127,9 @@ export const ObjectOptionsDropdownLayoutContent = () => {
const selectableItemIdArray = [
ViewType.TABLE,
- ...(isDefaultView ? [] : [ViewType.KANBAN]),
+ ...(isListViewEnabled ? [ViewType.LIST] : []),
...(!isDefaultView ? [ViewType.CALENDAR] : []),
+ ...(isDefaultView ? [] : [ViewType.KANBAN]),
...(currentView?.type === ViewType.KANBAN ? ['Group'] : []),
...(currentView?.type === ViewType.CALENDAR
? [
@@ -135,7 +139,10 @@ export const ObjectOptionsDropdownLayoutContent = () => {
: 'CalendarDateField',
]
: []),
- ...(currentView?.type !== ViewType.TABLE ? ['Compact view'] : []),
+ ...(currentView?.type !== ViewType.TABLE &&
+ currentView?.type !== ViewType.LIST
+ ? ['Compact view']
+ : []),
];
const selectedItemId = useAtomComponentStateValue(
@@ -181,6 +188,26 @@ export const ObjectOptionsDropdownLayoutContent = () => {
}}
/>
+ {isListViewEnabled && (
+ {
+ setAndPersistViewType(ViewType.LIST);
+ }}
+ >
+ {
+ if (currentView?.type !== ViewType.LIST) {
+ await setAndPersistViewType(ViewType.LIST);
+ }
+ }}
+ />
+
+ )}
{
@@ -305,31 +332,32 @@ export const ObjectOptionsDropdownLayoutContent = () => {
/>
)}
- {currentView?.type !== ViewType.TABLE && (
- {
- setAndPersistIsCompactModeActive(
- !isCompactModeActive,
- currentView,
- );
- }}
- >
-
+ {currentView?.type !== ViewType.TABLE &&
+ currentView?.type !== ViewType.LIST && (
+ {
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
- )
- }
- toggled={isCompactModeActive}
- text={t`Compact view`}
- toggleSize="small"
- />
-
- )}
+ );
+ }}
+ >
+
+ setAndPersistIsCompactModeActive(
+ !isCompactModeActive,
+ currentView,
+ )
+ }
+ toggled={isCompactModeActive}
+ text={t`Compact view`}
+ toggleSize="small"
+ />
+
+ )}
)}
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts
index 12987c1c62..9ed7397b52 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSetViewTypeFromLayoutOptionsMenu.ts
@@ -73,7 +73,8 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
await updateCurrentView(updateCurrentViewParams);
return;
}
- case ViewType.TABLE: {
+ case ViewType.TABLE:
+ case ViewType.LIST: {
if (shouldChangeIcon(currentView.icon, currentView.type)) {
updateCurrentViewParams.icon =
viewTypeIconMapping(viewType).displayName;
@@ -158,6 +159,12 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
) {
return true;
}
+ if (
+ oldViewType === ViewType.LIST &&
+ oldIcon === viewTypeIconMapping(ViewType.LIST).displayName
+ ) {
+ return true;
+ }
return false;
};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx
index efab2d7b59..990814b675 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCardBody.tsx
@@ -19,6 +19,7 @@ import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFi
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useContext } from 'react';
+import { isDefined } from 'twenty-shared/utils';
export const RecordBoardCardBody = () => {
const { recordId, isRecordReadOnly, isDragOverlay } = useContext(
@@ -70,6 +71,10 @@ export const RecordBoardCardBody = () => {
const correspondingFieldDefinition =
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
+ if (!isDefined(correspondingFieldDefinition)) {
+ return null;
+ }
+
return (
{
isNonInteractive={isRecordBoardViewSettingsReadOnly}
inert={isRecordBoardViewSettingsReadOnly || undefined}
>
- void;
- resetContent: () => void;
- previousContentId: RecordBoardColumnHeaderAggregateContentId | null;
- dropdownId: string;
-};
-
-export const RecordBoardColumnHeaderAggregateDropdownContext =
- createContext(
- {} as RecordBoardColumnHeaderAggregateDropdownContextValue,
- );
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdown.tsx
similarity index 52%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx
rename to packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdown.tsx
index ab1cbe6430..632f4b9045 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdown.tsx
@@ -1,16 +1,16 @@
import { useDropdownContextCurrentContentId } from '@/dropdown-context-state-management/hooks/useDropdownContextCurrentContentId';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
-import { RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext } from '@/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext';
-import { RecordBoardColumnHeaderAggregateDropdownButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton';
-import { AggregateDropdownContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent';
-import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
-import { type RecordBoardColumnHeaderAggregateContentId } from '@/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId';
+import { RecordGroupAggregateDropdownButton } from '@/object-record/record-group/components/RecordGroupAggregateDropdownButton';
+import { RecordGroupAggregateDropdownContent } from '@/object-record/record-group/components/RecordGroupAggregateDropdownContent';
+import { RecordGroupAggregateDropdownComponentInstanceContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownComponentInstanceContext';
+import { RecordGroupAggregateDropdownContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownContext';
+import { type RecordGroupAggregateContentId } from '@/object-record/record-group/types/RecordGroupAggregateContentId';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DROPDOWN_OFFSET_Y } from '@/ui/layout/dropdown/constants/DropdownOffsetY';
import { styled } from '@linaria/react';
import { type Nullable } from 'twenty-shared/types';
-type RecordBoardColumnHeaderAggregateDropdownProps = {
+type RecordGroupAggregateDropdownProps = {
aggregateValue?: Nullable;
aggregateLabel?: Nullable;
objectMetadataItem: EnrichedObjectMetadataItem;
@@ -21,22 +21,21 @@ const StyledContainer = styled.div`
flex-shrink: 0;
`;
-export const RecordBoardColumnHeaderAggregateDropdown = ({
+export const RecordGroupAggregateDropdown = ({
objectMetadataItem,
aggregateValue,
aggregateLabel,
dropdownId,
-}: RecordBoardColumnHeaderAggregateDropdownProps) => {
+}: RecordGroupAggregateDropdownProps) => {
const {
currentContentId,
handleContentChange,
handleResetContent,
previousContentId,
- } =
- useDropdownContextCurrentContentId();
+ } = useDropdownContextCurrentContentId();
return (
-
@@ -45,14 +44,14 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
dropdownId={dropdownId}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
-
}
dropdownComponents={
-
-
-
+
+
}
/>
-
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownButton.tsx
similarity index 96%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx
rename to packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownButton.tsx
index 606c7f82ff..d256601ea7 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownButton.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownButton.tsx
@@ -17,7 +17,7 @@ const StyledHeaderContainer = styled.div`
}
`;
-export const RecordBoardColumnHeaderAggregateDropdownButton = ({
+export const RecordGroupAggregateDropdownButton = ({
dropdownId,
value,
tooltip,
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownContent.tsx
similarity index 75%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx
rename to packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownContent.tsx
index fb85e08551..1f7aad578d 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownContent.tsx
@@ -1,10 +1,10 @@
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
-import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
-import { RecordBoardColumnHeaderAggregateDropdownFieldsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent';
-import { RecordBoardColumnHeaderAggregateDropdownMenuContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent';
-import { RecordBoardColumnHeaderAggregateDropdownOptionsContent } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent';
+import { RecordGroupAggregateDropdownFieldsContent } from '@/object-record/record-group/components/RecordGroupAggregateDropdownFieldsContent';
+import { RecordGroupAggregateDropdownMenuContent } from '@/object-record/record-group/components/RecordGroupAggregateDropdownMenuContent';
+import { RecordGroupAggregateDropdownOptionsContent } from '@/object-record/record-group/components/RecordGroupAggregateDropdownOptionsContent';
+import { RecordGroupAggregateDropdownContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownContext';
import { DateAggregateOperations } from '@/object-record/record-table/constants/DateAggregateOperations';
import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions';
import { NON_STANDARD_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/nonStandardAggregateOperationsOptions';
@@ -14,10 +14,10 @@ import { getAvailableFieldsIdsForAggregationFromObjectFields } from '@/object-re
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { t } from '@lingui/core/macro';
-export const AggregateDropdownContent = () => {
+export const RecordGroupAggregateDropdownContent = () => {
const { currentContentId, objectMetadataItem } =
useDropdownContextStateManagement({
- context: RecordBoardColumnHeaderAggregateDropdownContext,
+ context: RecordGroupAggregateDropdownContext,
});
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -42,7 +42,7 @@ export const AggregateDropdownContent = () => {
targetAggregateOperations: COUNT_AGGREGATE_OPERATION_OPTIONS,
});
return (
-
@@ -55,7 +55,7 @@ export const AggregateDropdownContent = () => {
targetAggregateOperations: PERCENT_AGGREGATE_OPERATION_OPTIONS,
});
return (
-
@@ -71,7 +71,7 @@ export const AggregateDropdownContent = () => {
],
});
return (
-
@@ -84,15 +84,15 @@ export const AggregateDropdownContent = () => {
targetAggregateOperations: NON_STANDARD_AGGREGATE_OPERATION_OPTIONS,
});
return (
-
);
}
case 'aggregateFields':
- return ;
+ return ;
default:
- return ;
+ return ;
}
};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownFieldsContent.tsx
similarity index 88%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx
rename to packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownFieldsContent.tsx
index 4675c5fe64..5cbaf1e911 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownFieldsContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownFieldsContent.tsx
@@ -1,8 +1,8 @@
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
-import { RecordBoardColumnHeaderAggregateDropdownContext } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
-import { aggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/aggregateOperationComponentState';
-import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
+import { RecordGroupAggregateDropdownContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownContext';
+import { aggregateOperationComponentState } from '@/object-record/record-group/states/aggregateOperationComponentState';
+import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-group/states/availableFieldIdsForAggregateOperationComponentState';
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -15,7 +15,7 @@ import { isDefined } from 'twenty-shared/utils';
import { Icon123, IconCheck, IconChevronLeft, useIcons } from 'twenty-ui/icon';
import { MenuItem } from 'twenty-ui/navigation';
-export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
+export const RecordGroupAggregateDropdownFieldsContent = () => {
const {
closeDropdown,
objectMetadataItem,
@@ -23,7 +23,7 @@ export const RecordBoardColumnHeaderAggregateDropdownFieldsContent = () => {
resetContent,
previousContentId,
} = useDropdownContextStateManagement({
- context: RecordBoardColumnHeaderAggregateDropdownContext,
+ context: RecordGroupAggregateDropdownContext,
});
const { updateViewAggregate } = useUpdateViewAggregate();
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownMenuContent.tsx
similarity index 74%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx
rename to packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownMenuContent.tsx
index 59211df675..f1e08b45bb 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownMenuContent.tsx
@@ -1,21 +1,19 @@
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
-import {
- RecordBoardColumnHeaderAggregateDropdownContext,
- type RecordBoardColumnHeaderAggregateDropdownContextValue,
-} from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
+import { RecordGroupAggregateDropdownContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownContext';
+import { type RecordGroupAggregateDropdownContextValue } from '@/object-record/record-group/types/RecordGroupAggregateDropdownContextValue';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useLingui } from '@lingui/react/macro';
import { MenuItem } from 'twenty-ui/navigation';
-export const RecordBoardColumnHeaderAggregateDropdownMenuContent = () => {
+export const RecordGroupAggregateDropdownMenuContent = () => {
const { t } = useLingui();
const { onContentChange } =
- useDropdownContextStateManagement(
+ useDropdownContextStateManagement(
{
- context: RecordBoardColumnHeaderAggregateDropdownContext,
+ context: RecordGroupAggregateDropdownContext,
},
);
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownMenuItem.tsx
similarity index 86%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuItem.tsx
rename to packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownMenuItem.tsx
index 1d51ddba4c..c761883cbc 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuItem.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownMenuItem.tsx
@@ -1,7 +1,7 @@
import { type IconComponent } from 'twenty-ui/icon';
import { MenuItem } from 'twenty-ui/navigation';
-export const RecordBoardColumnHeaderAggregateDropdownMenuItem = ({
+export const RecordGroupAggregateDropdownMenuItem = ({
onContentChange,
text,
hasSubMenu,
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownOptionsContent.tsx
similarity index 83%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx
rename to packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownOptionsContent.tsx
index b1366696dd..29eca7e139 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownOptionsContent.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupAggregateDropdownOptionsContent.tsx
@@ -1,12 +1,10 @@
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
-import {
- RecordBoardColumnHeaderAggregateDropdownContext,
- type RecordBoardColumnHeaderAggregateDropdownContextValue,
-} from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
-import { RecordBoardColumnHeaderAggregateDropdownMenuItem } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownMenuItem';
-import { aggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/aggregateOperationComponentState';
-import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState';
import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel';
+import { RecordGroupAggregateDropdownMenuItem } from '@/object-record/record-group/components/RecordGroupAggregateDropdownMenuItem';
+import { RecordGroupAggregateDropdownContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownContext';
+import { type RecordGroupAggregateDropdownContextValue } from '@/object-record/record-group/types/RecordGroupAggregateDropdownContextValue';
+import { aggregateOperationComponentState } from '@/object-record/record-group/states/aggregateOperationComponentState';
+import { availableFieldIdsForAggregateOperationComponentState } from '@/object-record/record-group/states/availableFieldIdsForAggregateOperationComponentState';
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
@@ -21,7 +19,7 @@ import { useUpdateViewAggregate } from '@/views/hooks/useUpdateViewAggregate';
import isEmpty from 'lodash.isempty';
import { IconCheck, IconChevronLeft } from 'twenty-ui/icon';
-export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
+export const RecordGroupAggregateDropdownOptionsContent = ({
availableAggregations,
title,
}: {
@@ -29,9 +27,9 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
title: string;
}) => {
const { onContentChange, closeDropdown, resetContent, objectMetadataItem } =
- useDropdownContextStateManagement(
+ useDropdownContextStateManagement(
{
- context: RecordBoardColumnHeaderAggregateDropdownContext,
+ context: RecordGroupAggregateDropdownContext,
},
);
@@ -69,7 +67,7 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
availableAggregationOperation,
availableAggregationFieldsIdsForOperation,
]) => (
- {
if (
diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupSectionHeader.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupSectionHeader.tsx
new file mode 100644
index 0000000000..547686aedf
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupSectionHeader.tsx
@@ -0,0 +1,72 @@
+import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
+import { RecordGroupChip } from '@/object-record/record-group/components/RecordGroupChip';
+import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
+import { styled } from '@linaria/react';
+import { t } from '@lingui/core/macro';
+import { useContext } from 'react';
+import { IconChevronDown } from 'twenty-ui/icon';
+import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledSectionToggle = styled.button`
+ align-items: center;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+ height: 100%;
+ min-width: 0;
+ overflow: hidden;
+ padding: 0;
+`;
+
+const StyledChevronContainer = styled.div<{ isExpanded: boolean }>`
+ align-items: center;
+ display: flex;
+ flex-shrink: 0;
+ justify-content: center;
+ transform: rotate(${({ isExpanded }) => (isExpanded ? 0 : -90)}deg);
+`;
+
+type RecordGroupSectionHeaderProps = {
+ recordGroupDefinition: RecordGroupDefinition;
+ fieldMetadataItem?: FieldMetadataItem | null;
+ isExpanded: boolean;
+ onToggle: () => void;
+ chevronWidth?: number;
+};
+
+export const RecordGroupSectionHeader = ({
+ recordGroupDefinition,
+ fieldMetadataItem,
+ isExpanded,
+ onToggle,
+ chevronWidth,
+}: RecordGroupSectionHeaderProps) => {
+ const { theme } = useContext(ThemeContext);
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/aggregateOperationComponentState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/aggregateOperationComponentState.ts
similarity index 63%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/aggregateOperationComponentState.ts
rename to packages/twenty-front/src/modules/object-record/record-group/states/aggregateOperationComponentState.ts
index ab1284914c..083c29a832 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/aggregateOperationComponentState.ts
+++ b/packages/twenty-front/src/modules/object-record/record-group/states/aggregateOperationComponentState.ts
@@ -1,4 +1,4 @@
-import { RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext } from '@/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext';
+import { RecordGroupAggregateDropdownComponentInstanceContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownComponentInstanceContext';
import { type ExtendedAggregateOperations } from '@/object-record/record-table/types/ExtendedAggregateOperations';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
@@ -7,5 +7,5 @@ export const aggregateOperationComponentState =
key: 'aggregateOperationComponentFamilyState',
defaultValue: null,
componentInstanceContext:
- RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext,
+ RecordGroupAggregateDropdownComponentInstanceContext,
});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/availableFieldIdsForAggregateOperationComponentState.ts
similarity index 56%
rename from packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState.ts
rename to packages/twenty-front/src/modules/object-record/record-group/states/availableFieldIdsForAggregateOperationComponentState.ts
index c9fa54076e..bb7204faa0 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/states/availableFieldIdsForAggregateOperationComponentState.ts
+++ b/packages/twenty-front/src/modules/object-record/record-group/states/availableFieldIdsForAggregateOperationComponentState.ts
@@ -1,4 +1,4 @@
-import { RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext } from '@/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext';
+import { RecordGroupAggregateDropdownComponentInstanceContext } from '@/object-record/record-group/states/context/RecordGroupAggregateDropdownComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const availableFieldIdsForAggregateOperationComponentState =
@@ -6,5 +6,5 @@ export const availableFieldIdsForAggregateOperationComponentState =
key: 'availableFieldIdsForAggregateOperationComponentFamilyState',
defaultValue: [],
componentInstanceContext:
- RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext,
+ RecordGroupAggregateDropdownComponentInstanceContext,
});
diff --git a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupAggregateDropdownComponentInstanceContext.ts
similarity index 66%
rename from packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.ts
rename to packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupAggregateDropdownComponentInstanceContext.ts
index f16fbf6c4e..ef5720131b 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupAggregateDropdownComponentInstanceContext.ts
@@ -1,4 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
-export const RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext =
+export const RecordGroupAggregateDropdownComponentInstanceContext =
createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupAggregateDropdownContext.tsx b/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupAggregateDropdownContext.tsx
new file mode 100644
index 0000000000..9e88ee2ef0
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-group/states/context/RecordGroupAggregateDropdownContext.tsx
@@ -0,0 +1,7 @@
+import { type RecordGroupAggregateDropdownContextValue } from '@/object-record/record-group/types/RecordGroupAggregateDropdownContextValue';
+import { createContext } from 'react';
+
+export const RecordGroupAggregateDropdownContext =
+ createContext(
+ {} as RecordGroupAggregateDropdownContextValue,
+ );
diff --git a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupAggregateContentId.ts
similarity index 78%
rename from packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts
rename to packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupAggregateContentId.ts
index d342eade68..e02b817f88 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts
+++ b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupAggregateContentId.ts
@@ -1,4 +1,4 @@
-export type RecordBoardColumnHeaderAggregateContentId =
+export type RecordGroupAggregateContentId =
| 'aggregateOperations'
| 'aggregateFields'
| 'countAggregateOperationsOptions'
diff --git a/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupAggregateDropdownContextValue.ts b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupAggregateDropdownContextValue.ts
new file mode 100644
index 0000000000..af5fdbd802
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-group/types/RecordGroupAggregateDropdownContextValue.ts
@@ -0,0 +1,11 @@
+import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
+import { type RecordGroupAggregateContentId } from '@/object-record/record-group/types/RecordGroupAggregateContentId';
+
+export type RecordGroupAggregateDropdownContextValue = {
+ objectMetadataItem: EnrichedObjectMetadataItem;
+ currentContentId: RecordGroupAggregateContentId | null;
+ onContentChange: (key: RecordGroupAggregateContentId) => void;
+ resetContent: () => void;
+ previousContentId: RecordGroupAggregateContentId | null;
+ dropdownId: string;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
index d1cdc9e9a4..3848536dfc 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
@@ -8,6 +8,7 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordIndexCalendarContainer } from '@/object-record/record-index/components/RecordIndexCalendarContainer';
+import { RecordIndexListContainer } from '@/object-record/record-index/components/RecordIndexListContainer';
import { RecordIndexEmptyStateNotShared } from '@/object-record/record-index/components/RecordIndexEmptyStateNotShared';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { useHasCurrentViewNonReadableFields } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
@@ -67,6 +68,14 @@ export const RecordIndexContainer = () => {
/>
)}
+ {recordIndexViewType === ViewType.LIST && (
+
+
+
+ )}
>
)}
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexListContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexListContainer.tsx
new file mode 100644
index 0000000000..cc1571cd9b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexListContainer.tsx
@@ -0,0 +1,45 @@
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
+import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { RecordList } from '@/object-record/record-list/components/RecordList';
+import { RecordListSSESubscribeEffect } from '@/object-record/record-list/components/RecordListSSESubscribeEffect';
+import { RecordListContextProvider } from '@/object-record/record-list/contexts/RecordListContext';
+
+type RecordIndexListContainerProps = {
+ recordListInstanceId: string;
+ viewBarInstanceId: string;
+};
+
+export const RecordIndexListContainer = ({
+ recordListInstanceId,
+ viewBarInstanceId,
+}: RecordIndexListContainerProps) => {
+ const { objectNameSingular } = useRecordIndexContextOrThrow();
+
+ const { objectMetadataItem } = useObjectMetadataItem({
+ objectNameSingular,
+ });
+
+ const objectPermissions = useObjectPermissionsForObject(
+ objectMetadataItem.id,
+ );
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts
index 91deff9c6a..60427ea020 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexTableQuery.ts
@@ -19,6 +19,7 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
hasNextPage,
queryIdentifier,
loading,
+ error,
totalCount,
fetchMoreRecords,
} = useFindManyRecords({
@@ -29,6 +30,7 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
return {
records,
loading,
+ error,
hasNextPage,
queryIdentifier,
totalCount,
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordList.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordList.tsx
new file mode 100644
index 0000000000..4ca8a14e21
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordList.tsx
@@ -0,0 +1,46 @@
+import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
+import { RecordListBody } from '@/object-record/record-list/components/RecordListBody';
+import { RecordListFieldTooltip } from '@/object-record/record-list/components/RecordListFieldTooltip';
+import { RecordListRecordGroupsBody } from '@/object-record/record-list/components/RecordListRecordGroupsBody';
+import { RecordListResponsiveFieldCountEffect } from '@/object-record/record-list/components/RecordListResponsiveFieldCountEffect';
+import { RecordListComponentInstanceContext } from '@/object-record/record-list/states/contexts/RecordListComponentInstanceContext';
+import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
+import { styled } from '@linaria/react';
+import { useRef } from 'react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledContainer = styled.div`
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ padding: ${themeCssVariables.spacing[2]};
+ padding-left: ${themeCssVariables.spacing[1]};
+`;
+
+export const RecordList = () => {
+ const recordListId = useAvailableComponentInstanceIdOrThrow(
+ RecordListComponentInstanceContext,
+ );
+
+ const hasRecordGroups = useAtomComponentSelectorValue(
+ hasRecordGroupsComponentSelector,
+ );
+
+ const containerRef = useRef(null);
+
+ return (
+
+
+
+ {hasRecordGroups ? : }
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListAddNew.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListAddNew.tsx
new file mode 100644
index 0000000000..87df7a6a4f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListAddNew.tsx
@@ -0,0 +1,110 @@
+import { getFieldMetadataItemGqlFieldName } from '@/object-metadata/utils/getFieldMetadataItemGqlFieldName';
+import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
+import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
+import { useCurrentRecordGroupDefinition } from '@/object-record/record-group/hooks/useCurrentRecordGroupDefinition';
+import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
+import { useRecordListContextOrThrow } from '@/object-record/record-list/contexts/RecordListContext';
+import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
+import { canCreateRecordsForObjectMetadataItem } from '@/object-record/utils/canCreateRecordsForObjectMetadataItem';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { styled } from '@linaria/react';
+import { t } from '@lingui/core/macro';
+import { useContext } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { IconPlus } from 'twenty-ui/icon';
+import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
+import { logError } from '~/utils/logError';
+
+const StyledAddNewRow = styled.button`
+ align-items: center;
+ background: transparent;
+ border: none;
+ border-radius: ${themeCssVariables.border.radius.sm};
+ color: ${themeCssVariables.font.color.tertiary};
+ cursor: pointer;
+ display: flex;
+ font-size: ${themeCssVariables.font.size.md};
+ gap: ${themeCssVariables.spacing[1]};
+ height: 24px;
+ padding: 0 6px;
+ text-align: left;
+
+ &:hover {
+ background: ${themeCssVariables.background.transparent.lighter};
+ }
+`;
+
+export const RecordListAddNew = () => {
+ const { theme } = useContext(ThemeContext);
+ const { objectMetadataItem } = useRecordListContextOrThrow();
+ const { enqueueErrorSnackBar } = useSnackBar();
+
+ const { createNewIndexRecord } = useCreateNewIndexRecord({
+ objectMetadataItem,
+ });
+
+ const currentRecordGroupDefinition = useCurrentRecordGroupDefinition();
+
+ const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
+ recordIndexGroupFieldMetadataItemComponentState,
+ );
+
+ const groupFieldMetadataItem = objectMetadataItem.fields.find(
+ (field) => field.id === recordIndexGroupFieldMetadataItem?.id,
+ );
+
+ const objectPermissions = useObjectPermissionsForObject(
+ objectMetadataItem.id,
+ );
+
+ const hasAnySoftDeleteFilterOnView = useAtomComponentSelectorValue(
+ hasAnySoftDeleteFilterOnViewComponentSelector,
+ );
+
+ if (hasAnySoftDeleteFilterOnView) {
+ return null;
+ }
+
+ if (
+ !canCreateRecordsForObjectMetadataItem({
+ objectPermissions,
+ objectMetadataItem,
+ })
+ ) {
+ return null;
+ }
+
+ const handleCreateRecord = async () => {
+ try {
+ const groupFieldValues =
+ isDefined(currentRecordGroupDefinition) &&
+ isDefined(groupFieldMetadataItem)
+ ? {
+ [getFieldMetadataItemGqlFieldName(groupFieldMetadataItem)]:
+ currentRecordGroupDefinition.value,
+ }
+ : {};
+
+ await createNewIndexRecord({
+ position: 'last',
+ ...groupFieldValues,
+ });
+ } catch (error) {
+ logError(error);
+ enqueueErrorSnackBar({ message: t`Failed to create record` });
+ }
+ };
+
+ return (
+
+
+ {t`Add new`}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListBody.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListBody.tsx
new file mode 100644
index 0000000000..9a09cfc2fb
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListBody.tsx
@@ -0,0 +1,28 @@
+import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery';
+import { RecordListRecords } from '@/object-record/record-list/components/RecordListRecords';
+import { useRecordListContextOrThrow } from '@/object-record/record-list/contexts/RecordListContext';
+import { styled } from '@linaria/react';
+
+const StyledBody = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+export const RecordListBody = () => {
+ const { objectNameSingular } = useRecordListContextOrThrow();
+
+ const { records, loading, error, hasNextPage, fetchMoreRecords } =
+ useRecordIndexTableQuery(objectNameSingular);
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListEmptyRecordGroupEffect.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListEmptyRecordGroupEffect.tsx
new file mode 100644
index 0000000000..470109875f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListEmptyRecordGroupEffect.tsx
@@ -0,0 +1,48 @@
+import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
+import { emptyRecordGroupByIdComponentFamilyState } from '@/object-record/record-group/states/emptyRecordGroupByIdComponentFamilyState';
+import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
+import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { useAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyState';
+import { useEffect } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+type RecordListEmptyRecordGroupEffectProps = {
+ loading: boolean;
+ error?: Error;
+ records: ObjectRecord[];
+};
+
+export const RecordListEmptyRecordGroupEffect = ({
+ loading,
+ error,
+ records,
+}: RecordListEmptyRecordGroupEffectProps) => {
+ const currentRecordGroupId = useCurrentRecordGroupId();
+
+ const [, setEmptyRecordGroupById] = useAtomComponentFamilyState(
+ emptyRecordGroupByIdComponentFamilyState,
+ currentRecordGroupId,
+ );
+
+ const [, setRecordIndexRecordIdsByGroup] = useAtomComponentFamilyState(
+ recordIndexRecordIdsByGroupComponentFamilyState,
+ currentRecordGroupId,
+ );
+
+ useEffect(() => {
+ if (loading || isDefined(error)) {
+ return;
+ }
+
+ setRecordIndexRecordIdsByGroup(records.map((record) => record.id));
+ setEmptyRecordGroupById(records.length === 0);
+ }, [
+ loading,
+ error,
+ records,
+ setEmptyRecordGroupById,
+ setRecordIndexRecordIdsByGroup,
+ ]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListFieldTooltip.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListFieldTooltip.tsx
new file mode 100644
index 0000000000..1b61f014e8
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListFieldTooltip.tsx
@@ -0,0 +1,69 @@
+import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { RECORD_LIST_ROW_FIELD_ANCHOR_CLASS_NAME } from '@/object-record/record-list/constants/RecordListRowFieldAnchorClassName';
+import { recordListHoveredFieldMetadataItemIdComponentState } from '@/object-record/record-list/states/recordListHoveredFieldMetadataItemIdComponentState';
+import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { styled } from '@linaria/react';
+import { useContext } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { useIcons } from 'twenty-ui/icon';
+import { AppTooltip, TooltipDelay } from 'twenty-ui/surfaces';
+import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledTooltipContent = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[1]};
+`;
+
+export const RecordListFieldTooltip = () => {
+ const { theme } = useContext(ThemeContext);
+ const { getIcon } = useIcons();
+
+ const { fieldDefinitionByFieldMetadataItemId } =
+ useRecordIndexContextOrThrow();
+
+ const recordListHoveredFieldMetadataItemId = useAtomComponentStateValue(
+ recordListHoveredFieldMetadataItemIdComponentState,
+ );
+
+ const visibleRecordFields = useAtomComponentSelectorValue(
+ visibleRecordFieldsComponentSelector,
+ );
+
+ const visibleRecordFieldIds = visibleRecordFields
+ .map((recordField) => recordField.fieldMetadataItemId)
+ .join('-');
+
+ const hoveredFieldDefinition = isDefined(recordListHoveredFieldMetadataItemId)
+ ? fieldDefinitionByFieldMetadataItemId[recordListHoveredFieldMetadataItemId]
+ : undefined;
+
+ const FieldIcon = isDefined(hoveredFieldDefinition)
+ ? getIcon(hoveredFieldDefinition.iconName)
+ : null;
+
+ return (
+
+ {isDefined(hoveredFieldDefinition) && (
+
+ {isDefined(FieldIcon) && (
+
+ )}
+ {hoveredFieldDefinition.label}
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroup.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroup.tsx
new file mode 100644
index 0000000000..588ed4eae6
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroup.tsx
@@ -0,0 +1,55 @@
+import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
+import { useShouldHideRecordGroup } from '@/object-record/record-group/hooks/useShouldHideRecordGroup';
+import { useRecordIndexTableQuery } from '@/object-record/record-index/hooks/useRecordIndexTableQuery';
+import { RecordListEmptyRecordGroupEffect } from '@/object-record/record-list/components/RecordListEmptyRecordGroupEffect';
+import { RecordListRecordGroupSection } from '@/object-record/record-list/components/RecordListRecordGroupSection';
+import { RecordListRecords } from '@/object-record/record-list/components/RecordListRecords';
+import { useRecordListContextOrThrow } from '@/object-record/record-list/contexts/RecordListContext';
+import { isRecordListGroupSectionToggledComponentState } from '@/object-record/record-list/states/isRecordListGroupSectionToggledComponentState';
+import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
+import { styled } from '@linaria/react';
+
+const StyledSection = styled.div`
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 14px;
+`;
+
+export const RecordListRecordGroup = () => {
+ const { objectNameSingular } = useRecordListContextOrThrow();
+
+ const currentRecordGroupId = useCurrentRecordGroupId();
+
+ const shouldHideRecordGroup = useShouldHideRecordGroup(currentRecordGroupId);
+
+ const { records, loading, error, hasNextPage, fetchMoreRecords } =
+ useRecordIndexTableQuery(objectNameSingular);
+
+ const isRecordListGroupSectionToggled = useAtomComponentFamilyStateValue(
+ isRecordListGroupSectionToggledComponentState,
+ currentRecordGroupId,
+ );
+
+ if (shouldHideRecordGroup) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroupSection.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroupSection.tsx
new file mode 100644
index 0000000000..b9a7f84cb6
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroupSection.tsx
@@ -0,0 +1,78 @@
+import { RecordGroupAggregateDropdown } from '@/object-record/record-group/components/RecordGroupAggregateDropdown';
+import { RecordGroupSectionHeader } from '@/object-record/record-group/components/RecordGroupSectionHeader';
+import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
+import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
+import { recordIndexAggregateDisplayLabelComponentState } from '@/object-record/record-index/states/recordIndexAggregateDisplayLabelComponentState';
+import { recordIndexAggregateDisplayValueForGroupValueComponentFamilyState } from '@/object-record/record-index/states/recordIndexAggregateDisplayValueForGroupValueComponentFamilyState';
+import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
+import { useRecordListContextOrThrow } from '@/object-record/record-list/contexts/RecordListContext';
+import { isRecordListGroupSectionToggledComponentState } from '@/object-record/record-list/states/isRecordListGroupSectionToggledComponentState';
+import { useAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyState';
+import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
+import { styled } from '@linaria/react';
+import { isDefined } from 'twenty-shared/utils';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledSectionHeader = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+ height: 32px;
+ margin-bottom: 2px;
+`;
+
+export const RecordListRecordGroupSection = () => {
+ const { objectMetadataItem } = useRecordListContextOrThrow();
+
+ const currentRecordGroupId = useCurrentRecordGroupId();
+
+ const recordGroupDefinition = useAtomFamilyStateValue(
+ recordGroupDefinitionFamilyState,
+ currentRecordGroupId,
+ );
+
+ const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
+ recordIndexGroupFieldMetadataItemComponentState,
+ );
+
+ const recordIndexAggregateDisplayValueForGroupValue =
+ useAtomComponentFamilyStateValue(
+ recordIndexAggregateDisplayValueForGroupValueComponentFamilyState,
+ { groupValue: recordGroupDefinition?.value ?? '' },
+ );
+
+ const recordIndexAggregateDisplayLabel = useAtomComponentStateValue(
+ recordIndexAggregateDisplayLabelComponentState,
+ );
+
+ const [isRecordListGroupSectionToggled, setIsRecordListGroupSectionToggled] =
+ useAtomComponentFamilyState(
+ isRecordListGroupSectionToggledComponentState,
+ currentRecordGroupId,
+ );
+
+ if (!isDefined(recordGroupDefinition)) {
+ return null;
+ }
+
+ return (
+
+
+ setIsRecordListGroupSectionToggled((prevState) => !prevState)
+ }
+ />
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroupsBody.tsx
new file mode 100644
index 0000000000..4e64806ea8
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecordGroupsBody.tsx
@@ -0,0 +1,27 @@
+import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
+import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
+import { RecordIndexGroupAggregatesDataLoader } from '@/object-record/record-index/components/RecordIndexGroupAggregatesDataLoader';
+import { RecordListRecordGroup } from '@/object-record/record-list/components/RecordListRecordGroup';
+import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
+import { ViewType } from '@/views/types/ViewType';
+
+export const RecordListRecordGroupsBody = () => {
+ const visibleRecordGroupIds = useAtomComponentFamilySelectorValue(
+ visibleRecordGroupIdsComponentFamilySelector,
+ ViewType.LIST,
+ );
+
+ return (
+ <>
+ {visibleRecordGroupIds.map((recordGroupId) => (
+
+
+
+ ))}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecords.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecords.tsx
new file mode 100644
index 0000000000..4d42e8104b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRecords.tsx
@@ -0,0 +1,54 @@
+import { RecordListAddNew } from '@/object-record/record-list/components/RecordListAddNew';
+import { RecordListRow } from '@/object-record/record-list/components/RecordListRow';
+import { RecordListUpsertRecordsInStoreEffect } from '@/object-record/record-list/components/RecordListUpsertRecordsInStoreEffect';
+import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { styled } from '@linaria/react';
+import { useInView } from 'react-intersection-observer';
+import { isDefined } from 'twenty-shared/utils';
+
+const StyledFetchMoreTrigger = styled.div`
+ height: 0;
+`;
+
+type RecordListRecordsProps = {
+ records: ObjectRecord[];
+ loading: boolean;
+ error?: Error;
+ hasNextPage: boolean;
+ fetchMoreRecords: () => void;
+ isVisible?: boolean;
+};
+
+export const RecordListRecords = ({
+ records,
+ loading,
+ error,
+ hasNextPage,
+ fetchMoreRecords,
+ isVisible = true,
+}: RecordListRecordsProps) => {
+ const { ref: fetchMoreRef } = useInView({
+ onChange: (inView) => {
+ if (inView && hasNextPage && !loading) {
+ fetchMoreRecords();
+ }
+ },
+ });
+
+ return (
+ <>
+
+ {isVisible && !isDefined(error) && (
+ <>
+ {records.map((record) => (
+
+ ))}
+ {hasNextPage && !loading && (
+
+ )}
+ {!loading && }
+ >
+ )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListResponsiveFieldCountEffect.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListResponsiveFieldCountEffect.tsx
new file mode 100644
index 0000000000..04794365f9
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListResponsiveFieldCountEffect.tsx
@@ -0,0 +1,41 @@
+import { recordListDisplayedFieldCountComponentState } from '@/object-record/record-list/states/recordListDisplayedFieldCountComponentState';
+import { computeRecordListDisplayedFieldCount } from '@/object-record/record-list/utils/computeRecordListDisplayedFieldCount';
+import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
+import { type RefObject, useLayoutEffect } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+type RecordListResponsiveFieldCountEffectProps = {
+ containerRef: RefObject;
+};
+
+export const RecordListResponsiveFieldCountEffect = ({
+ containerRef,
+}: RecordListResponsiveFieldCountEffectProps) => {
+ const setRecordListDisplayedFieldCount = useSetAtomComponentState(
+ recordListDisplayedFieldCountComponentState,
+ );
+
+ useLayoutEffect(() => {
+ const containerElement = containerRef.current;
+
+ if (!isDefined(containerElement)) {
+ return;
+ }
+
+ const updateDisplayedFieldCount = () => {
+ setRecordListDisplayedFieldCount(
+ computeRecordListDisplayedFieldCount(containerElement.clientWidth),
+ );
+ };
+
+ updateDisplayedFieldCount();
+
+ const resizeObserver = new ResizeObserver(updateDisplayedFieldCount);
+
+ resizeObserver.observe(containerElement);
+
+ return () => resizeObserver.disconnect();
+ }, [containerRef, setRecordListDisplayedFieldCount]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRow.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRow.tsx
new file mode 100644
index 0000000000..679bdacff2
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRow.tsx
@@ -0,0 +1,185 @@
+import { RecordChip } from '@/object-record/components/RecordChip';
+import { StopPropagationContainer } from '@/object-record/record-board/record-board-card/components/StopPropagationContainer';
+import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
+import { isFieldValueEmpty } from '@/object-record/record-field/ui/utils/isFieldValueEmpty';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
+import { RecordListRowField } from '@/object-record/record-list/components/RecordListRowField';
+import { RECORD_LIST_ROW_LABEL_IDENTIFIER_WIDTH } from '@/object-record/record-list/constants/RecordListRowLabelIdentifierWidth';
+import { useRecordListContextOrThrow } from '@/object-record/record-list/contexts/RecordListContext';
+import { recordListDisplayedFieldCountComponentState } from '@/object-record/record-list/states/recordListDisplayedFieldCountComponentState';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
+import { styled } from '@linaria/react';
+import { t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+import { ChipVariant } from 'twenty-ui/data-display';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+const StyledRowContainer = styled.div`
+ cursor: pointer;
+ padding-bottom: 2px;
+
+ &:hover > div {
+ background: ${themeCssVariables.background.transparent.lighter};
+ }
+
+ &:active > div {
+ background: ${themeCssVariables.accent.quaternary};
+ }
+`;
+
+const StyledRow = styled.div`
+ align-items: center;
+ border-radius: ${themeCssVariables.border.radius.sm};
+ display: flex;
+ gap: ${themeCssVariables.spacing[3]};
+ height: 32px;
+ justify-content: space-between;
+ padding: 0 6px;
+`;
+
+const StyledRecordChipContainer = styled.div`
+ display: flex;
+ min-width: ${RECORD_LIST_ROW_LABEL_IDENTIFIER_WIDTH}px;
+ overflow: hidden;
+`;
+
+const StyledFieldsContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${themeCssVariables.spacing[3]};
+ justify-content: flex-end;
+ overflow: hidden;
+`;
+
+const StyledHiddenFieldCountChip = styled.div`
+ align-items: center;
+ background: ${themeCssVariables.background.transparent.light};
+ border-radius: ${themeCssVariables.border.radius.sm};
+ color: ${themeCssVariables.font.color.primary};
+ display: flex;
+ flex-shrink: 0;
+ font-size: ${themeCssVariables.font.size.md};
+ gap: 2px;
+ height: 20px;
+ padding: 0 ${themeCssVariables.spacing[1]};
+
+ &::before {
+ color: ${themeCssVariables.font.color.secondary};
+ content: '+';
+ font-size: ${themeCssVariables.font.size.sm};
+ }
+`;
+
+type RecordListRowProps = {
+ recordId: string;
+};
+
+export const RecordListRow = ({ recordId }: RecordListRowProps) => {
+ const { objectNameSingular } = useRecordListContextOrThrow();
+ const {
+ labelIdentifierFieldMetadataItem,
+ fieldDefinitionByFieldMetadataItemId,
+ } = useRecordIndexContextOrThrow();
+
+ const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
+
+ const visibleRecordFields = useAtomComponentSelectorValue(
+ visibleRecordFieldsComponentSelector,
+ );
+
+ const recordListDisplayedFieldCount = useAtomComponentStateValue(
+ recordListDisplayedFieldCountComponentState,
+ );
+
+ const { openRecordFromIndexView } = useOpenRecordFromIndexView();
+
+ if (!isDefined(recordStore)) {
+ return null;
+ }
+
+ const visibleRecordFieldsExceptLabelIdentifier = visibleRecordFields.filter(
+ (recordField) =>
+ recordField.fieldMetadataItemId !== labelIdentifierFieldMetadataItem?.id,
+ );
+
+ const nonEmptyRecordFields = visibleRecordFieldsExceptLabelIdentifier.flatMap(
+ (recordField) => {
+ const fieldDefinition =
+ fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
+
+ if (
+ !isDefined(fieldDefinition) ||
+ isFieldValueEmpty({
+ fieldDefinition,
+ fieldValue: recordStore[fieldDefinition.metadata.fieldName],
+ })
+ ) {
+ return [];
+ }
+
+ return [{ recordField, fieldDefinition }];
+ },
+ );
+
+ const displayedRecordFields = nonEmptyRecordFields.slice(
+ 0,
+ recordListDisplayedFieldCount,
+ );
+
+ const hiddenFieldCount =
+ nonEmptyRecordFields.length - displayedRecordFields.length;
+
+ const openRecord = () => openRecordFromIndexView({ recordId });
+
+ return (
+ {
+ if (event.target !== event.currentTarget) {
+ return;
+ }
+
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ openRecord();
+ }
+ }}
+ >
+
+
+
+
+
+
+
+ {displayedRecordFields.map(({ recordField, fieldDefinition }) => (
+
+ ))}
+ {hiddenFieldCount > 0 && (
+
+ {hiddenFieldCount}
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRowField.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRowField.tsx
new file mode 100644
index 0000000000..bdeda95fcf
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListRowField.tsx
@@ -0,0 +1,75 @@
+import { type RecordField } from '@/object-record/record-field/types/RecordField';
+import { FieldDisplay } from '@/object-record/record-field/ui/components/FieldDisplay';
+import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
+import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
+import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
+import { RECORD_LIST_ROW_FIELD_ANCHOR_CLASS_NAME } from '@/object-record/record-list/constants/RecordListRowFieldAnchorClassName';
+import { RECORD_LIST_ROW_FIELD_MAX_WIDTH } from '@/object-record/record-list/constants/RecordListRowFieldMaxWidth';
+import { RECORD_LIST_ROW_INPUT_ID_PREFIX } from '@/object-record/record-list/constants/RecordListRowInputIdPrefix';
+import { recordListHoveredFieldMetadataItemIdComponentState } from '@/object-record/record-list/states/recordListHoveredFieldMetadataItemIdComponentState';
+import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
+import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
+import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
+import { styled } from '@linaria/react';
+
+const StyledFieldContainer = styled.div`
+ align-items: center;
+ display: flex;
+ flex-shrink: 0;
+ max-width: ${RECORD_LIST_ROW_FIELD_MAX_WIDTH}px;
+ overflow: hidden;
+
+ & > * {
+ pointer-events: none;
+ }
+`;
+
+type RecordListRowFieldProps = {
+ recordId: string;
+ recordField: RecordField;
+ fieldDefinition: ColumnDefinition;
+};
+
+export const RecordListRowField = ({
+ recordId,
+ recordField,
+ fieldDefinition,
+}: RecordListRowFieldProps) => {
+ const setRecordListHoveredFieldMetadataItemId = useSetAtomComponentState(
+ recordListHoveredFieldMetadataItemIdComponentState,
+ );
+
+ return (
+
+ setRecordListHoveredFieldMetadataItemId(recordField.fieldMetadataItemId)
+ }
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListSSESubscribeEffect.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListSSESubscribeEffect.tsx
new file mode 100644
index 0000000000..a3ab570a10
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListSSESubscribeEffect.tsx
@@ -0,0 +1,38 @@
+import { useRefetchFindManyRecords } from '@/object-record/hooks/useRefetchFindManyRecords';
+import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
+import { useRecordListContextOrThrow } from '@/object-record/record-list/contexts/RecordListContext';
+import { RecordListComponentInstanceContext } from '@/object-record/record-list/states/contexts/RecordListComponentInstanceContext';
+import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+
+export const RecordListSSESubscribeEffect = () => {
+ const recordListId = useAvailableComponentInstanceIdOrThrow(
+ RecordListComponentInstanceContext,
+ );
+
+ const { objectNameSingular, objectMetadataItem } =
+ useRecordListContextOrThrow();
+
+ const { filter, orderBy } =
+ useFindManyRecordIndexTableParams(objectNameSingular);
+
+ const { refetchFindManyRecords } = useRefetchFindManyRecords({
+ objectMetadataNamePlural: objectMetadataItem.namePlural,
+ });
+
+ const queryId = `record-list-${recordListId}`;
+
+ useListenToEventsForQuery({
+ queryId,
+ operationSignature: {
+ objectNameSingular: objectMetadataItem.nameSingular,
+ variables: {
+ filter,
+ orderBy,
+ },
+ },
+ onSseReconnected: refetchFindManyRecords,
+ });
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/components/RecordListUpsertRecordsInStoreEffect.tsx b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListUpsertRecordsInStoreEffect.tsx
new file mode 100644
index 0000000000..83cc58ff2a
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/components/RecordListUpsertRecordsInStoreEffect.tsx
@@ -0,0 +1,19 @@
+import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
+import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { useEffect } from 'react';
+
+type RecordListUpsertRecordsInStoreEffectProps = {
+ records: ObjectRecord[];
+};
+
+export const RecordListUpsertRecordsInStoreEffect = ({
+ records,
+}: RecordListUpsertRecordsInStoreEffectProps) => {
+ const { upsertRecordsInStore } = useUpsertRecordsInStore();
+
+ useEffect(() => {
+ upsertRecordsInStore({ partialRecords: records });
+ }, [records, upsertRecordsInStore]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowFieldAnchorClassName.ts b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowFieldAnchorClassName.ts
new file mode 100644
index 0000000000..1cc950c744
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowFieldAnchorClassName.ts
@@ -0,0 +1,2 @@
+export const RECORD_LIST_ROW_FIELD_ANCHOR_CLASS_NAME =
+ 'record-list-row-field-anchor';
diff --git a/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowFieldMaxWidth.ts b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowFieldMaxWidth.ts
new file mode 100644
index 0000000000..63a7a47b4b
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowFieldMaxWidth.ts
@@ -0,0 +1 @@
+export const RECORD_LIST_ROW_FIELD_MAX_WIDTH = 156;
diff --git a/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowInputIdPrefix.ts b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowInputIdPrefix.ts
new file mode 100644
index 0000000000..65cb9221ac
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowInputIdPrefix.ts
@@ -0,0 +1 @@
+export const RECORD_LIST_ROW_INPUT_ID_PREFIX = 'record-list-row';
diff --git a/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowLabelIdentifierWidth.ts b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowLabelIdentifierWidth.ts
new file mode 100644
index 0000000000..990d163f38
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowLabelIdentifierWidth.ts
@@ -0,0 +1 @@
+export const RECORD_LIST_ROW_LABEL_IDENTIFIER_WIDTH = 176;
diff --git a/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowVisibleFieldLimit.ts b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowVisibleFieldLimit.ts
new file mode 100644
index 0000000000..2814d95432
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/constants/RecordListRowVisibleFieldLimit.ts
@@ -0,0 +1 @@
+export const RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT = 6;
diff --git a/packages/twenty-front/src/modules/object-record/record-list/contexts/RecordListContext.ts b/packages/twenty-front/src/modules/object-record/record-list/contexts/RecordListContext.ts
new file mode 100644
index 0000000000..a6873283d6
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/contexts/RecordListContext.ts
@@ -0,0 +1,13 @@
+import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
+import { type ObjectPermission } from '~/generated-metadata/graphql';
+import { createRequiredContext } from '~/utils/createRequiredContext';
+
+type RecordListContextValue = {
+ viewBarInstanceId: string;
+ objectNameSingular: string;
+ objectMetadataItem: EnrichedObjectMetadataItem;
+ objectPermissions: ObjectPermission;
+};
+
+export const [RecordListContextProvider, useRecordListContextOrThrow] =
+ createRequiredContext('RecordListContext');
diff --git a/packages/twenty-front/src/modules/object-record/record-list/states/contexts/RecordListComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-list/states/contexts/RecordListComponentInstanceContext.ts
new file mode 100644
index 0000000000..0cbe3b2b62
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/states/contexts/RecordListComponentInstanceContext.ts
@@ -0,0 +1,4 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const RecordListComponentInstanceContext =
+ createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/object-record/record-list/states/isRecordListGroupSectionToggledComponentState.ts b/packages/twenty-front/src/modules/object-record/record-list/states/isRecordListGroupSectionToggledComponentState.ts
new file mode 100644
index 0000000000..a66bb49c17
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/states/isRecordListGroupSectionToggledComponentState.ts
@@ -0,0 +1,10 @@
+import { type RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
+import { createAtomComponentFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomComponentFamilyState';
+import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
+
+export const isRecordListGroupSectionToggledComponentState =
+ createAtomComponentFamilyState({
+ key: 'isRecordListGroupSectionToggledComponentState',
+ defaultValue: true,
+ componentInstanceContext: ViewComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/record-list/states/recordListDisplayedFieldCountComponentState.ts b/packages/twenty-front/src/modules/object-record/record-list/states/recordListDisplayedFieldCountComponentState.ts
new file mode 100644
index 0000000000..27b5b329e0
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/states/recordListDisplayedFieldCountComponentState.ts
@@ -0,0 +1,10 @@
+import { RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT } from '@/object-record/record-list/constants/RecordListRowVisibleFieldLimit';
+import { RecordListComponentInstanceContext } from '@/object-record/record-list/states/contexts/RecordListComponentInstanceContext';
+import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
+
+export const recordListDisplayedFieldCountComponentState =
+ createAtomComponentState({
+ key: 'recordListDisplayedFieldCountComponentState',
+ defaultValue: RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT,
+ componentInstanceContext: RecordListComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/record-list/states/recordListHoveredFieldMetadataItemIdComponentState.ts b/packages/twenty-front/src/modules/object-record/record-list/states/recordListHoveredFieldMetadataItemIdComponentState.ts
new file mode 100644
index 0000000000..05d33bb8c1
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/states/recordListHoveredFieldMetadataItemIdComponentState.ts
@@ -0,0 +1,9 @@
+import { RecordListComponentInstanceContext } from '@/object-record/record-list/states/contexts/RecordListComponentInstanceContext';
+import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
+
+export const recordListHoveredFieldMetadataItemIdComponentState =
+ createAtomComponentState({
+ key: 'recordListHoveredFieldMetadataItemIdComponentState',
+ defaultValue: null,
+ componentInstanceContext: RecordListComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/record-list/utils/__tests__/computeRecordListDisplayedFieldCount.test.ts b/packages/twenty-front/src/modules/object-record/record-list/utils/__tests__/computeRecordListDisplayedFieldCount.test.ts
new file mode 100644
index 0000000000..fb5a60de51
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/utils/__tests__/computeRecordListDisplayedFieldCount.test.ts
@@ -0,0 +1,16 @@
+import { computeRecordListDisplayedFieldCount } from '@/object-record/record-list/utils/computeRecordListDisplayedFieldCount';
+
+describe('computeRecordListDisplayedFieldCount', () => {
+ it('returns zero when only the record label and hidden count fit', () => {
+ expect(computeRecordListDisplayedFieldCount(407)).toBe(0);
+ });
+
+ it('accounts for row padding when calculating field breakpoints', () => {
+ expect(computeRecordListDisplayedFieldCount(408)).toBe(1);
+ expect(computeRecordListDisplayedFieldCount(576)).toBe(2);
+ });
+
+ it('caps the displayed field count at the visible field limit', () => {
+ expect(computeRecordListDisplayedFieldCount(10_000)).toBe(6);
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-list/utils/computeRecordListDisplayedFieldCount.ts b/packages/twenty-front/src/modules/object-record/record-list/utils/computeRecordListDisplayedFieldCount.ts
new file mode 100644
index 0000000000..68b30d464a
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-list/utils/computeRecordListDisplayedFieldCount.ts
@@ -0,0 +1,29 @@
+import { RECORD_LIST_ROW_FIELD_MAX_WIDTH } from '@/object-record/record-list/constants/RecordListRowFieldMaxWidth';
+import { RECORD_LIST_ROW_LABEL_IDENTIFIER_WIDTH } from '@/object-record/record-list/constants/RecordListRowLabelIdentifierWidth';
+import { RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT } from '@/object-record/record-list/constants/RecordListRowVisibleFieldLimit';
+
+const ROW_HORIZONTAL_PADDING_WIDTH = 12;
+const ROW_ITEM_GAP_WIDTH = 12;
+const HIDDEN_FIELD_COUNT_CHIP_WIDTH = 40;
+
+export const computeRecordListDisplayedFieldCount = (
+ containerWidth: number,
+) => {
+ const reservedWidth =
+ ROW_HORIZONTAL_PADDING_WIDTH +
+ RECORD_LIST_ROW_LABEL_IDENTIFIER_WIDTH +
+ ROW_ITEM_GAP_WIDTH +
+ HIDDEN_FIELD_COUNT_CHIP_WIDTH;
+
+ const fieldWidthWithGap =
+ RECORD_LIST_ROW_FIELD_MAX_WIDTH + ROW_ITEM_GAP_WIDTH;
+
+ const fittingFieldCount = Math.floor(
+ (containerWidth - reservedWidth) / fieldWidthWithGap,
+ );
+
+ return Math.min(
+ RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT,
+ Math.max(0, fittingFieldCount),
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector.ts
index a2b401b194..6171db9496 100644
--- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector.ts
+++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerIsSelectedComponentFamilySelector.ts
@@ -4,7 +4,7 @@ import { createAtomComponentFamilySelector } from '@/ui/utilities/state/jotai/ut
export const multipleRecordPickerIsSelectedComponentFamilySelector =
createAtomComponentFamilySelector({
- key: 'visibleRecordGroupIdsComponentFamilySelector',
+ key: 'multipleRecordPickerIsSelectedComponentFamilySelector',
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
get:
({ instanceId, familyKey: recordId }) =>
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx
index 5a605bdfea..0bff1ed6b4 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection.tsx
@@ -1,10 +1,9 @@
import { styled } from '@linaria/react';
-import { useCallback } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
-import { RecordGroupChip } from '@/object-record/record-group/components/RecordGroupChip';
+import { RecordGroupAggregateDropdown } from '@/object-record/record-group/components/RecordGroupAggregateDropdown';
+import { RecordGroupSectionHeader } from '@/object-record/record-group/components/RecordGroupSectionHeader';
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { useShouldHideRecordGroup } from '@/object-record/record-group/hooks/useShouldHideRecordGroup';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
@@ -36,12 +35,9 @@ import {
isDefined,
sumByProperty,
} from 'twenty-shared/utils';
-import { IconChevronDown } from 'twenty-ui/icon';
-import { AnimatedLightIconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
const StyledTrContainer = styled.div`
- cursor: pointer;
display: flex;
flex-direction: row;
@@ -50,48 +46,22 @@ const StyledTrContainer = styled.div`
}
`;
-const StyledChevronContainer = styled.div`
- border-right: none;
- color: ${themeCssVariables.font.color.secondary};
- display: flex;
- left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
- min-width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
- position: sticky;
- text-align: center;
-
- vertical-align: middle;
- width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
-
- z-index: ${TABLE_Z_INDEX.groupSection.stickyCell};
-`;
-
-const StyledAnimatedLightIconButtonContainer = styled.div`
- display: block;
- margin: auto;
-
- z-index: ${TABLE_Z_INDEX.groupSection.stickyCell};
-`;
-
-const StyledRecordGroupSection = styled.div<{ width: number }>`
+const StyledRecordGroupHeaderContainer = styled.div<{ width: number }>`
align-items: center;
border-right: none;
display: flex;
flex-direction: row;
gap: ${themeCssVariables.spacing[1]};
height: ${RECORD_TABLE_ROW_HEIGHT}px;
- left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
- min-width: ${({ width }) => width}px;
+ left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
+ min-width: ${({ width }) => width + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
position: sticky;
- width: ${({ width }) => width}px;
+ width: ${({ width }) => width + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
z-index: ${TABLE_Z_INDEX.groupSection.stickyCell};
`;
-const StyledTagContainer = styled.div`
- flex-shrink: 0;
-`;
-
const StyledAggregateDropdownContainer = styled.div<{
isNonInteractive: boolean;
}>`
@@ -177,10 +147,6 @@ export const RecordTableRecordGroupSection = () => {
isRecordTableCellsNonEditableComponentState,
);
- const handleDropdownToggle = useCallback(() => {
- setIsRecordGroupTableSectionToggled((prevState) => !prevState);
- }, [setIsRecordGroupTableSectionToggled]);
-
const visibleRecordFieldsWithoutLabelIdentifier = visibleRecordFields.filter(
filterOutByProperty(
'fieldMetadataItemId',
@@ -206,41 +172,33 @@ export const RecordTableRecordGroupSection = () => {
}
return (
-
+
-
-
-
-
-
-
-
-
-
+
+ setIsRecordGroupTableSectionToggled((prevState) => !prevState)
+ }
+ chevronWidth={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}
+ />
-
-
+
diff --git a/packages/twenty-front/src/modules/views/types/ViewType.ts b/packages/twenty-front/src/modules/views/types/ViewType.ts
index bf14615d1a..d59a868d02 100644
--- a/packages/twenty-front/src/modules/views/types/ViewType.ts
+++ b/packages/twenty-front/src/modules/views/types/ViewType.ts
@@ -4,6 +4,7 @@ import {
IconCalendar,
type IconComponent,
IconLayoutKanban,
+ IconList,
IconTable,
} from 'twenty-ui/icon';
@@ -16,6 +17,7 @@ export const VIEW_TYPE_LABELS = {
[ViewType.TABLE]: msg`Table`,
[ViewType.KANBAN]: msg`Kanban`,
[ViewType.CALENDAR]: msg`Calendar`,
+ [ViewType.LIST]: msg`List`,
[ViewType.FIELDS_WIDGET]: msg`Fields widget`,
[ViewType.TABLE_WIDGET]: msg`Table widget`,
[ViewType.KANBAN_WIDGET]: msg`Kanban widget`,
@@ -31,6 +33,7 @@ const VIEW_TYPE_ICON_MAPPING = [
{ icon: IconLayoutKanban, value: ViewType.KANBAN },
{ icon: IconTable, value: ViewType.TABLE },
{ icon: IconCalendar, value: ViewType.CALENDAR },
+ { icon: IconList, value: ViewType.LIST },
] as const satisfies {
icon: IconComponent;
value: ViewType;
diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx
index f8ded3baf8..9dfbd10835 100644
--- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx
+++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx
@@ -61,6 +61,9 @@ export const ViewPickerContentCreateMode = () => {
const isCalendarWeekViewEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_CALENDAR_WEEK_VIEW_ENABLED,
);
+ const isListViewEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_LIST_VIEW_ENABLED,
+ );
const viewObjectMetadataId = useAtomComponentStateValue(
viewObjectMetadataIdComponentState,
@@ -200,7 +203,9 @@ export const ViewPickerContentCreateMode = () => {
setViewPickerIsDirty(true);
setViewPickerType(value);
}}
- options={VIEW_PICKER_TYPE_SELECT_OPTIONS.map((option) => ({
+ options={VIEW_PICKER_TYPE_SELECT_OPTIONS.filter(
+ (option) => option.value !== ViewType.LIST || isListViewEnabled,
+ ).map((option) => ({
...option,
label: t(option.label),
}))}
diff --git a/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts b/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts
index 4c8bbffd15..1420f4ac02 100644
--- a/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts
+++ b/packages/twenty-front/src/modules/views/view-picker/constants/ViewPickerTypeSelectOptions.ts
@@ -20,4 +20,9 @@ export const VIEW_PICKER_TYPE_SELECT_OPTIONS = [
label: VIEW_TYPE_LABELS[ViewType.CALENDAR],
Icon: viewTypeIconMapping(ViewType.CALENDAR),
},
+ {
+ value: ViewType.LIST,
+ label: VIEW_TYPE_LABELS[ViewType.LIST],
+ Icon: viewTypeIconMapping(ViewType.LIST),
+ },
];
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-30/2-30-instance-command-fast-1785929606728-add-list-view-type.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-30/2-30-instance-command-fast-1785929606728-add-list-view-type.ts
new file mode 100644
index 0000000000..13c2f90f39
--- /dev/null
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-30/2-30-instance-command-fast-1785929606728-add-list-view-type.ts
@@ -0,0 +1,35 @@
+import { type QueryRunner } from 'typeorm';
+
+import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
+import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
+
+@RegisteredInstanceCommand('2.30.0', 1785929606728)
+export class AddListViewTypeFastInstanceCommand implements FastInstanceCommand {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TYPE "core"."view_type_enum" ADD VALUE IF NOT EXISTS 'LIST' AFTER 'CALENDAR'`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `UPDATE "core"."view" SET "type" = 'TABLE' WHERE "type" = 'LIST'`,
+ );
+ await queryRunner.query(
+ "CREATE TYPE \"core\".\"view_type_enum_old\" AS ENUM('TABLE', 'KANBAN', 'CALENDAR', 'FIELDS_WIDGET', 'TABLE_WIDGET', 'KANBAN_WIDGET', 'CALENDAR_WIDGET')",
+ );
+ await queryRunner.query(
+ 'ALTER TABLE "core"."view" ALTER COLUMN "type" DROP DEFAULT',
+ );
+ await queryRunner.query(
+ 'ALTER TABLE "core"."view" ALTER COLUMN "type" TYPE "core"."view_type_enum_old" USING "type"::"text"::"core"."view_type_enum_old"',
+ );
+ await queryRunner.query(
+ 'ALTER TABLE "core"."view" ALTER COLUMN "type" SET DEFAULT \'TABLE\'',
+ );
+ await queryRunner.query('DROP TYPE "core"."view_type_enum"');
+ await queryRunner.query(
+ 'ALTER TYPE "core"."view_type_enum_old" RENAME TO "view_type_enum"',
+ );
+ }
+}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts
index 05dcfbad14..768ac3cbb6 100644
--- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts
@@ -135,6 +135,7 @@ import { AddConnectedAccountHandleProviderIndexFastInstanceCommand } from 'src/d
import { AddOpenRecordInToObjectMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-instance-command-fast-1785504900000-add-open-record-in-to-object-metadata';
import { CreateUserSessionCoreTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-instance-command-fast-1785518325511-create-user-session-core-table';
import { CreateApplicationAuthorizationCoreTableFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-instance-command-fast-1785681272278-create-application-authorization-core-table';
+import { AddListViewTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-30/2-30-instance-command-fast-1785929606728-add-list-view-type';
import { AddCallRecordingWidgetTypesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-29/2-29-instance-command-fast-1786009374684-add-call-recording-widget-types';
export const INSTANCE_COMMANDS = [
@@ -273,5 +274,6 @@ export const INSTANCE_COMMANDS = [
AddOpenRecordInToObjectMetadataFastInstanceCommand,
CreateUserSessionCoreTableFastInstanceCommand,
CreateApplicationAuthorizationCoreTableFastInstanceCommand,
+ AddListViewTypeFastInstanceCommand,
AddCallRecordingWidgetTypesFastInstanceCommand,
];
diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts
index 847c4016d1..f081eac35b 100644
--- a/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts
+++ b/packages/twenty-server/src/engine/core-modules/feature-flag/constants/public-feature-flag.const.ts
@@ -28,6 +28,14 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
'Enable many-to-many relations through junction tables configuration',
},
},
+ {
+ key: FeatureFlagKey.IS_LIST_VIEW_ENABLED,
+ metadata: {
+ label: 'List View',
+ description:
+ 'Display records in a list layout with collapsible groups and inline fields',
+ },
+ },
{
key: FeatureFlagKey.IS_SETTINGS_DISCOVERY_HERO_ENABLED,
metadata: {
diff --git a/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts b/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts
index a5b551cf8d..86352b1d9b 100644
--- a/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/view/tools/view-tools.factory.ts
@@ -30,6 +30,7 @@ import {
const CREATABLE_VIEW_TYPES = [
ViewType.TABLE,
+ ViewType.LIST,
ViewType.KANBAN,
ViewType.CALENDAR,
ViewType.TABLE_WIDGET,
@@ -671,7 +672,7 @@ DECLARATIVE CHILDREN (replace semantics): fields, filters, and sorts each descri
- Omitting the key leaves existing entries untouched.
This means you never need to fetch child ids to edit a view — just pass the desired end state. For surgical single-entry edits, the granular tools (create_view_filter, update_view_sort, etc.) remain available.
-VIEW TYPES: TABLE (default), KANBAN (requires mainGroupByFieldName, a SELECT field), CALENDAR (requires calendarFieldName + calendarLayout).`,
+VIEW TYPES: TABLE (default), LIST, KANBAN (requires mainGroupByFieldName, a SELECT field), CALENDAR (requires calendarFieldName + calendarLayout).`,
inputSchema: UpsertCompleteViewInputSchema,
execute: async (parameters: {
id?: string;
diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
index 935b924e94..8ec193d46c 100644
--- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
@@ -245,6 +245,7 @@ describe('WorkspaceEntityManager', () => {
IS_CALENDAR_WEEK_VIEW_ENABLED: false,
IS_EMAIL_GROUP_ENABLED: false,
IS_JUNCTION_RELATIONS_ENABLED: false,
+ IS_LIST_VIEW_ENABLED: false,
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: false,
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: false,
IS_SETTINGS_DISCOVERY_HERO_ENABLED: false,
diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts
index 46d9899ca4..2c6521e3f3 100644
--- a/packages/twenty-shared/src/types/FeatureFlagKey.ts
+++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts
@@ -5,6 +5,7 @@ export enum FeatureFlagKey {
IS_CALENDAR_WEEK_VIEW_ENABLED = 'IS_CALENDAR_WEEK_VIEW_ENABLED',
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
+ IS_LIST_VIEW_ENABLED = 'IS_LIST_VIEW_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED',
IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
diff --git a/packages/twenty-shared/src/types/ViewType.ts b/packages/twenty-shared/src/types/ViewType.ts
index df6d1b19e3..c36ce999b0 100644
--- a/packages/twenty-shared/src/types/ViewType.ts
+++ b/packages/twenty-shared/src/types/ViewType.ts
@@ -2,6 +2,7 @@ export enum ViewType {
TABLE = 'TABLE',
KANBAN = 'KANBAN',
CALENDAR = 'CALENDAR',
+ LIST = 'LIST',
FIELDS_WIDGET = 'FIELDS_WIDGET',
TABLE_WIDGET = 'TABLE_WIDGET',
KANBAN_WIDGET = 'KANBAN_WIDGET',
diff --git a/packages/twenty-shared/src/utils/views/getViewLayoutFromViewType.ts b/packages/twenty-shared/src/utils/views/getViewLayoutFromViewType.ts
index 419e3e0a7d..7cd7de1579 100644
--- a/packages/twenty-shared/src/utils/views/getViewLayoutFromViewType.ts
+++ b/packages/twenty-shared/src/utils/views/getViewLayoutFromViewType.ts
@@ -6,6 +6,7 @@ const VIEW_TYPE_TO_LAYOUT_MAPPING: Record = {
[ViewType.TABLE]: ViewType.TABLE,
[ViewType.KANBAN]: ViewType.KANBAN,
[ViewType.CALENDAR]: ViewType.CALENDAR,
+ [ViewType.LIST]: ViewType.LIST,
[ViewType.FIELDS_WIDGET]: ViewType.FIELDS_WIDGET,
[ViewType.TABLE_WIDGET]: ViewType.TABLE,
[ViewType.KANBAN_WIDGET]: ViewType.KANBAN,