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:
@@ -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'
|
||||
}
|
||||
|
||||
+7
-1
@@ -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);
|
||||
};
|
||||
|
||||
+2
-2
@@ -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,
|
||||
>({
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
+6
-1
@@ -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>
|
||||
|
||||
+52
-24
@@ -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>
|
||||
)}
|
||||
|
||||
+8
-1
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
+2
-2
@@ -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}
|
||||
|
||||
-17
@@ -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,
|
||||
);
|
||||
+15
-16
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -17,7 +17,7 @@ const StyledHeaderContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const RecordBoardColumnHeaderAggregateDropdownButton = ({
|
||||
export const RecordGroupAggregateDropdownButton = ({
|
||||
dropdownId,
|
||||
value,
|
||||
tooltip,
|
||||
+12
-12
@@ -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 />;
|
||||
}
|
||||
};
|
||||
+5
-5
@@ -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();
|
||||
+5
-7
@@ -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
-1
@@ -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,
|
||||
+9
-11
@@ -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 (
|
||||
+72
@@ -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>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -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,
|
||||
});
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const RecordBoardColumnHeaderAggregateDropdownComponentInstanceContext =
|
||||
export const RecordGroupAggregateDropdownComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
+7
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
export type RecordBoardColumnHeaderAggregateContentId =
|
||||
export type RecordGroupAggregateContentId =
|
||||
| 'aggregateOperations'
|
||||
| 'aggregateFields'
|
||||
| 'countAggregateOperationsOptions'
|
||||
+11
@@ -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;
|
||||
};
|
||||
+9
@@ -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>
|
||||
|
||||
+45
@@ -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>
|
||||
);
|
||||
};
|
||||
+2
@@ -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>
|
||||
);
|
||||
};
|
||||
+110
@@ -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>
|
||||
);
|
||||
};
|
||||
+28
@@ -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>
|
||||
);
|
||||
};
|
||||
+48
@@ -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;
|
||||
};
|
||||
+69
@@ -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>
|
||||
);
|
||||
};
|
||||
+55
@@ -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>
|
||||
);
|
||||
};
|
||||
+78
@@ -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>
|
||||
);
|
||||
};
|
||||
+27
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+54
@@ -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 />}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+41
@@ -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;
|
||||
};
|
||||
+185
@@ -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>
|
||||
);
|
||||
};
|
||||
+75
@@ -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>
|
||||
);
|
||||
};
|
||||
+38
@@ -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;
|
||||
};
|
||||
+19
@@ -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;
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const RECORD_LIST_ROW_FIELD_ANCHOR_CLASS_NAME =
|
||||
'record-list-row-field-anchor';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_LIST_ROW_FIELD_MAX_WIDTH = 156;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_LIST_ROW_INPUT_ID_PREFIX = 'record-list-row';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_LIST_ROW_LABEL_IDENTIFIER_WIDTH = 176;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const RECORD_LIST_ROW_VISIBLE_FIELD_LIMIT = 6;
|
||||
+13
@@ -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');
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const RecordListComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
+10
@@ -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,
|
||||
});
|
||||
+10
@@ -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,
|
||||
});
|
||||
+9
@@ -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,
|
||||
});
|
||||
+16
@@ -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);
|
||||
});
|
||||
});
|
||||
+29
@@ -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),
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -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 }) =>
|
||||
|
||||
+19
-61
@@ -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;
|
||||
|
||||
+6
-1
@@ -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),
|
||||
}))}
|
||||
|
||||
+5
@@ -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),
|
||||
},
|
||||
];
|
||||
|
||||
+35
@@ -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"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -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,
|
||||
];
|
||||
|
||||
+8
@@ -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;
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user