Add a list view type (#23829)

https://github.com/user-attachments/assets/45c11b5b-8da6-43ee-89de-f3fca0b64038



https://github.com/user-attachments/assets/a1b22a00-f6c8-4074-ae98-00a9a960e62f



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23829?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: neo773 <huzef@twenty.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
neo773
2026-08-08 03:25:51 +05:30
committed by GitHub
parent 845b48f243
commit d464065044
70 changed files with 1353 additions and 175 deletions
@@ -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
@@ -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,
@@ -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. |
@@ -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',
@@ -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'
}
@@ -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);
};
@@ -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,
>({
@@ -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,
@@ -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 = ({
<RecordCalendarComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
{children}
<RecordListComponentInstanceContext.Provider
value={{ instanceId: componentInstanceId }}
>
{children}
</RecordListComponentInstanceContext.Provider>
</RecordCalendarComponentInstanceContext.Provider>
</RecordFieldsComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
@@ -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 = () => {
}}
/>
</SelectableListItem>
{isListViewEnabled && (
<SelectableListItem
itemId={ViewType.LIST}
onEnter={() => {
setAndPersistViewType(ViewType.LIST);
}}
>
<MenuItemSelect
LeftIcon={viewTypeIconMapping(ViewType.LIST)}
text={t(getViewTypeLabel(ViewType.LIST))}
selected={currentView?.type === ViewType.LIST}
focused={selectedItemId === ViewType.LIST}
onClick={async () => {
if (currentView?.type !== ViewType.LIST) {
await setAndPersistViewType(ViewType.LIST);
}
}}
/>
</SelectableListItem>
)}
<SelectableListItem
itemId={ViewType.CALENDAR}
onEnter={() => {
@@ -305,31 +332,32 @@ export const ObjectOptionsDropdownLayoutContent = () => {
/>
</SelectableListItem>
)}
{currentView?.type !== ViewType.TABLE && (
<SelectableListItem
itemId="Compact view"
onEnter={() => {
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
);
}}
>
<MenuItemToggle
focused={selectedItemId === 'Compact view'}
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
{currentView?.type !== ViewType.TABLE &&
currentView?.type !== ViewType.LIST && (
<SelectableListItem
itemId="Compact view"
onEnter={() => {
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
)
}
toggled={isCompactModeActive}
text={t`Compact view`}
toggleSize="small"
/>
</SelectableListItem>
)}
);
}}
>
<MenuItemToggle
focused={selectedItemId === 'Compact view'}
LeftIcon={IconBaselineDensitySmall}
onToggleChange={() =>
setAndPersistIsCompactModeActive(
!isCompactModeActive,
currentView,
)
}
toggled={isCompactModeActive}
text={t`Compact view`}
toggleSize="small"
/>
</SelectableListItem>
)}
</DropdownMenuItemsContainer>
</SelectableList>
)}
@@ -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;
};
@@ -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 (
<StopPropagationContainer key={recordField.fieldMetadataItemId}>
<FieldContext.Provider
@@ -6,13 +6,13 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu';
import { RecordBoardColumnHeaderAggregateDropdown } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdown';
import { DragDropItemSortableHandle } from '@/ui/utilities/drag-and-drop/components/DragDropItemSortableHandle';
import { RECORD_BOARD_COLUMN_WIDTH } from '@/object-record/record-board/constants/RecordBoardColumnWidth';
import { RECORD_BOARD_COLUMN_WIDTH_CSS_VARIABLE_NAME } from '@/object-record/record-board/constants/RecordBoardColumnWidthCssVariableName';
import { RecordBoardColumnResizeHandler } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnResizeHandler';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
import { RecordGroupAggregateDropdown } from '@/object-record/record-group/components/RecordGroupAggregateDropdown';
import { RecordGroupChip } from '@/object-record/record-group/components/RecordGroupChip';
import { getFieldMetadataItemGqlFieldName } from '@/object-metadata/utils/getFieldMetadataItemGqlFieldName';
import { recordIndexAggregateDisplayLabelComponentState } from '@/object-record/record-index/states/recordIndexAggregateDisplayLabelComponentState';
@@ -237,7 +237,7 @@ export const RecordBoardColumnHeader = () => {
isNonInteractive={isRecordBoardViewSettingsReadOnly}
inert={isRecordBoardViewSettingsReadOnly || undefined}
>
<RecordBoardColumnHeaderAggregateDropdown
<RecordGroupAggregateDropdown
aggregateValue={recordIndexAggregateDisplayValueForGroupValue}
dropdownId={`record-board-column-aggregate-dropdown-${columnDefinition.id}`}
objectMetadataItem={objectMetadataItem}
@@ -1,17 +0,0 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type RecordBoardColumnHeaderAggregateContentId } from '@/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId';
import { createContext } from 'react';
export type RecordBoardColumnHeaderAggregateDropdownContextValue = {
objectMetadataItem: EnrichedObjectMetadataItem;
currentContentId: RecordBoardColumnHeaderAggregateContentId | null;
onContentChange: (key: RecordBoardColumnHeaderAggregateContentId) => void;
resetContent: () => void;
previousContentId: RecordBoardColumnHeaderAggregateContentId | null;
dropdownId: string;
};
export const RecordBoardColumnHeaderAggregateDropdownContext =
createContext<RecordBoardColumnHeaderAggregateDropdownContextValue>(
{} as RecordBoardColumnHeaderAggregateDropdownContextValue,
);
@@ -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<string | number>;
aggregateLabel?: Nullable<string>;
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<RecordBoardColumnHeaderAggregateContentId>();
} = useDropdownContextCurrentContentId<RecordGroupAggregateContentId>();
return (
<RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.Provider
<RecordGroupAggregateDropdownComponentInstanceContext.Provider
value={{ instanceId: dropdownId }}
>
<StyledContainer>
@@ -45,14 +44,14 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
dropdownId={dropdownId}
dropdownOffset={{ y: DROPDOWN_OFFSET_Y }}
clickableComponent={
<RecordBoardColumnHeaderAggregateDropdownButton
<RecordGroupAggregateDropdownButton
dropdownId={dropdownId}
value={aggregateValue}
tooltip={aggregateLabel}
/>
}
dropdownComponents={
<RecordBoardColumnHeaderAggregateDropdownContext.Provider
<RecordGroupAggregateDropdownContext.Provider
value={{
currentContentId,
onContentChange: handleContentChange,
@@ -62,11 +61,11 @@ export const RecordBoardColumnHeaderAggregateDropdown = ({
dropdownId: dropdownId,
}}
>
<AggregateDropdownContent />
</RecordBoardColumnHeaderAggregateDropdownContext.Provider>
<RecordGroupAggregateDropdownContent />
</RecordGroupAggregateDropdownContext.Provider>
}
/>
</StyledContainer>
</RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext.Provider>
</RecordGroupAggregateDropdownComponentInstanceContext.Provider>
);
};
@@ -17,7 +17,7 @@ const StyledHeaderContainer = styled.div`
}
`;
export const RecordBoardColumnHeaderAggregateDropdownButton = ({
export const RecordGroupAggregateDropdownButton = ({
dropdownId,
value,
tooltip,
@@ -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 (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
<RecordGroupAggregateDropdownOptionsContent
availableAggregations={availableAggregations}
title={t`Count`}
/>
@@ -55,7 +55,7 @@ export const AggregateDropdownContent = () => {
targetAggregateOperations: PERCENT_AGGREGATE_OPERATION_OPTIONS,
});
return (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
<RecordGroupAggregateDropdownOptionsContent
availableAggregations={availableAggregations}
title={t`Percent`}
/>
@@ -71,7 +71,7 @@ export const AggregateDropdownContent = () => {
],
});
return (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
<RecordGroupAggregateDropdownOptionsContent
availableAggregations={datesAvailableAggregations}
title={t`Date`}
/>
@@ -84,15 +84,15 @@ export const AggregateDropdownContent = () => {
targetAggregateOperations: NON_STANDARD_AGGREGATE_OPERATION_OPTIONS,
});
return (
<RecordBoardColumnHeaderAggregateDropdownOptionsContent
<RecordGroupAggregateDropdownOptionsContent
availableAggregations={availableAggregationsWithoutDates}
title={t`More options`}
/>
);
}
case 'aggregateFields':
return <RecordBoardColumnHeaderAggregateDropdownFieldsContent />;
return <RecordGroupAggregateDropdownFieldsContent />;
default:
return <RecordBoardColumnHeaderAggregateDropdownMenuContent />;
return <RecordGroupAggregateDropdownMenuContent />;
}
};
@@ -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();
@@ -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<RecordBoardColumnHeaderAggregateDropdownContextValue>(
useDropdownContextStateManagement<RecordGroupAggregateDropdownContextValue>(
{
context: RecordBoardColumnHeaderAggregateDropdownContext,
context: RecordGroupAggregateDropdownContext,
},
);
@@ -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,
@@ -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<RecordBoardColumnHeaderAggregateDropdownContextValue>(
useDropdownContextStateManagement<RecordGroupAggregateDropdownContextValue>(
{
context: RecordBoardColumnHeaderAggregateDropdownContext,
context: RecordGroupAggregateDropdownContext,
},
);
@@ -69,7 +67,7 @@ export const RecordBoardColumnHeaderAggregateDropdownOptionsContent = ({
availableAggregationOperation,
availableAggregationFieldsIdsForOperation,
]) => (
<RecordBoardColumnHeaderAggregateDropdownMenuItem
<RecordGroupAggregateDropdownMenuItem
key={`aggregate-dropdown-menu-content-${availableAggregationOperation}`}
onContentChange={() => {
if (
@@ -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 (
<StyledSectionToggle
type="button"
aria-expanded={isExpanded}
aria-label={t`Toggle ${recordGroupDefinition.title} group`}
onClick={onToggle}
>
<StyledChevronContainer
isExpanded={isExpanded}
style={{ minWidth: chevronWidth, width: chevronWidth }}
>
<IconChevronDown
aria-hidden
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
/>
</StyledChevronContainer>
<RecordGroupChip
recordGroupDefinition={recordGroupDefinition}
fieldMetadataItem={fieldMetadataItem}
valueTagWeight="medium"
/>
</StyledSectionToggle>
);
};
@@ -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,
});
@@ -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,
});
@@ -1,4 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext =
export const RecordGroupAggregateDropdownComponentInstanceContext =
createComponentInstanceContext();
@@ -0,0 +1,7 @@
import { type RecordGroupAggregateDropdownContextValue } from '@/object-record/record-group/types/RecordGroupAggregateDropdownContextValue';
import { createContext } from 'react';
export const RecordGroupAggregateDropdownContext =
createContext<RecordGroupAggregateDropdownContextValue>(
{} as RecordGroupAggregateDropdownContextValue,
);
@@ -1,4 +1,4 @@
export type RecordBoardColumnHeaderAggregateContentId =
export type RecordGroupAggregateContentId =
| 'aggregateOperations'
| 'aggregateFields'
| 'countAggregateOperationsOptions'
@@ -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;
};
@@ -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 = () => {
/>
</StyledContainerWithPadding>
)}
{recordIndexViewType === ViewType.LIST && (
<StyledContainerWithPadding>
<RecordIndexListContainer
recordListInstanceId={recordIndexId}
viewBarInstanceId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</>
)}
</StyledContainer>
@@ -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 (
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordListInstanceId}
>
<RecordListContextProvider
value={{
viewBarInstanceId,
objectNameSingular,
objectMetadataItem,
objectPermissions,
}}
>
<RecordList />
<RecordListSSESubscribeEffect />
</RecordListContextProvider>
</RecordComponentInstanceContextsWrapper>
);
};
@@ -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,
@@ -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<HTMLDivElement>(null);
return (
<StyledContainer ref={containerRef}>
<RecordListResponsiveFieldCountEffect containerRef={containerRef} />
<ScrollWrapper
componentInstanceId={`scroll-wrapper-record-list-${recordListId}`}
defaultEnableXScroll={false}
>
{hasRecordGroups ? <RecordListRecordGroupsBody /> : <RecordListBody />}
</ScrollWrapper>
<RecordListFieldTooltip />
</StyledContainer>
);
};
@@ -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 (
<StyledAddNewRow type="button" onClick={handleCreateRecord}>
<IconPlus
stroke={theme.icon.stroke.sm}
size={theme.icon.size.sm}
color={theme.font.color.tertiary}
/>
{t`Add new`}
</StyledAddNewRow>
);
};
@@ -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 (
<StyledBody>
<RecordListRecords
records={records}
loading={loading}
error={error}
hasNextPage={hasNextPage}
fetchMoreRecords={fetchMoreRecords}
/>
</StyledBody>
);
};
@@ -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;
};
@@ -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 (
<AppTooltip
key={visibleRecordFieldIds}
anchorSelect={`.${RECORD_LIST_ROW_FIELD_ANCHOR_CLASS_NAME}`}
noArrow
place="bottom"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
>
{isDefined(hoveredFieldDefinition) && (
<StyledTooltipContent>
{isDefined(FieldIcon) && (
<FieldIcon
size={theme.icon.size.sm}
stroke={theme.icon.stroke.sm}
/>
)}
{hoveredFieldDefinition.label}
</StyledTooltipContent>
)}
</AppTooltip>
);
};
@@ -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 (
<StyledSection>
<RecordListEmptyRecordGroupEffect
loading={loading}
error={error}
records={records}
/>
<RecordListRecordGroupSection />
<RecordListRecords
records={records}
loading={loading}
error={error}
hasNextPage={hasNextPage}
fetchMoreRecords={fetchMoreRecords}
isVisible={isRecordListGroupSectionToggled}
/>
</StyledSection>
);
};
@@ -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 (
<StyledSectionHeader>
<RecordGroupSectionHeader
recordGroupDefinition={recordGroupDefinition}
fieldMetadataItem={recordIndexGroupFieldMetadataItem}
isExpanded={isRecordListGroupSectionToggled}
onToggle={() =>
setIsRecordListGroupSectionToggled((prevState) => !prevState)
}
/>
<RecordGroupAggregateDropdown
aggregateValue={recordIndexAggregateDisplayValueForGroupValue}
dropdownId={`record-list-group-section-aggregate-dropdown-${currentRecordGroupId}`}
objectMetadataItem={objectMetadataItem}
aggregateLabel={recordIndexAggregateDisplayLabel}
/>
</StyledSectionHeader>
);
};
@@ -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) => (
<RecordGroupContext.Provider
key={recordGroupId}
value={{ recordGroupId }}
>
<RecordListRecordGroup />
</RecordGroupContext.Provider>
))}
<RecordIndexGroupAggregatesDataLoader />
</>
);
};
@@ -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 (
<>
<RecordListUpsertRecordsInStoreEffect records={records} />
{isVisible && !isDefined(error) && (
<>
{records.map((record) => (
<RecordListRow key={record.id} recordId={record.id} />
))}
{hasNextPage && !loading && (
<StyledFetchMoreTrigger ref={fetchMoreRef} />
)}
{!loading && <RecordListAddNew />}
</>
)}
</>
);
};
@@ -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<HTMLElement | null>;
};
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;
};
@@ -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 (
<StyledRowContainer
role="button"
tabIndex={0}
aria-label={t`Open record`}
onClick={openRecord}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
openRecord();
}
}}
>
<StyledRow>
<StyledRecordChipContainer>
<StopPropagationContainer>
<RecordChip
objectNameSingular={objectNameSingular}
record={recordStore}
variant={ChipVariant.Transparent}
onClick={openRecord}
triggerEvent={'CLICK'}
/>
</StopPropagationContainer>
</StyledRecordChipContainer>
<StyledFieldsContainer>
{displayedRecordFields.map(({ recordField, fieldDefinition }) => (
<RecordListRowField
key={recordField.fieldMetadataItemId}
recordId={recordId}
recordField={recordField}
fieldDefinition={fieldDefinition}
/>
))}
{hiddenFieldCount > 0 && (
<StyledHiddenFieldCountChip>
{hiddenFieldCount}
</StyledHiddenFieldCountChip>
)}
</StyledFieldsContainer>
</StyledRow>
</StyledRowContainer>
);
};
@@ -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<FieldMetadata>;
};
export const RecordListRowField = ({
recordId,
recordField,
fieldDefinition,
}: RecordListRowFieldProps) => {
const setRecordListHoveredFieldMetadataItemId = useSetAtomComponentState(
recordListHoveredFieldMetadataItemIdComponentState,
);
return (
<StyledFieldContainer
className={RECORD_LIST_ROW_FIELD_ANCHOR_CLASS_NAME}
onMouseEnter={() =>
setRecordListHoveredFieldMetadataItemId(recordField.fieldMetadataItemId)
}
>
<FieldContext.Provider
value={{
recordId,
maxWidth: RECORD_LIST_ROW_FIELD_MAX_WIDTH,
isLabelIdentifier: false,
isRecordFieldReadOnly: true,
fieldDefinition,
isDisplayModeFixHeight: true,
disableChipClick: true,
triggerEvent: 'CLICK',
}}
>
<RecordFieldComponentInstanceContext.Provider
value={{
instanceId: getRecordFieldInputInstanceId({
recordId,
fieldName: fieldDefinition.metadata.fieldName,
prefix: RECORD_LIST_ROW_INPUT_ID_PREFIX,
}),
}}
>
<FieldDisplay />
</RecordFieldComponentInstanceContext.Provider>
</FieldContext.Provider>
</StyledFieldContainer>
);
};
@@ -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;
};
@@ -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;
};
@@ -0,0 +1,2 @@
export const RECORD_LIST_ROW_FIELD_ANCHOR_CLASS_NAME =
'record-list-row-field-anchor';
@@ -0,0 +1 @@
export const RECORD_LIST_ROW_FIELD_MAX_WIDTH = 156;
@@ -0,0 +1 @@
export const RECORD_LIST_ROW_INPUT_ID_PREFIX = 'record-list-row';
@@ -0,0 +1 @@
export const RECORD_LIST_ROW_LABEL_IDENTIFIER_WIDTH = 176;
@@ -0,0 +1 @@
export const RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT = 6;
@@ -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<RecordListContextValue>('RecordListContext');
@@ -0,0 +1,4 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const RecordListComponentInstanceContext =
createComponentInstanceContext();
@@ -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<boolean, RecordGroupDefinition['id']>({
key: 'isRecordListGroupSectionToggledComponentState',
defaultValue: true,
componentInstanceContext: ViewComponentInstanceContext,
});
@@ -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<number>({
key: 'recordListDisplayedFieldCountComponentState',
defaultValue: RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT,
componentInstanceContext: RecordListComponentInstanceContext,
});
@@ -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<string | null>({
key: 'recordListHoveredFieldMetadataItemIdComponentState',
defaultValue: null,
componentInstanceContext: RecordListComponentInstanceContext,
});
@@ -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);
});
});
@@ -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),
);
};
@@ -4,7 +4,7 @@ import { createAtomComponentFamilySelector } from '@/ui/utilities/state/jotai/ut
export const multipleRecordPickerIsSelectedComponentFamilySelector =
createAtomComponentFamilySelector<boolean, string>({
key: 'visibleRecordGroupIdsComponentFamilySelector',
key: 'multipleRecordPickerIsSelectedComponentFamilySelector',
componentInstanceContext: MultipleRecordPickerComponentInstanceContext,
get:
({ instanceId, familyKey: recordId }) =>
@@ -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 (
<StyledTrContainer onClick={handleDropdownToggle}>
<StyledTrContainer>
<StyledRecordTableDragAndDropPlaceholderCell />
<StyledChevronContainer>
<StyledAnimatedLightIconButtonContainer>
<AnimatedLightIconButton
Icon={IconChevronDown}
size="small"
accent="secondary"
rotate={!isRecordGroupTableSectionToggled ? -90 : 0}
/>
</StyledAnimatedLightIconButtonContainer>
</StyledChevronContainer>
<StyledRecordGroupSection
<StyledRecordGroupHeaderContainer
className="disable-shadow"
width={widthOfLabelIdentifierRecordField}
>
<StyledTagContainer>
<RecordGroupChip
recordGroupDefinition={recordGroupDefinition}
fieldMetadataItem={recordIndexGroupFieldMetadataItem}
valueTagWeight="medium"
/>
</StyledTagContainer>
<RecordGroupSectionHeader
recordGroupDefinition={recordGroupDefinition}
fieldMetadataItem={recordIndexGroupFieldMetadataItem}
isExpanded={isRecordGroupTableSectionToggled}
onToggle={() =>
setIsRecordGroupTableSectionToggled((prevState) => !prevState)
}
chevronWidth={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}
/>
<StyledAggregateDropdownContainer
isNonInteractive={isRecordTableCellsNonEditable}
inert={isRecordTableCellsNonEditable || undefined}
>
<RecordBoardColumnHeaderAggregateDropdown
<RecordGroupAggregateDropdown
aggregateValue={recordIndexAggregateDisplayValueForGroupValue}
dropdownId={`record-group-section-aggregate-dropdown-${currentRecordGroupId}`}
objectMetadataItem={objectMetadataItem}
aggregateLabel={recordIndexAggregateDisplayLabel}
/>
</StyledAggregateDropdownContainer>
</StyledRecordGroupSection>
</StyledRecordGroupHeaderContainer>
<StyledFieldPlaceholderCell widthOfFields={fieldsPlaceholderWidth} />
<RecordTableAddButtonPlaceholderCell />
<RecordTableGroupSectionLastDynamicFillingCell />
@@ -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;
@@ -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),
}))}
@@ -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),
},
];
@@ -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<void> {
await queryRunner.query(
`ALTER TYPE "core"."view_type_enum" ADD VALUE IF NOT EXISTS 'LIST' AFTER 'CALENDAR'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
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"',
);
}
}
@@ -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,
];
@@ -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: {
@@ -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;
@@ -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,
@@ -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',
@@ -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',
@@ -6,6 +6,7 @@ const VIEW_TYPE_TO_LAYOUT_MAPPING: Record<ViewTypeValue, ViewTypeValue> = {
[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,