Add record table widget to dashboards (#18747)

## Demo



https://github.com/user-attachments/assets/584de452-544a-41f8-ae9f-4be9e9d0cd9f


## Problem
- Dashboards only supported chart widgets — tabular record data had no
inline widget type
- `RecordTable` was tightly coupled to the record index: HTML IDs, CSS
variables, and hover portals were global strings with no per-instance
scoping, so multiple tables on the same page would collide
- `updateRecordTableCSSVariable`, `RECORD_TABLE_HTML_ID`, and cell
portal IDs were hardcoded — placing two tables caused hover portals and
CSS column widths to bleed across instances
- Grid drag-select captured record UUIDs as cell IDs, producing `NaN`
layout coordinates and a full-page freeze on second widget creation

## Fix
- `RECORD_TABLE` is now a valid widget type across the full stack —
server DTOs, DB enum migration, universal config mapping, GraphQL
codegen, shared types (`RecordTableConfigurationDto`, `WidgetType`,
`addRecordTableWidgetType` migration)
- A record table widget can be placed on a dashboard and boots from a
View ID with no record index dependency —
`StandaloneRecordTableProvider` + `StandaloneRecordTableViewLoadEffect`
(wraps existing `RecordTableWithWrappers` unchanged)
- Selecting a data source auto-creates a dedicated View with up to 6
initial fields; switching source or deleting the widget cleans up the
View — `useCreateViewForRecordTableWidget` +
`useDeleteViewForRecordTableWidget`
- The settings panel exposes source, field visibility/reorder, filter
conditions, sort rules, and editable widget title —
`SidePanelPageLayoutRecordTableSettings` + sub-pages, matching chart
widget pattern
- Filters, sorts, and aggregate operations update the table in real time
but only persist to the View on explicit dashboard save —
`useSaveRecordTableWidgetsViewDataOnDashboardSave` (diff + flush on
save)
- Headers are always non-interactive (no dropdown, no cursor pointer);
columns are resizable only in edit mode; cells are non-editable in both
modes — `isRecordTableColumnHeadersReadOnlyComponentState`,
`isRecordTableColumnResizableComponentState`,
`isRecordTableCellsNonEditableComponentState` (Jotai component states)
- Hover portals and CSS column widths no longer bleed between multiple
table widgets — `getRecordTableHtmlId(tableId)`,
`getRecordTableCellId(tableId, …)`,
`updateRecordTableCSSVariable(tableId, …)` scope all DOM IDs and CSS
variables per instance
- Clicking inside a widget's content area no longer opens the settings
panel — `WidgetCardContent` stops click propagation when editable,
limiting settings-open to the card header and chrome
- Second widget creation no longer freezes the page —
`PageLayoutGridLayout` drag-select filters by `cell-` prefix to exclude
record UUIDs from grid cell detection

## Follow-up fixes

**Widget save flow**
- Saving a dashboard silently dropped record table widget changes
(column visibility, order, filters, sorts, aggregates) because widget
data save was bundled inside the layout save and only ran when layout
structure changed
- Widget data now persists independently via
`useSavePageLayoutWidgetsData`, called in all save paths (dashboard
save, record page save, layout customization save); saves are also
skipped when nothing has changed

**Drag-and-drop / checkbox columns in widget**
- Record table widgets showed the drag handle column and checkbox
selection column even though row reordering and multi-select are
meaningless in a read-only widget
- Two new component states
(`isRecordTableDragColumnHiddenComponentState`,
`isRecordTableCheckboxColumnHiddenComponentState`) hide each column
independently; widget tables now display only data columns

**Sticky column layout**
- Sticky positioning of the first three columns used `:nth-of-type` CSS
selectors — when drag or checkbox columns were hidden, the selector
targeted the wrong column and the first data column didn't stick
- Sticky CSS now targets semantic class names
(`RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME`, etc.) so sticky
behavior is correct regardless of which columns are hidden

**Save/Cancel buttons during edit mode**
- Save and Cancel command-menu buttons were unpinned during dashboard
edit mode because the pin logic excluded all items while
`isPageInEditMode` was true
- Items whose availability expression contains `isPageInEditMode` are
now exempted from the unpin rule; Save/Cancel stay pinned during editing

**Title input auto-focus**
- Selecting "Record Table" as widget type auto-focused the title input,
interrupting the configuration flow
- `focusTitleInput` is now `false` when navigating to record table
settings

**Morph relation field error**
- A field with missing `morphRelations` metadata crashed the page with a
"refresh" error from `mapObjectMetadataToGraphQLQuery`
- Now returns an empty array and silently omits the field from the query
instead of crashing

**`updateRecordMutation` prop removal**
- `RecordTableWithWrappers` required callers to pass an
`updateRecordMutation` callback, duplicating `useUpdateOneRecord` at
every usage site
- The mutation is now owned inside `RecordTableContextProvider` via
`RecordTableUpdateContext`; the prop is gone

**Standalone → Widget module rename**
- `record-table-standalone` module renamed to `record-table-widget` —
`StandaloneRecordTable` → `RecordTableWidget`,
`StandaloneRecordTableViewLoadEffect` →
`RecordTableWidgetViewLoadEffect`, etc.

**RecordTableRow cell extraction**
- Row rendering logic (`RecordTableCellDragAndDrop`,
`RecordTableCellCheckbox`, `RecordTableFieldsCells`, hotkey/arrow-key
effects) was duplicated between `RecordTableRow` and
`RecordTableRowVirtualizedFullData`
- Extracted `RecordTableRowCells` (shared cell content) and
`RecordTableStaticTr` (non-draggable `<tr>` wrapper); when drag column
is hidden, rows render inside a static `<tr>` instead of the draggable
wrapper

**View load effect metadata tracking**
- `RecordTableWidgetViewLoadEffect` now tracks
`objectMetadataItem.updatedAt` alongside `viewId` to re-load states when
metadata changes (e.g. field additions), preventing stale column data

**Data source dropdown deduplication**
- Extracted `filterReadableActiveObjectMetadataItems` util, shared by
both chart and record table data source dropdowns — removes duplicated
permission-filtering logic

**RECORD_TABLE view identifier mapping (server)**
- Added `RECORD_TABLE` case to
`fromPageLayoutWidgetConfigurationToUniversalConfiguration` and
`fromUniversalConfigurationToFlatPageLayoutWidgetConfiguration` so
widget views are properly mapped during workspace import/export

**GraphQL error handler typing (server)**
- `formatError` parameter changed from `any` to `unknown`;
`workspaceQueryRunnerGraphqlApiExceptionHandler` broadened from
`QueryFailedErrorWithCode` to `Error | QueryFailedError` — removes
unsafe type casts

**Save hook signature**
- `useSaveRecordTableWidgetsViewDataOnDashboardSave` no longer takes
`pageLayoutId` in constructor; receives it as a callback parameter,
eliminating the need for `useAtomComponentStateCallbackState`

**Customize Dashboard hidden during edit mode**
- The "Customize Dashboard" command was still visible while already
editing — its `conditionalAvailabilityExpression` now includes `not
isPageInEditMode`

**Fields dropdown split**
- `RecordTableFieldsDropdownContent` (300+ lines) split into
`RecordTableFieldsDropdownVisibleFieldsContent` and
`RecordTableFieldsDropdownHiddenFieldsContent`

**Checkbox placeholder cleanup**
- Removed unnecessary `StyledRecordTableTdContainer` wrapper from
`RecordTableCellCheckboxPlaceholder`
This commit is contained in:
Lucas Bordeau
2026-03-24 18:28:56 +01:00
committed by GitHub
parent c94aa7316a
commit 96f3ff0e90
152 changed files with 5369 additions and 1048 deletions
-7
View File
@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git stash:*)"
]
}
}
+1
View File
@@ -1,6 +1,7 @@
**/**/.env
.DS_Store
/.idea
.claude/settings.json
**/**/node_modules/
.cache
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
@@ -17,6 +18,8 @@ export const SaveDashboardSingleRecordCommand = () => {
const { savePageLayout } = useSavePageLayout(pageLayoutId);
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
@@ -26,6 +29,7 @@ export const SaveDashboardSingleRecordCommand = () => {
const result = await savePageLayout();
if (result.status === 'successful') {
await savePageLayoutWidgetsData(pageLayoutId);
closeSidePanelMenu();
setIsPageLayoutInEditMode(false);
}
@@ -1,8 +1,8 @@
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isDefined } from 'twenty-shared/utils';
@@ -22,7 +22,7 @@ export const SaveRecordPageLayoutSingleRecordCommand = () => {
const { savePageLayout } = useSavePageLayout(pageLayoutId);
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
@@ -33,7 +33,7 @@ export const SaveRecordPageLayoutSingleRecordCommand = () => {
const result = await savePageLayout();
if (result.status === 'successful') {
await saveFieldsWidgetGroups(pageLayoutId);
await savePageLayoutWidgetsData(pageLayoutId);
closeSidePanelMenu();
setIsPageLayoutInEditMode(false);
@@ -3,6 +3,7 @@ import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-re
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
@@ -15,6 +16,8 @@ export const SaveDashboardSingleRecordCommand = () => {
const { savePageLayout } = useSavePageLayout(pageLayoutId);
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
const { setIsPageLayoutInEditMode } =
useSetIsPageLayoutInEditMode(pageLayoutId);
@@ -24,6 +27,7 @@ export const SaveDashboardSingleRecordCommand = () => {
const result = await savePageLayout();
if (result.status === 'successful') {
await savePageLayoutWidgetsData(pageLayoutId);
closeSidePanelMenu();
setIsPageLayoutInEditMode(false);
}
@@ -82,9 +82,13 @@ export const useConvertBackendItemToCommandMenuItemConfig = () => {
return null;
}
const isEditModeItem =
item.conditionalAvailabilityExpression?.includes('isPageInEditMode') ??
false;
const isPinned =
item.availabilityType !== CommandMenuItemAvailabilityType.FALLBACK &&
!contextStoreIsPageInEditMode &&
(!contextStoreIsPageInEditMode || isEditModeItem) &&
item.isPinned;
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
@@ -4,7 +4,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/sta
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
@@ -35,7 +35,7 @@ export const useSaveLayoutCustomization = () => {
const { updatePageLayoutWithTabsAndWidgets } =
useUpdatePageLayoutWithTabsAndWidgets();
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
@@ -143,7 +143,7 @@ export const useSaveLayoutCustomization = () => {
}
}
await saveFieldsWidgetGroups(pageLayoutId);
await savePageLayoutWidgetsData(pageLayoutId);
}
if (hasAnyFailure) {
@@ -165,7 +165,7 @@ export const useSaveLayoutCustomization = () => {
}, [
saveDraft,
updatePageLayoutWithTabsAndWidgets,
saveFieldsWidgetGroups,
savePageLayoutWidgetsData,
exitLayoutCustomizationMode,
enqueueErrorSnackBar,
isRecordPageLayoutEditingEnabled,
@@ -0,0 +1,21 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const filterReadableActiveObjectMetadataItems = (
objectMetadataItems: EnrichedObjectMetadataItem[],
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>,
): EnrichedObjectMetadataItem[] =>
objectMetadataItems.filter((objectMetadataItem) => {
const objectPermissions =
objectPermissionsByObjectMetadataId[objectMetadataItem.id];
return (
isDefined(objectPermissions) &&
objectPermissions.canReadObjectRecords &&
objectMetadataItem.isActive
);
});
@@ -65,9 +65,7 @@ export const mapObjectMetadataToGraphQLQuery = ({
}
if (!isDefined(fieldMetadata.morphRelations)) {
throw new Error(
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
);
return [];
}
return fieldMetadata.morphRelations.map((morphRelation) => ({
@@ -100,9 +98,7 @@ export const mapObjectMetadataToGraphQLQuery = ({
}
if (!isDefined(fieldMetadata.morphRelations)) {
throw new Error(
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
);
return [];
}
return fieldMetadata.morphRelations.map((morphRelation) => ({
@@ -1,6 +1,5 @@
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { type RecordUpdateHookParams } from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
@@ -22,23 +21,13 @@ export const RecordIndexTableContainer = ({
RECORD_INDEX_REMOVE_SORTING_MODAL_ID,
);
const { updateOneRecord } = useUpdateOneRecord();
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
updateOneRecord({
objectNameSingular,
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
};
return (
<>
<RecordIndexTableContainerEffect />
<RecordTableWithWrappers
recordTableId={recordTableId}
objectNameSingular={objectNameSingular}
viewBarId={viewBarInstanceId}
updateRecordMutation={updateEntity}
/>
{isModalOpened && <RecordIndexRemoveSortingModal />}
</>
@@ -7,6 +7,7 @@ import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/uti
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups';
import { recordIndexGroupFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupFieldMetadataComponentState';
@@ -32,6 +33,7 @@ import { type View } from '@/views/types/View';
import { getFilterableFields } from '@/views/utils/getFilterableFields';
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToColumnDefinitions';
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { atom, useStore } from 'jotai';
import { useCallback } from 'react';
@@ -132,6 +134,10 @@ export const useLoadRecordIndexStates = () => {
allFilterableFields,
);
const recordFilterGroups = mapViewFilterGroupsToRecordFilterGroups(
view.viewFilterGroups ?? [],
);
const contextStoreFilters = mapViewFiltersToFilters(
view.viewFilters,
filterableFieldMetadataItems,
@@ -172,6 +178,10 @@ export const useLoadRecordIndexStates = () => {
currentRecordFiltersComponentState.atomFamily({
instanceId: recordIndexId,
});
const currentRecordFilterGroupsAtom =
currentRecordFilterGroupsComponentState.atomFamily({
instanceId: recordIndexId,
});
const currentRecordSortsAtom =
currentRecordSortsComponentState.atomFamily({
instanceId: recordIndexId,
@@ -237,6 +247,7 @@ export const useLoadRecordIndexStates = () => {
batchSet(hasInitializedFieldsAtom, true);
batchSet(currentRecordFiltersAtom, recordFilters);
batchSet(currentRecordFilterGroupsAtom, recordFilterGroups);
batchSet(hasInitializedFiltersAtom, true);
batchSet(currentRecordSortsAtom, view.viewSorts);
@@ -0,0 +1,21 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWidgetSetReadOnlyColumnHeadersEffect } from '@/object-record/record-table-widget/components/RecordTableWidgetSetReadOnlyColumnHeadersEffect';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
export const RecordTableWidget = () => {
const { objectNameSingular, recordIndexId, viewBarInstanceId } =
useRecordIndexContextOrThrow();
return (
<>
<RecordTableWidgetSetReadOnlyColumnHeadersEffect
recordTableId={recordIndexId}
/>
<RecordTableWithWrappers
recordTableId={recordIndexId}
objectNameSingular={objectNameSingular}
viewBarId={viewBarInstanceId}
/>
</>
);
};
@@ -0,0 +1,42 @@
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useEffect } from 'react';
type RecordTableWidgetContextStoreInitEffectProps = {
objectMetadataItemId: string;
viewId: string;
};
export const RecordTableWidgetContextStoreInitEffect = ({
objectMetadataItemId,
viewId,
}: RecordTableWidgetContextStoreInitEffectProps) => {
const setContextStoreCurrentObjectMetadataItemId = useSetAtomComponentState(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const setContextStoreCurrentViewId = useSetAtomComponentState(
contextStoreCurrentViewIdComponentState,
);
const setContextStoreCurrentViewType = useSetAtomComponentState(
contextStoreCurrentViewTypeComponentState,
);
useEffect(() => {
setContextStoreCurrentObjectMetadataItemId(objectMetadataItemId);
setContextStoreCurrentViewId(viewId);
setContextStoreCurrentViewType(ContextStoreViewType.Table);
}, [
objectMetadataItemId,
viewId,
setContextStoreCurrentObjectMetadataItemId,
setContextStoreCurrentViewId,
setContextStoreCurrentViewType,
]);
return null;
};
@@ -0,0 +1,109 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
import { RecordTableWidgetContextStoreInitEffect } from '@/object-record/record-table-widget/components/RecordTableWidgetContextStoreInitEffect';
import { RecordTableWidgetViewLoadEffect } from '@/object-record/record-table-widget/components/RecordTableWidgetViewLoadEffect';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { type PropsWithChildren, useCallback } from 'react';
import { AppPath } from 'twenty-shared/types';
import { getAppPath } from 'twenty-shared/utils';
type RecordTableWidgetProviderProps = PropsWithChildren<{
objectNameSingular: string;
viewId: string;
widgetId: string;
}>;
export const RecordTableWidgetProvider = ({
objectNameSingular,
viewId,
widgetId,
children,
}: RecordTableWidgetProviderProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
viewId,
);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
objectMetadataItem.id,
);
const {
fieldDefinitionByFieldMetadataItemId,
fieldMetadataItemByFieldMetadataItemId,
labelIdentifierFieldMetadataItem,
recordFieldByFieldMetadataItemId,
} = useRecordIndexFieldMetadataDerivedStates(
objectMetadataItem,
recordIndexId,
);
const indexIdentifierUrl = useCallback(
(recordId: string) => {
return getAppPath(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: recordId,
});
},
[objectNameSingular],
);
const handleIndexRecordsLoaded = useCallback(() => {}, []);
if (!objectPermissions.canReadObjectRecords) {
return null;
}
return (
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: `record-table-widget-${widgetId}` }}
>
<RecordTableWidgetContextStoreInitEffect
objectMetadataItemId={objectMetadataItem.id}
viewId={viewId}
/>
<RecordIndexContextProvider
value={{
objectPermissionsByObjectMetadataId,
recordIndexId,
viewBarInstanceId: recordIndexId,
objectNamePlural: objectMetadataItem.namePlural,
objectNameSingular,
objectMetadataItem,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
indexIdentifierUrl,
recordFieldByFieldMetadataItemId,
labelIdentifierFieldMetadataItem,
fieldMetadataItemByFieldMetadataItemId,
fieldDefinitionByFieldMetadataItemId,
}}
>
<ViewComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordIndexId}
>
<RecordTableWidgetViewLoadEffect
viewId={viewId}
objectMetadataItem={objectMetadataItem}
/>
{children}
</RecordComponentInstanceContextsWrapper>
</ViewComponentInstanceContext.Provider>
</RecordIndexContextProvider>
</ContextStoreComponentInstanceContext.Provider>
);
};
@@ -0,0 +1,56 @@
import { useIsPageLayoutInEditMode } from '@/page-layout/hooks/useIsPageLayoutInEditMode';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { isRecordTableCellsNonEditableComponentState } from '@/object-record/record-table/states/isRecordTableCellsNonEditableComponentState';
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
import { useStore } from 'jotai';
import { useEffect } from 'react';
export const RecordTableWidgetSetReadOnlyColumnHeadersEffect = ({
recordTableId,
}: {
recordTableId: string;
}) => {
const store = useStore();
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
useEffect(() => {
store.set(
isRecordTableColumnHeadersReadOnlyComponentState.atomFamily({
instanceId: recordTableId,
}),
true,
);
store.set(
isRecordTableDragColumnHiddenComponentState.atomFamily({
instanceId: recordTableId,
}),
true,
);
store.set(
isRecordTableCheckboxColumnHiddenComponentState.atomFamily({
instanceId: recordTableId,
}),
true,
);
store.set(
isRecordTableColumnResizableComponentState.atomFamily({
instanceId: recordTableId,
}),
isPageLayoutInEditMode,
);
store.set(
isRecordTableCellsNonEditableComponentState.atomFamily({
instanceId: recordTableId,
}),
true,
);
}, [store, recordTableId, isPageLayoutInEditMode]);
return null;
};
@@ -0,0 +1,70 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { lastLoadedRecordTableWidgetViewIdComponentState } from '@/object-record/record-table-widget/states/lastLoadedRecordTableWidgetViewIdComponentState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
type RecordTableWidgetViewLoadEffectProps = {
viewId: string;
objectMetadataItem: EnrichedObjectMetadataItem;
};
export const RecordTableWidgetViewLoadEffect = ({
viewId,
objectMetadataItem,
}: RecordTableWidgetViewLoadEffectProps) => {
const { loadRecordIndexStates } = useLoadRecordIndexStates();
const [
lastLoadedRecordTableWidgetViewId,
setLastLoadedRecordTableWidgetViewId,
] = useAtomComponentState(lastLoadedRecordTableWidgetViewIdComponentState);
const viewFromViewId = useAtomFamilySelectorValue(
viewFromViewIdFamilySelector,
{
viewId,
},
);
const viewHasFields =
isDefined(viewFromViewId) && viewFromViewId.viewFields.length > 0;
useEffect(() => {
if (!isDefined(viewFromViewId)) {
return;
}
if (!viewHasFields) {
return;
}
if (
viewId === lastLoadedRecordTableWidgetViewId?.viewId &&
objectMetadataItem.updatedAt ===
lastLoadedRecordTableWidgetViewId?.objectMetadataItemUpdatedAt
) {
return;
}
loadRecordIndexStates(viewFromViewId, objectMetadataItem);
setLastLoadedRecordTableWidgetViewId({
viewId,
objectMetadataItemUpdatedAt: objectMetadataItem.updatedAt,
});
}, [
viewId,
lastLoadedRecordTableWidgetViewId,
setLastLoadedRecordTableWidgetViewId,
viewFromViewId,
viewHasFields,
objectMetadataItem,
loadRecordIndexStates,
]);
return null;
};
@@ -0,0 +1,12 @@
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const lastLoadedRecordTableWidgetViewIdComponentState =
createAtomComponentState<{
viewId: string;
objectMetadataItemUpdatedAt: string;
} | null>({
key: 'lastLoadedRecordTableWidgetViewIdComponentState',
defaultValue: null,
componentInstanceContext: ContextStoreComponentInstanceContext,
});
@@ -6,6 +6,8 @@ import { RECORD_TABLE_COLUMN_MIN_WIDTH } from '@/object-record/record-table/cons
import { RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableColumnWithGroupLastEmptyColumnWidthVariableName';
import { RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE } from '@/object-record/record-table/constants/RecordTableLabelIdentifierColumnWidthOnMobile';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { shouldCompactRecordTableFirstColumnComponentState } from '@/object-record/record-table/states/shouldCompactRecordTableFirstColumnComponentState';
@@ -25,7 +27,7 @@ export const RecordTableColumnWidthEffect = () => {
resizedFieldMetadataIdComponentState,
);
const { visibleRecordFields } = useRecordTableContextOrThrow();
const { visibleRecordFields, recordTableId } = useRecordTableContextOrThrow();
const shouldCompactRecordTableFirstColumn = useAtomComponentStateValue(
shouldCompactRecordTableFirstColumnComponentState,
@@ -35,6 +37,14 @@ export const RecordTableColumnWidthEffect = () => {
recordTableWidthComponentState,
);
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
useEffect(() => {
if (isDefined(resizedFieldMetadataId)) {
return;
@@ -44,6 +54,8 @@ export const RecordTableColumnWidthEffect = () => {
recordFields: visibleRecordFields,
shouldCompactFirstColumn: shouldCompactRecordTableFirstColumn,
tableWidth: recordTableWidth,
isDragColumnHidden: isRecordTableDragColumnHidden,
isCheckboxColumnHidden: isRecordTableCheckboxColumnHidden,
});
const { visibleRecordFieldsWidth } = computeVisibleRecordFieldsWidthOnTable(
@@ -53,31 +65,44 @@ export const RecordTableColumnWidthEffect = () => {
},
);
const dragColumnWidth = isRecordTableDragColumnHidden
? 0
: RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH;
const checkboxColumnWidth = isRecordTableCheckboxColumnHidden
? 0
: RECORD_TABLE_COLUMN_CHECKBOX_WIDTH;
const leftColumnsWidth = dragColumnWidth + checkboxColumnWidth;
const totalTableBodyWidth =
visibleRecordFieldsWidth +
RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH +
RECORD_TABLE_COLUMN_CHECKBOX_WIDTH +
leftColumnsWidth +
RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH +
lastColumnWidth +
visibleRecordFields.length;
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_VIRTUALIZATION_BODY_PLACEHOLDER_WIDTH_CSS_VARIABLE_NAME,
`${totalTableBodyWidth}px`,
);
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME,
`${lastColumnWidth}px`,
);
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME,
`${lastColumnWidth}px`,
);
for (const [index, recordField] of visibleRecordFields.entries()) {
updateRecordTableCSSVariable(
recordTableId,
getRecordTableColumnFieldWidthCSSVariableName(index),
`${recordField.size}px`,
);
@@ -85,6 +110,7 @@ export const RecordTableColumnWidthEffect = () => {
if (shouldCompactRecordTableFirstColumn) {
updateRecordTableCSSVariable(
recordTableId,
getRecordTableColumnFieldWidthCSSVariableName(0),
`${RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE}px`,
);
@@ -93,6 +119,7 @@ export const RecordTableColumnWidthEffect = () => {
visibleRecordFields[0]?.size ?? RECORD_TABLE_COLUMN_MIN_WIDTH;
updateRecordTableCSSVariable(
recordTableId,
getRecordTableColumnFieldWidthCSSVariableName(0),
`${firstColumnWidth}px`,
);
@@ -102,6 +129,9 @@ export const RecordTableColumnWidthEffect = () => {
visibleRecordFields,
recordTableWidth,
shouldCompactRecordTableFirstColumn,
isRecordTableDragColumnHidden,
isRecordTableCheckboxColumnHidden,
recordTableId,
]);
return null;
@@ -4,8 +4,10 @@ import {
getRecordTableColumnWidthInlineStyles,
RecordTableStyleWrapper,
} from '@/object-record/record-table/components/RecordTableStyleWrapper';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { RecordTableWidthEffect } from '@/object-record/record-table/components/RecordTableWidthEffect';
import { RECORD_TABLE_HTML_ID } from '@/object-record/record-table/constants/RecordTableHtmlId';
import { getRecordTableHtmlId } from '@/object-record/record-table/utils/getRecordTableHtmlId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody';
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
@@ -18,6 +20,7 @@ import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-sel
import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateCallbackState';
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { useStore } from 'jotai';
import { useCallback, useMemo, useRef, useState } from 'react';
@@ -128,9 +131,26 @@ export const RecordTableContent = ({
[store, isSomeCellInEditMode, recordTableHoverPositionCallbackState],
);
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
const columnWidthStyles = useMemo(
() => getRecordTableColumnWidthInlineStyles(visibleRecordFields),
[visibleRecordFields],
() =>
getRecordTableColumnWidthInlineStyles({
visibleRecordFields,
isDragColumnHidden: isRecordTableDragColumnHidden,
isCheckboxColumnHidden: isRecordTableCheckboxColumnHidden,
}),
[
visibleRecordFields,
isRecordTableDragColumnHidden,
isRecordTableCheckboxColumnHidden,
],
);
return (
@@ -139,7 +159,7 @@ export const RecordTableContent = ({
ref={tableBodyRef}
isDragging={isDragging}
style={columnWidthStyles}
id={RECORD_TABLE_HTML_ID}
id={getRecordTableHtmlId(recordTableId)}
onMouseMove={handleDelegatedMouseMove}
onMouseLeave={handleMouseLeave}
>
@@ -1,15 +1,18 @@
import { type ReactNode } from 'react';
import { type ReactNode, useCallback } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { visibleRecordFieldsComponentSelector } from '@/object-record/record-field/states/visibleRecordFieldsComponentSelector';
import { type RecordUpdateHookParams } from '@/object-record/record-field/ui/contexts/FieldContext';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { RECORD_TABLE_COLUMN_MIN_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnMinWidth';
import { RecordTableUpdateContext } from '@/object-record/record-table/contexts/RecordTableUpdateContext';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
@@ -43,6 +46,19 @@ export const RecordTableContextProvider = ({
visibleRecordFieldsComponentSelector,
);
const { updateOneRecord } = useUpdateOneRecord();
const updateRecord = useCallback(
({ variables }: RecordUpdateHookParams) => {
updateOneRecord({
objectNameSingular,
idToUpdate: variables.where.id as string,
updateOneRecordInput: variables.updateOneRecordInput,
});
},
[objectNameSingular, updateOneRecord],
);
const recordIndexOpenRecordIn = useAtomStateValue(
recordIndexOpenRecordInState,
);
@@ -71,7 +87,9 @@ export const RecordTableContextProvider = ({
triggerEvent,
}}
>
{children}
<RecordTableUpdateContext.Provider value={updateRecord}>
{children}
</RecordTableUpdateContext.Provider>
</RecordTableContextInternalProvider>
</RecordFieldsScopeContextProvider>
);
@@ -7,7 +7,9 @@ import { RecordTableWidthEffect } from '@/object-record/record-table/components/
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
import { RECORD_TABLE_HTML_ID } from '@/object-record/record-table/constants/RecordTableHtmlId';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { getRecordTableHtmlId } from '@/object-record/record-table/utils/getRecordTableHtmlId';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
@@ -33,7 +35,15 @@ export interface RecordTableEmptyProps {
}
export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const { visibleRecordFields, recordTableId } = useRecordTableContextOrThrow();
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
const recordTableWidth = useAtomComponentStateValue(
recordTableWidthComponentState,
@@ -66,10 +76,19 @@ export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
visibleRecordFields,
});
const dragColumnWidth = isRecordTableDragColumnHidden
? 0
: RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH;
const checkboxColumnWidth = isRecordTableCheckboxColumnHidden
? 0
: RECORD_TABLE_COLUMN_CHECKBOX_WIDTH;
const leftColumnsWidth = dragColumnWidth + checkboxColumnWidth;
const emptyTableContainerComputedWidth =
visibleRecordFieldsWidth +
RECORD_TABLE_COLUMN_CHECKBOX_WIDTH +
RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH +
leftColumnsWidth +
RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH +
totalColumnsBorderWidth +
resizeOffsetToAddOnlyIfItMakesTableContainerGrow;
@@ -80,7 +99,7 @@ export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
);
const columnWidthStyles = useMemo(
() => getRecordTableColumnWidthInlineStyles(visibleRecordFields),
() => getRecordTableColumnWidthInlineStyles({ visibleRecordFields }),
[visibleRecordFields],
);
@@ -89,7 +108,7 @@ export const RecordTableEmpty = ({ tableBodyRef }: RecordTableEmptyProps) => {
<RecordTableStyleWrapper
ref={tableBodyRef}
style={columnWidthStyles}
id={RECORD_TABLE_HTML_ID}
id={getRecordTableHtmlId(recordTableId)}
>
<RecordTableHeader />
</RecordTableStyleWrapper>
@@ -3,6 +3,7 @@ import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/r
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
import { isRecordTableCellsNonEditableComponentState } from '@/object-record/record-table/states/isRecordTableCellsNonEditableComponentState';
import { RecordTableActionRow } from '@/object-record/record-table/record-table-row/components/RecordTableActionRow';
import { isRecordTableCreateDisabled } from '@/object-record/record-table/utils/isRecordTableCreateDisabled';
import { useLoadRecordsToVirtualRows } from '@/object-record/record-table/virtualization/hooks/useLoadRecordsToVirtualRows';
@@ -17,6 +18,10 @@ import { IconPlus } from 'twenty-ui/display';
export const RecordTableNoRecordGroupAddNew = () => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isRecordTableCellsNonEditable = useAtomComponentStateValue(
isRecordTableCellsNonEditableComponentState,
);
const { createNewIndexRecord } = useCreateNewIndexRecord({
objectMetadataItem,
});
@@ -58,6 +63,10 @@ export const RecordTableNoRecordGroupAddNew = () => {
totalNumberOfRecordsToVirtualize,
]);
if (isRecordTableCellsNonEditable) {
return null;
}
if (hasAnySoftDeleteFilterOnView) {
return null;
}
@@ -1,6 +1,7 @@
import { shouldCompactRecordIndexLabelIdentifierComponentState } from '@/object-record/record-index/states/shouldCompactRecordIndexLabelIdentifierComponentState';
import { RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableHorizontalScrollShadowVisibilityCssVariableName';
import { RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME } from '@/object-record/record-table/constants/RecordTableVerticalScrollShadowVisibilityCssVariableName';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { shouldCompactRecordTableFirstColumnComponentState } from '@/object-record/record-table/states/shouldCompactRecordTableFirstColumnComponentState';
@@ -15,6 +16,7 @@ import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
export const RecordTableScrollAndZIndexEffect = () => {
const { recordTableId } = useRecordTableContextOrThrow();
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
const isMobile = useIsMobile();
const [
@@ -51,6 +53,7 @@ export const RecordTableScrollAndZIndexEffect = () => {
: 'hidden';
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
newVisibilityOfShadows,
);
@@ -66,6 +69,7 @@ export const RecordTableScrollAndZIndexEffect = () => {
: 'hidden';
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
newVisibilityOfShadows,
);
@@ -88,6 +92,7 @@ export const RecordTableScrollAndZIndexEffect = () => {
scrollWrapperHTMLElement?.removeEventListener('scroll', handleScroll);
};
}, [
recordTableId,
scrollWrapperHTMLElement,
isRecordTableScrolledVertically,
isRecordTableScrolledHorizontally,
@@ -2,6 +2,7 @@ import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRecordTableCellFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableCellFocusActiveComponentState';
import { recordTableFocusPositionComponentState } from '@/object-record/record-table/states/recordTableFocusPositionComponentState';
import { getRecordTableCellId } from '@/object-record/record-table/utils/getRecordTableCellId';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
@@ -19,7 +20,6 @@ export const RecordTableScrollToFocusedCellEffect = () => {
recordTableId,
);
// Handle cell focus
useEffect(() => {
if (!isRecordTableCellFocusActive) {
return;
@@ -30,7 +30,11 @@ export const RecordTableScrollToFocusedCellEffect = () => {
}
const focusElement = document.getElementById(
`record-table-cell-${recordTableFocusPosition.column}-${recordTableFocusPosition.row}`,
getRecordTableCellId(
recordTableId,
recordTableFocusPosition.column,
recordTableFocusPosition.row,
),
);
if (!focusElement) {
@@ -41,9 +45,11 @@ export const RecordTableScrollToFocusedCellEffect = () => {
if (isSecondColumn) {
const checkBoxColumnCell = document.getElementById(
`record-table-cell-0-0`,
getRecordTableCellId(recordTableId, 0, 0),
);
const firstColumnCell = document.getElementById(
getRecordTableCellId(recordTableId, 1, 0),
);
const firstColumnCell = document.getElementById(`record-table-cell-1-0`);
if (isDefined(checkBoxColumnCell) && isDefined(firstColumnCell)) {
const checkBoxColumnWidth = checkBoxColumnCell.offsetWidth;
@@ -63,7 +69,7 @@ export const RecordTableScrollToFocusedCellEffect = () => {
focusElement.style.scrollMarginBottom = '';
}
};
}, [recordTableFocusPosition, isRecordTableCellFocusActive]);
}, [recordTableId, recordTableFocusPosition, isRecordTableCellFocusActive]);
return null;
};
@@ -1,6 +1,7 @@
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { getRecordTableCellId } from '@/object-record/record-table/utils/getRecordTableCellId';
import { focusedRecordTableRowIndexComponentState } from '@/object-record/record-table/states/focusedRecordTableRowIndexComponentState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
@@ -42,7 +43,7 @@ export const RecordTableScrollToFocusedRowEffect = () => {
}
const focusElement = document.getElementById(
`record-table-cell-0-${focusedRecordTableRowIndex}`,
getRecordTableCellId(recordTableId, 0, focusedRecordTableRowIndex),
);
if (!focusElement) {
@@ -59,7 +60,12 @@ export const RecordTableScrollToFocusedRowEffect = () => {
focusElement.style.scrollMarginBottom = '';
}
};
}, [focusedRecordTableRowIndex, isRecordTableRowFocusActive, allRecordIds]);
}, [
recordTableId,
focusedRecordTableRowIndex,
isRecordTableRowFocusActive,
allRecordIds,
]);
return null;
};
@@ -19,6 +19,11 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
export { HorizontalScrollBoxShadowCSS, VerticalScrollBoxShadowCSS };
export const RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR =
'--record-table-drag-drop-width';
export const RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR =
'--record-table-checkbox-width';
const MAX_COLUMNS = 100;
const columnFieldWidthRules = Array.from(
@@ -31,9 +36,15 @@ const columnFieldWidthRules = Array.from(
}`,
).join('\n');
export const getRecordTableColumnWidthInlineStyles = (
visibleRecordFields: RecordField[],
): Record<string, string> => {
export const getRecordTableColumnWidthInlineStyles = ({
visibleRecordFields,
isDragColumnHidden,
isCheckboxColumnHidden,
}: {
visibleRecordFields: RecordField[];
isDragColumnHidden?: boolean;
isCheckboxColumnHidden?: boolean;
}): Record<string, string> => {
const style: Record<string, string> = {};
for (let i = 0; i < visibleRecordFields.length; i++) {
@@ -41,6 +52,14 @@ export const getRecordTableColumnWidthInlineStyles = (
`${visibleRecordFields[i].size}px`;
}
style[RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR] = isDragColumnHidden
? '0px'
: `${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px`;
style[RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR] = isCheckboxColumnHidden
? '0px'
: `${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`;
return style;
};
@@ -58,64 +77,65 @@ const StyledTable = styled.div<{
width: 100%;
div.header-cell:nth-of-type(n + 5) {
div.header-cell {
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsNormal};
}
div.header-cell:nth-of-type(1) {
div.header-cell.${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME} {
background-color: ${themeCssVariables.background.primary};
left: 0px;
position: sticky;
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsSticky};
}
div.header-cell:nth-of-type(2) {
div.header-cell.${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME} {
background-color: ${themeCssVariables.background.primary};
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
left: var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR});
position: sticky;
top: 0;
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsSticky};
}
div.header-cell:nth-of-type(3) {
div.header-cell.${getRecordTableColumnFieldWidthClassName(0)} {
background-color: ${themeCssVariables.background.primary};
left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
left: calc(
var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR}) +
var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR})
);
position: sticky;
right: 0;
z-index: ${TABLE_Z_INDEX.headerColumns.headerColumnsSticky};
${HorizontalScrollBoxShadowCSS}
}
div.table-cell:nth-of-type(1) {
div.table-cell.${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME} {
left: 0px;
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.sticky};
}
div.table-cell:nth-of-type(2) {
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
div.table-cell.${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME} {
left: var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR});
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.sticky};
}
div.table-cell-0-0 {
left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
left: calc(
var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR}) +
var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR})
);
position: sticky;
${HorizontalScrollBoxShadowCSS}
}
div.table-cell:nth-of-type(3) {
left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
div.table-cell.${getRecordTableColumnFieldWidthClassName(0)} {
left: calc(
var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR}) +
var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR})
);
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.sticky};
@@ -123,15 +143,15 @@ const StyledTable = styled.div<{
}
div.${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME} {
max-width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
min-width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
max-width: var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR});
min-width: var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR});
width: var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR});
}
div.${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME} {
max-width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
min-width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px;
max-width: var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR});
min-width: var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR});
width: var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR});
}
div.${RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME} {
@@ -1,5 +1,4 @@
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
import { RecordTable } from '@/object-record/record-table/components/RecordTable';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
@@ -11,17 +10,14 @@ import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/use
import { PageFocusId } from '@/types/PageFocusId';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
type RecordTableWithWrappersProps = {
objectNameSingular: string;
recordTableId: string;
viewBarId: string;
updateRecordMutation: (params: any) => void;
};
export const RecordTableWithWrappers = ({
updateRecordMutation,
objectNameSingular,
recordTableId,
viewBarId,
@@ -66,10 +62,7 @@ export const RecordTableWithWrappers = ({
<ScrollWrapper
componentInstanceId={`record-table-scroll-${recordTableId}`}
>
<RecordUpdateContext.Provider value={updateRecordMutation}>
<RecordTable />
<RecordIndexTableContainerEffect />
</RecordUpdateContext.Provider>
<RecordTable />
</ScrollWrapper>
</EntityDeleteContext.Provider>
</RecordTableContextProvider>
@@ -2,6 +2,6 @@ import { createContext } from 'react';
import { type RecordUpdateHookParams } from '@/object-record/record-field/ui/contexts/FieldContext';
export const RecordUpdateContext = createContext<
(params: RecordUpdateHookParams) => void
>({} as any);
export const RecordTableUpdateContext = createContext<
((params: RecordUpdateHookParams) => void) | undefined
>(undefined);
@@ -1,4 +1,6 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState';
import { shouldCompactRecordTableFirstColumnComponentState } from '@/object-record/record-table/states/shouldCompactRecordTableFirstColumnComponentState';
import { computeLastRecordTableColumnWidth } from '@/object-record/record-table/utils/computeLastRecordTableColumnWidth';
@@ -15,10 +17,20 @@ export const useRecordTableLastColumnWidthToFill = () => {
shouldCompactRecordTableFirstColumnComponentState,
);
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
const { lastColumnWidth } = computeLastRecordTableColumnWidth({
recordFields: visibleRecordFields,
tableWidth: recordTableWidth,
shouldCompactFirstColumn: shouldCompactRecordTableFirstColumn,
isDragColumnHidden: isRecordTableDragColumnHidden,
isCheckboxColumnHidden: isRecordTableCheckboxColumnHidden,
});
return {
@@ -4,6 +4,9 @@ import { RecordTableRowDraggableContextProvider } from '@/object-record/record-t
import { RecordTableBody } from '@/object-record/record-table/record-table-body/components/RecordTableBody';
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
@@ -12,6 +15,14 @@ import { RecordTableTr } from '@/object-record/record-table/record-table-row/com
export const RecordTableBodyLoading = () => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
return (
<RecordTableBody>
{Array.from({ length: 80 }).map((_, rowIndex) => (
@@ -38,8 +49,10 @@ export const RecordTableBodyLoading = () => {
data-testid={`row-id-${rowIndex}`}
data-selectable-id={`row-id-${rowIndex}`}
>
<RecordTableCellDragAndDrop />
<RecordTableCellCheckbox />
{!isRecordTableDragColumnHidden && <RecordTableCellDragAndDrop />}
{!isRecordTableCheckboxColumnHidden && (
<RecordTableCellCheckbox />
)}
{visibleRecordFields.map((recordField, index) => (
<RecordTableCellLoading
key={recordField.fieldMetadataItemId}
@@ -65,13 +65,13 @@ const cloneColumnFieldWidthRules = Array.from(
).join('\n');
const StyledRowDraggableCloneCSSBridge = styled.div`
div.table-cell:nth-of-type(1) {
div.table-cell.${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME} {
left: 0px;
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.sticky};
}
div.table-cell:nth-of-type(2) {
div.table-cell.${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME} {
left: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.sticky};
@@ -84,7 +84,7 @@ const StyledRowDraggableCloneCSSBridge = styled.div`
${HorizontalScrollBoxShadowCSS}
}
div.table-cell:nth-of-type(3) {
div.table-cell.${getRecordTableColumnFieldWidthClassName(0)} {
left: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
position: sticky;
z-index: ${TABLE_Z_INDEX.cell.sticky};
@@ -147,7 +147,7 @@ export const RecordTableBodyVirtualizedDraggableClone = ({
const columnWidthStyles = useMemo(() => {
const styles: Record<string, string> =
getRecordTableColumnWidthInlineStyles(visibleRecordFields);
getRecordTableColumnWidthInlineStyles({ visibleRecordFields });
styles[RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME] =
`${lastColumnWidth}px`;
styles[
@@ -5,7 +5,9 @@ import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldCont
import { isFieldIdentifierDisplay } from '@/object-record/record-field/ui/meta-types/display/utils/isFieldIdentifierDisplay';
import { RECORD_CHIP_CLICK_OUTSIDE_ID } from '@/object-record/record-table/constants/RecordChipClickOutsideId';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
import { getRecordTableCellId } from '@/object-record/record-table/utils/getRecordTableCellId';
import { ThemeContext } from 'twenty-ui/theme-constants';
const StyledBaseContainer = styled.div<{
@@ -57,6 +59,7 @@ export const RecordTableCellBaseContainer = ({
const { theme } = useContext(ThemeContext);
const { cellPosition } = useContext(RecordTableCellContext);
const { recordTableId } = useRecordTableContextOrThrow();
const isChipDisplay = isFieldIdentifierDisplay(
fieldDefinition,
@@ -74,7 +77,11 @@ export const RecordTableCellBaseContainer = ({
fontColorSecondary={theme.font.color.secondary}
fontColorMedium={theme.border.color.medium}
isReadOnly={isReadOnly ?? false}
id={`record-table-cell-${cellPosition.column}-${cellPosition.row}`}
id={getRecordTableCellId(
recordTableId,
cellPosition.column,
cellPosition.row,
)}
data-record-table-col={cellPosition.column}
data-record-table-row={cellPosition.row}
data-click-outside-id={
@@ -19,24 +19,18 @@ const StyledContainer = styled.div`
width: ${RECORD_TABLE_COLUMN_CHECKBOX_WIDTH};
`;
const StyledRecordTableTdContainer = styled.div`
border-left: 1px solid transparent;
`;
export const RecordTableCellCheckboxPlaceholder = () => {
const { hasUserSelectedAllRows } = useRecordTableBodyContextOrThrow();
return (
<StyledRecordTableTdContainer>
<RecordTableCellStyleWrapper
isSelected={hasUserSelectedAllRows}
hasRightBorder={false}
widthClassName={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME}
>
<StyledContainer data-select-disable>
<Checkbox hoverable checked={hasUserSelectedAllRows === true} />
</StyledContainer>
</RecordTableCellStyleWrapper>
</StyledRecordTableTdContainer>
<RecordTableCellStyleWrapper
isSelected={hasUserSelectedAllRows}
hasRightBorder={false}
widthClassName={RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME}
>
<StyledContainer data-select-disable>
<Checkbox hoverable checked={hasUserSelectedAllRows === true} />
</StyledContainer>
</RecordTableCellStyleWrapper>
);
};
@@ -9,9 +9,11 @@ import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junctio
import { getTargetObjectMetadataIdsFromField } from '@/object-record/record-field/ui/utils/junction/getTargetObjectMetadataIdsFromField';
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableUpdateContext } from '@/object-record/record-table/contexts/RecordTableUpdateContext';
import { isRecordTableCellsNonEditableComponentState } from '@/object-record/record-table/states/isRecordTableCellsNonEditableComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useContext, type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
type RecordTableCellFieldContextGenericProps = {
@@ -25,6 +27,10 @@ export const RecordTableCellFieldContextGeneric = ({
}: RecordTableCellFieldContextGenericProps) => {
const { recordId, isRecordReadOnly } = useRecordTableRowContextOrThrow();
const isRecordTableCellsNonEditable = useAtomComponentStateValue(
isRecordTableCellsNonEditableComponentState,
);
const { objectMetadataItem, objectMetadataItems, objectPermissions } =
useRecordTableContextOrThrow();
@@ -36,7 +42,7 @@ export const RecordTableCellFieldContextGeneric = ({
const fieldDefinition =
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
const updateRecord = useContext(RecordUpdateContext);
const updateRecord = useContext(RecordTableUpdateContext);
let hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
@@ -90,7 +96,7 @@ export const RecordTableCellFieldContextGeneric = ({
fieldMetadataItemId: recordField.fieldMetadataItemId,
recordId,
fieldDefinition: fieldDefinition,
useUpdateRecord: () => [updateRecord, {}],
useUpdateRecord: updateRecord ? () => [updateRecord, {}] : undefined,
isLabelIdentifier: isLabelIdentifierField({
fieldMetadataItem: {
id: fieldDefinition.fieldMetadataId,
@@ -99,16 +105,18 @@ export const RecordTableCellFieldContextGeneric = ({
objectMetadataItem,
}),
displayedMaxRows: 1,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly: isRecordReadOnly ?? false,
isSystemObject: objectMetadataItem.isSystem,
objectPermissions,
fieldMetadataItem: {
id: fieldDefinition.fieldMetadataId,
isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false,
isCustom: fieldDefinition.metadata.isCustom ?? false,
},
}),
isRecordFieldReadOnly:
isRecordTableCellsNonEditable ||
isRecordFieldReadOnly({
isRecordReadOnly: isRecordReadOnly ?? false,
isSystemObject: objectMetadataItem.isSystem,
objectPermissions,
fieldMetadataItem: {
id: fieldDefinition.fieldMetadataId,
isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false,
isCustom: fieldDefinition.metadata.isCustom ?? false,
},
}),
isForbidden: !hasObjectReadPermissions,
}}
>
@@ -3,10 +3,11 @@ import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordF
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { shouldCompactRecordIndexLabelIdentifierComponentState } from '@/object-record/record-index/states/shouldCompactRecordIndexLabelIdentifierComponentState';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableUpdateContext } from '@/object-record/record-table/contexts/RecordTableUpdateContext';
import { isRecordTableCellsNonEditableComponentState } from '@/object-record/record-table/states/isRecordTableCellsNonEditableComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useContext, type ReactNode } from 'react';
@@ -24,6 +25,10 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
const { recordId, isRecordReadOnly, rowIndex } =
useRecordTableRowContextOrThrow();
const isRecordTableCellsNonEditable = useAtomComponentStateValue(
isRecordTableCellsNonEditableComponentState,
);
const { recordField } = useContext(RecordTableCellContext);
const { objectMetadataItem, onRecordIdentifierClick, triggerEvent } =
useRecordTableContextOrThrow();
@@ -39,7 +44,7 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
const hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
const updateRecord = useContext(RecordUpdateContext);
const updateRecord = useContext(RecordTableUpdateContext);
const fieldDefinition =
fieldDefinitionByFieldMetadataItemId[recordField.fieldMetadataItemId];
@@ -53,20 +58,22 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
value={{
recordId,
fieldDefinition,
useUpdateRecord: () => [updateRecord, {}],
useUpdateRecord: updateRecord ? () => [updateRecord, {}] : undefined,
isLabelIdentifier: true,
isLabelIdentifierCompact: shouldCompactRecordIndexLabelIdentifier,
displayedMaxRows: 1,
isRecordFieldReadOnly: isRecordFieldReadOnly({
isRecordReadOnly: isRecordReadOnly ?? false,
isSystemObject: objectMetadataItem.isSystem,
objectPermissions,
fieldMetadataItem: {
id: recordField.fieldMetadataItemId,
isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false,
isCustom: fieldDefinition.metadata.isCustom ?? false,
},
}),
isRecordFieldReadOnly:
isRecordTableCellsNonEditable ||
isRecordFieldReadOnly({
isRecordReadOnly: isRecordReadOnly ?? false,
isSystemObject: objectMetadataItem.isSystem,
objectPermissions,
fieldMetadataItem: {
id: recordField.fieldMetadataItemId,
isUIReadOnly: fieldDefinition.metadata.isUIReadOnly ?? false,
isCustom: fieldDefinition.metadata.isCustom ?? false,
},
}),
maxWidth: recordField.size,
onRecordChipClick: handleChipClick,
isForbidden: !hasObjectReadPermissions,
@@ -59,7 +59,7 @@ export const RecordTableCellHoveredPortalContent = () => {
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
const isFieldInputOnly = useIsFieldInputOnly();
const isFieldInputOnly = useIsFieldInputOnly() && !isReadOnly;
const showButton =
!isFieldInputOnly &&
@@ -1,5 +1,7 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableCellPortalContexts } from '@/object-record/record-table/record-table-cell/components/RecordTableCellPortalContexts';
import { type TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { getRecordTableCellId } from '@/object-record/record-table/utils/getRecordTableCellId';
import { createPortal } from 'react-dom';
import { isDefined } from 'twenty-shared/utils';
@@ -10,8 +12,16 @@ export const RecordTableCellPortalWrapper = ({
position: TableCellPosition;
children: React.ReactNode;
}) => {
const { recordTableId } = useRecordTableContextOrThrow();
const cellId = getRecordTableCellId(
recordTableId,
position.column,
position.row,
);
const tableCellAnchorElement = document.body.querySelector<HTMLAnchorElement>(
`#record-table-cell-${position.column}-${position.row}`,
`#${cellId}`,
);
if (!isDefined(tableCellAnchorElement)) {
@@ -1,4 +1,4 @@
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidthClassName';
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
import { styled } from '@linaria/react';
import { cx } from '@linaria/core';
@@ -8,13 +8,9 @@ const StyledPlaceholderDragAndDropCell = styled.div`
background-color: ${themeCssVariables.background.primary};
border-bottom: 1px solid ${themeCssVariables.background.primary};
height: ${RECORD_TABLE_ROW_HEIGHT}px;
left: 0;
min-width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
overflow: hidden;
position: sticky;
width: ${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH}px;
`;
export const RecordTableDragAndDropPlaceholderCell = ({
@@ -22,5 +18,12 @@ export const RecordTableDragAndDropPlaceholderCell = ({
}: {
className?: string;
}) => {
return <StyledPlaceholderDragAndDropCell className={cx(className)} />;
return (
<StyledPlaceholderDragAndDropCell
className={cx(
RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH_CLASS_NAME,
className,
)}
/>
);
};
@@ -1,3 +1,4 @@
import { MockedProvider } from '@apollo/client/testing/react';
import { act, renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
@@ -33,38 +34,42 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<JotaiProvider store={jotaiStore}>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordTableId}
>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
<MockedProvider>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordTableId}
>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
>
<RecordTableRowContextProvider
value={recordTableRowContextValue}
>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecordComponentInstanceContextsWrapper>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecordComponentInstanceContextsWrapper>
</MockedProvider>
</JotaiProvider>
);
};
@@ -1,3 +1,4 @@
import { MockedProvider } from '@apollo/client/testing/react';
import { act, renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
@@ -33,39 +34,43 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<JotaiProvider store={jotaiStore}>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordTableId}
>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
onRecordIdentifierClick={() => {}}
>
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
<MockedProvider>
<RecordComponentInstanceContextsWrapper
componentInstanceId={recordTableId}
>
<RecordTableComponentInstance recordTableId={recordTableId}>
<RecordTableContextProvider
recordTableId={recordTableId}
viewBarId="viewBarId"
objectNameSingular={CoreObjectNameSingular.Person}
onRecordIdentifierClick={() => {}}
>
<RecordTableRowContextProvider value={recordTableRowContextValue}>
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
<FieldContext.Provider
value={{
fieldDefinition: textfieldDefinition,
recordId: 'recordId',
isLabelIdentifier: false,
isRecordFieldReadOnly: false,
}}
>
<RecordTableRowContextProvider
value={recordTableRowContextValue}
>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
<RecordTableRowDraggableContextProvider
value={recordTableRowDraggableContextValue}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecordComponentInstanceContextsWrapper>
<RecordTableCellContext.Provider
value={{ ...recordTableCellContextValue }}
>
{children}
</RecordTableCellContext.Provider>
</RecordTableRowDraggableContextProvider>
</RecordTableRowContextProvider>
</FieldContext.Provider>
</RecordTableContextProvider>
</RecordTableComponentInstance>
</RecordComponentInstanceContextsWrapper>
</MockedProvider>
</JotaiProvider>
);
};
@@ -1,8 +1,10 @@
import { styled } from '@linaria/react';
import {
RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR,
RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR,
} from '@/object-record/record-table/components/RecordTableStyleWrapper';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnLastEmptyColumnWidthClassName';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
@@ -15,7 +17,10 @@ const StyledPlaceholderDragAndDropFooterCell = styled.div`
bottom: 0;
left: 0px;
position: sticky;
width: ${`${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`};
width: calc(
var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR}) +
var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR})
);
z-index: ${TABLE_Z_INDEX.footer.stickyColumn};
`;
@@ -2,21 +2,29 @@ import { styled } from '@linaria/react';
import { useContext } from 'react';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
import { RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnDragAndDropWidth';
import {
RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR,
RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR,
} from '@/object-record/record-table/components/RecordTableStyleWrapper';
import { RECORD_TABLE_LABEL_IDENTIFIER_COLUMN_WIDTH_ON_MOBILE } from '@/object-record/record-table/constants/RecordTableLabelIdentifierColumnWidthOnMobile';
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue';
import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown';
import { hasAggregateOperationForViewFieldFamilySelector } from '@/object-record/record-table/record-table-footer/states/hasAggregateOperationForViewFieldFamilySelector';
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
import { getRecordTableColumnFieldWidthClassName } from '@/object-record/record-table/utils/getRecordTableColumnFieldWidthClassName';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { cx } from '@linaria/core';
import { findByProperty, isDefined } from 'twenty-shared/utils';
const StyledColumnFooterCell = styled.div<{
columnWidth: number;
isFirstCell: boolean;
isReadOnly: boolean;
}>`
background-color: ${themeCssVariables.background.primary};
border-right: solid 1px ${themeCssVariables.background.primary};
@@ -25,16 +33,23 @@ const StyledColumnFooterCell = styled.div<{
color: ${themeCssVariables.font.color.tertiary};
cursor: ${({ isReadOnly }) => (isReadOnly ? 'default' : 'pointer')};
height: ${RECORD_TABLE_ROW_HEIGHT}px;
left: ${({ isFirstCell }) =>
isFirstCell
? `${RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH + RECORD_TABLE_COLUMN_CHECKBOX_WIDTH}px`
? `calc(var(${RECORD_TABLE_DRAG_DROP_WIDTH_CSS_VAR}) + var(${RECORD_TABLE_CHECKBOX_WIDTH_CSS_VAR}))`
: 'auto'};
min-width: ${({ columnWidth }) => columnWidth}px;
&:hover {
background: ${themeCssVariables.background.secondary};
background: ${({ isReadOnly }) =>
isReadOnly
? themeCssVariables.background.primary
: themeCssVariables.background.secondary};
}
min-width: ${({ columnWidth }) => columnWidth}px;
overflow: hidden;
padding: 0;
@@ -80,10 +95,21 @@ export const RecordTableAggregateFooterCell = ({
}) => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const { fieldMetadataId } = useContext(
const { fieldMetadataId, viewFieldId } = useContext(
RecordTableColumnAggregateFooterCellContext,
);
const hasAggregateOperationForViewField = useAtomFamilySelectorValue(
hasAggregateOperationForViewFieldFamilySelector,
{ viewFieldId },
);
const isRecordTableColumnResizable = useAtomComponentStateValue(
isRecordTableColumnResizableComponentState,
);
const isFooterReadOnly = !isRecordTableColumnResizable;
const recordField = visibleRecordFields.find(
findByProperty('fieldMetadataItemId', fieldMetadataId),
);
@@ -98,16 +124,26 @@ export const RecordTableAggregateFooterCell = ({
<StyledColumnFooterCell
columnWidth={recordField.size + 1}
isFirstCell={isFirstCell}
isReadOnly={isFooterReadOnly}
className={cx(
'footer-cell',
getRecordTableColumnFieldWidthClassName(columnIndex),
)}
>
<StyledColumnFootContainer>
<RecordTableColumnFooterWithDropdown
currentRecordGroupId={currentRecordGroupId}
isFirstCell={isFirstCell}
/>
{isFooterReadOnly ? (
hasAggregateOperationForViewField ? (
<RecordTableColumnAggregateFooterValue
fieldMetadataId={fieldMetadataId}
dropdownId={`${fieldMetadataId}-footer-readonly`}
/>
) : null
) : (
<RecordTableColumnFooterWithDropdown
currentRecordGroupId={currentRecordGroupId}
isFirstCell={isFirstCell}
/>
)}
</StyledColumnFootContainer>
</StyledColumnFooterCell>
);
@@ -5,10 +5,15 @@ import { RecordTableHeaderAddColumnButton } from '@/object-record/record-table/r
import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell';
import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn';
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
import { RecordTableHeaderEmptyLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderEmptyLastColumn';
import { RecordTableHeaderFirstCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstCell';
import { RecordTableHeaderFirstScrollableCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderFirstScrollableCell';
import { RecordTableHeaderLastEmptyColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastEmptyColumn';
import { useResizeTableHeader } from '@/object-record/record-table/record-table-header/hooks/useResizeTableHeader';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { filterOutByProperty } from 'twenty-shared/utils';
@@ -24,6 +29,18 @@ export const RecordTableHeader = () => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const { labelIdentifierFieldMetadataItem } = useRecordIndexContextOrThrow();
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
isRecordTableColumnHeadersReadOnlyComponentState,
);
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
const recordFieldsWithoutLabelIdentifierAndFirstOne = visibleRecordFields
.filter(
filterOutByProperty(
@@ -37,8 +54,10 @@ export const RecordTableHeader = () => {
return (
<StyledHeaderContainer>
<RecordTableHeaderDragDropColumn />
<RecordTableHeaderCheckboxColumn />
{!isRecordTableDragColumnHidden && <RecordTableHeaderDragDropColumn />}
{!isRecordTableCheckboxColumnHidden && (
<RecordTableHeaderCheckboxColumn />
)}
<RecordTableHeaderFirstCell />
<RecordTableHeaderFirstScrollableCell />
{recordFieldsWithoutLabelIdentifierAndFirstOne.map(
@@ -50,7 +69,11 @@ export const RecordTableHeader = () => {
/>
),
)}
<RecordTableHeaderAddColumnButton />
{isRecordTableColumnHeadersReadOnly ? (
<RecordTableHeaderEmptyLastColumn />
) : (
<RecordTableHeaderAddColumnButton />
)}
<RecordTableHeaderLastEmptyColumn />
</StyledHeaderContainer>
);
@@ -30,7 +30,6 @@ const StyledPlusIconHeaderCell = styled.div<{
? `1px solid ${themeCssVariables.border.color.light}`
: 'none'};
border-right: ${themeCssVariables.border.color.light} !important;
color: ${themeCssVariables.font.color.tertiary};
cursor: pointer;
@@ -2,9 +2,12 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnHead } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHead';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
@@ -28,6 +31,14 @@ export const RecordTableHeaderCell = ({
}: RecordTableHeaderCellProps) => {
const { objectMetadataItem } = useRecordTableContextOrThrow();
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
isRecordTableColumnHeadersReadOnlyComponentState,
);
const isRecordTableColumnResizable = useAtomComponentStateValue(
isRecordTableColumnResizableComponentState,
);
const isRecordTableRowActive = useAtomComponentFamilyStateValue(
isRecordTableRowActiveComponentFamilyState,
0,
@@ -74,19 +85,28 @@ export const RecordTableHeaderCell = ({
key={recordField.fieldMetadataItemId}
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
isResizing={isResizingAnyColumn}
isReadOnly={isRecordTableColumnHeadersReadOnly}
>
<RecordTableHeaderResizeHandler
recordFieldIndex={recordFieldIndex}
position="left"
/>
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
<RecordTableHeaderResizeHandler
recordFieldIndex={recordFieldIndex}
position="right"
/>
{isRecordTableColumnResizable && (
<RecordTableHeaderResizeHandler
recordFieldIndex={recordFieldIndex}
position="left"
/>
)}
{isRecordTableColumnHeadersReadOnly ? (
<RecordTableColumnHead recordField={recordField} />
) : (
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
)}
{isRecordTableColumnResizable && (
<RecordTableHeaderResizeHandler
recordFieldIndex={recordFieldIndex}
position="right"
/>
)}
</RecordTableHeaderCellContainer>
);
};
@@ -6,6 +6,7 @@ const StyledHeaderCell = styled.div<{
zIndex?: number;
shouldDisplayBorderBottom: boolean;
isResizing: boolean;
isReadOnly: boolean;
}>`
background-color: ${themeCssVariables.background.primary};
border-bottom: ${({ shouldDisplayBorderBottom }) =>
@@ -16,7 +17,8 @@ const StyledHeaderCell = styled.div<{
color: ${themeCssVariables.font.color.tertiary};
cursor: ${({ isResizing }) => (isResizing ? 'col-resize' : 'pointer')};
cursor: ${({ isResizing, isReadOnly }) =>
isReadOnly ? 'default' : isResizing ? 'col-resize' : 'pointer'};
height: ${RECORD_TABLE_ROW_HEIGHT}px;
max-height: ${RECORD_TABLE_ROW_HEIGHT}px;
@@ -27,15 +29,15 @@ const StyledHeaderCell = styled.div<{
text-align: left;
&:hover {
background: ${({ isResizing }) =>
isResizing
background: ${({ isResizing, isReadOnly }) =>
isReadOnly || isResizing
? themeCssVariables.background.primary
: themeCssVariables.background.secondary};
}
&:active {
background: ${({ isResizing }) =>
isResizing
background: ${({ isResizing, isReadOnly }) =>
isReadOnly || isResizing
? themeCssVariables.background.primary
: themeCssVariables.background.tertiary};
}
@@ -3,7 +3,6 @@ import { styled } from '@linaria/react';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidth';
import { RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnCheckboxWidthClassName';
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
@@ -11,26 +10,16 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { useSelectAllRows } from '@/object-record/record-table/hooks/internal/useSelectAllRows';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { cx } from '@linaria/core';
import { Checkbox } from 'twenty-ui/input';
const StyledContainer = styled.div<{
shouldDisplayBorderBottom: boolean;
}>`
const StyledContainer = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.primary};
border-bottom: ${({ shouldDisplayBorderBottom }) =>
shouldDisplayBorderBottom
? `1px solid ${themeCssVariables.border.color.light}`
: 'none'};
border-bottom: 1px solid ${themeCssVariables.border.color.light};
display: flex;
height: ${RECORD_TABLE_ROW_HEIGHT}px;
justify-content: center;
@@ -41,7 +30,6 @@ const StyledContainer = styled.div<{
const StyledColumnHeaderCell = styled.div`
background-color: ${themeCssVariables.background.primary};
box-sizing: border-box;
cursor: pointer;
max-height: ${RECORD_TABLE_ROW_HEIGHT}px;
@@ -85,37 +73,6 @@ export const RecordTableHeaderCheckboxColumn = () => {
}
};
const isRecordTableRowActive = useAtomComponentFamilyStateValue(
isRecordTableRowActiveComponentFamilyState,
0,
);
const isRecordTableRowFocused = useAtomComponentFamilyStateValue(
isRecordTableRowFocusedComponentFamilyState,
0,
);
const isRecordTableRowFocusActive = useAtomComponentStateValue(
isRecordTableRowFocusActiveComponentState,
);
const isFirstRowActiveOrFocused =
isRecordTableRowActive ||
(isRecordTableRowFocused && isRecordTableRowFocusActive);
const isRecordTableScrolledVertically = useAtomComponentStateValue(
isRecordTableScrolledVerticallyComponentState,
);
const hasRecordGroups = useAtomComponentSelectorValue(
hasRecordGroupsComponentSelector,
);
const shouldDisplayBorderBottom =
hasRecordGroups ||
!isFirstRowActiveOrFocused ||
isRecordTableScrolledVertically;
return (
<StyledColumnHeaderCell
className={cx(
@@ -123,10 +80,7 @@ export const RecordTableHeaderCheckboxColumn = () => {
RECORD_TABLE_COLUMN_CHECKBOX_WIDTH_CLASS_NAME,
)}
>
<StyledContainer
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
data-select-disable
>
<StyledContainer data-select-disable>
<Checkbox
hoverable
checked={checked}
@@ -0,0 +1,71 @@
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidth';
import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnAddColumnButtonWidthClassName';
import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/RecordTableRowHeight';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { cx } from '@linaria/core';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledEmptyHeaderCell = styled.div<{
shouldDisplayBorderBottom: boolean;
}>`
background-color: ${themeCssVariables.background.primary};
border-bottom: ${({ shouldDisplayBorderBottom }) =>
shouldDisplayBorderBottom
? `1px solid ${themeCssVariables.border.color.light}`
: 'none'};
height: ${RECORD_TABLE_ROW_HEIGHT}px;
max-height: ${RECORD_TABLE_ROW_HEIGHT}px;
width: ${RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH}px;
z-index: 1;
`;
export const RecordTableHeaderEmptyLastColumn = () => {
const isRecordTableRowActive = useAtomComponentFamilyStateValue(
isRecordTableRowActiveComponentFamilyState,
0,
);
const isRecordTableRowFocused = useAtomComponentFamilyStateValue(
isRecordTableRowFocusedComponentFamilyState,
0,
);
const isRecordTableRowFocusActive = useAtomComponentStateValue(
isRecordTableRowFocusActiveComponentState,
);
const isFirstRowActiveOrFocused =
isRecordTableRowActive ||
(isRecordTableRowFocused && isRecordTableRowFocusActive);
const isRecordTableScrolledVertically = useAtomComponentStateValue(
isRecordTableScrolledVerticallyComponentState,
);
const hasRecordGroups = useAtomComponentSelectorValue(
hasRecordGroupsComponentSelector,
);
const shouldDisplayBorderBottom =
hasRecordGroups ||
!isFirstRowActiveOrFocused ||
isRecordTableScrolledVertically;
return (
<StyledEmptyHeaderCell
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
className={cx(
'header-cell',
RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME,
)}
/>
);
};
@@ -1,6 +1,7 @@
import { styled } from '@linaria/react';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnHead } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHead';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
@@ -10,6 +11,8 @@ import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/s
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { RecordTableHeaderLabelIdentifierCellPlusButton } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLabelIdentifierCellPlusButton';
import { getVisibleFieldWithLowestPosition } from '@/object-record/record-table/record-table-header/utils/getVisibleFieldWithLowestPosition';
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
@@ -34,6 +37,14 @@ export const RecordTableHeaderFirstCell = () => {
const { objectMetadataItem, visibleRecordFields } =
useRecordTableContextOrThrow();
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
isRecordTableColumnHeadersReadOnlyComponentState,
);
const isRecordTableColumnResizable = useAtomComponentStateValue(
isRecordTableColumnResizableComponentState,
);
const [iconIsVisible, setIconIsVisible] = useState(false);
const isRecordTableRowActive = useAtomComponentFamilyStateValue(
@@ -87,18 +98,25 @@ export const RecordTableHeaderFirstCell = () => {
onMouseLeave={() => setIconIsVisible(false)}
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
isResizing={isResizingAnyColumn}
isReadOnly={isRecordTableColumnHeadersReadOnly}
>
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
{iconIsVisible && (
{isRecordTableColumnHeadersReadOnly ? (
<RecordTableColumnHead recordField={recordField} />
) : (
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
)}
{iconIsVisible && !isRecordTableColumnHeadersReadOnly && (
<StyledPlusButtonWrapper>
<RecordTableHeaderLabelIdentifierCellPlusButton />
</StyledPlusButtonWrapper>
)}
<RecordTableHeaderResizeHandler recordFieldIndex={0} position="right" />
{isRecordTableColumnResizable && (
<RecordTableHeaderResizeHandler recordFieldIndex={0} position="right" />
)}
</RecordTableHeaderCellContainer>
);
};
@@ -2,12 +2,15 @@ import { type RecordField } from '@/object-record/record-field/types/RecordField
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { TABLE_Z_INDEX } from '@/object-record/record-table/constants/TableZIndex';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableColumnHead } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHead';
import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown';
import { RecordTableHeaderResizeHandler } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderResizeHandler';
import { RecordTableHeaderCellContainer } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCellContainer';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { isRecordTableColumnHeadersReadOnlyComponentState } from '@/object-record/record-table/states/isRecordTableColumnHeadersReadOnlyComponentState';
import { isRecordTableColumnResizableComponentState } from '@/object-record/record-table/states/isRecordTableColumnResizableComponentState';
import { isRecordTableRowActiveComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowActiveComponentFamilyState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
@@ -24,6 +27,14 @@ export const RecordTableHeaderFirstScrollableCell = () => {
const { objectMetadataItem, visibleRecordFields } =
useRecordTableContextOrThrow();
const isRecordTableColumnHeadersReadOnly = useAtomComponentStateValue(
isRecordTableColumnHeadersReadOnlyComponentState,
);
const isRecordTableColumnResizable = useAtomComponentStateValue(
isRecordTableColumnResizableComponentState,
);
const isRecordTableRowActive = useAtomComponentFamilyStateValue(
isRecordTableRowActiveComponentFamilyState,
0,
@@ -81,13 +92,22 @@ export const RecordTableHeaderFirstScrollableCell = () => {
shouldDisplayBorderBottom={shouldDisplayBorderBottom}
zIndex={TABLE_Z_INDEX.headerColumns.headerColumnsNormal}
isResizing={isResizingAnyColumn}
isReadOnly={isRecordTableColumnHeadersReadOnly}
>
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="left" />
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="right" />
{isRecordTableColumnResizable && (
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="left" />
)}
{isRecordTableColumnHeadersReadOnly ? (
<RecordTableColumnHead recordField={recordField} />
) : (
<RecordTableColumnHeadWithDropdown
recordField={recordField}
objectMetadataId={objectMetadataItem.id}
/>
)}
{isRecordTableColumnResizable && (
<RecordTableHeaderResizeHandler recordFieldIndex={1} position="right" />
)}
</RecordTableHeaderCellContainer>
);
};
@@ -7,6 +7,8 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { resizedFieldMetadataIdComponentState } from '@/object-record/record-table/states/resizedFieldMetadataIdComponentState';
import { resizeFieldOffsetComponentState } from '@/object-record/record-table/states/resizeFieldOffsetComponentState';
import { shouldCompactRecordTableFirstColumnComponentState } from '@/object-record/record-table/states/shouldCompactRecordTableFirstColumnComponentState';
@@ -69,6 +71,16 @@ export const useResizeTableHeader = () => {
recordTableId,
);
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
recordTableId,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
recordTableId,
);
const handleResizeHandlerStart = useCallback<PointerEventListener>(
({ x }) => {
resetTableRowSelection();
@@ -98,6 +110,7 @@ export const useResizeTableHeader = () => {
);
updateRecordTableCSSVariable(
recordTableId,
getRecordTableColumnFieldWidthCSSVariableName(recordFieldIndex),
`${newWidth}px`,
);
@@ -106,11 +119,14 @@ export const useResizeTableHeader = () => {
recordFields: visibleRecordFields,
shouldCompactFirstColumn: shouldCompactRecordTableFirstColumn,
tableWidth: recordTableWidth,
isDragColumnHidden: isRecordTableDragColumnHidden,
isCheckboxColumnHidden: isRecordTableCheckboxColumnHidden,
});
const newLastColumnWidth = lastColumnWidth - newResizeOffset;
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME,
`${newLastColumnWidth}px`,
);
@@ -121,6 +137,7 @@ export const useResizeTableHeader = () => {
);
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_COLUMN_WITH_GROUP_LAST_EMPTY_COLUMN_WIDTH_VARIABLE_NAME,
`${newGroupSectionLastColumnWidth}px`,
);
@@ -130,9 +147,12 @@ export const useResizeTableHeader = () => {
[
initialPointerPositionX,
recordField,
recordTableId,
visibleRecordFields,
shouldCompactRecordTableFirstColumn,
recordTableWidth,
isRecordTableDragColumnHidden,
isRecordTableCheckboxColumnHidden,
setResizeFieldOffset,
],
);
@@ -1,16 +1,7 @@
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { RecordTableRowCells } from '@/object-record/record-table/record-table-row/components/RecordTableRowCells';
import { RecordTableStaticTr } from '@/object-record/record-table/record-table-row/components/RecordTableStaticTr';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
type RecordTableRowProps = {
@@ -24,17 +15,17 @@ export const RecordTableRow = ({
rowIndexForFocus,
rowIndexForDrag,
}: RecordTableRowProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
const isRecordTableRowFocused = useAtomComponentFamilyStateValue(
isRecordTableRowFocusedComponentFamilyState,
rowIndexForFocus,
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableRowFocusActive = useAtomComponentStateValue(
isRecordTableRowFocusActiveComponentState,
recordTableId,
);
if (isRecordTableDragColumnHidden) {
return (
<RecordTableStaticTr recordId={recordId} focusIndex={rowIndexForFocus}>
<RecordTableRowCells rowIndexForFocus={rowIndexForFocus} />
</RecordTableStaticTr>
);
}
return (
<RecordTableDraggableTr
@@ -42,17 +33,7 @@ export const RecordTableRow = ({
draggableIndex={rowIndexForDrag}
focusIndex={rowIndexForFocus}
>
{isRecordTableRowFocusActive && isRecordTableRowFocused && (
<>
<RecordTableRowHotkeyEffect />
<RecordTableRowArrowKeysEffect />
</>
)}
<RecordTableCellDragAndDrop />
<RecordTableCellCheckbox />
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
<RecordTableRowCells rowIndexForFocus={rowIndexForFocus} />
</RecordTableDraggableTr>
);
};
@@ -0,0 +1,58 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
type RecordTableRowCellsProps = {
rowIndexForFocus: number;
};
export const RecordTableRowCells = ({
rowIndexForFocus,
}: RecordTableRowCellsProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
const isRecordTableRowFocused = useAtomComponentFamilyStateValue(
isRecordTableRowFocusedComponentFamilyState,
rowIndexForFocus,
);
const isRecordTableRowFocusActive = useAtomComponentStateValue(
isRecordTableRowFocusActiveComponentState,
recordTableId,
);
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
return (
<>
{isRecordTableRowFocusActive && isRecordTableRowFocused && (
<>
<RecordTableRowHotkeyEffect />
<RecordTableRowArrowKeysEffect />
</>
)}
{!isRecordTableDragColumnHidden && <RecordTableCellDragAndDrop />}
{!isRecordTableCheckboxColumnHidden && <RecordTableCellCheckbox />}
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
</>
);
};
@@ -0,0 +1,35 @@
import { type ReactNode } from 'react';
import { RecordTableRowDraggableContextProvider } from '@/object-record/record-table/contexts/RecordTableRowDraggableContext';
import { RecordTableTr } from '@/object-record/record-table/record-table-row/components/RecordTableTr';
type RecordTableStaticTrProps = {
recordId: string;
focusIndex: number;
children: ReactNode;
};
export const RecordTableStaticTr = ({
recordId,
focusIndex,
children,
}: RecordTableStaticTrProps) => {
return (
<RecordTableTr
recordId={recordId}
focusIndex={focusIndex}
isDragging={false}
data-testid={`row-id-${recordId}`}
data-selectable-id={recordId}
>
<RecordTableRowDraggableContextProvider
value={{
isDragging: false,
dragHandleProps: null,
}}
>
{children}
</RecordTableRowDraggableContextProvider>
</RecordTableTr>
);
};
@@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const isRecordTableCellsNonEditableComponentState =
createAtomComponentState<boolean>({
key: 'isRecordTableCellsNonEditableComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const isRecordTableCheckboxColumnHiddenComponentState =
createAtomComponentState<boolean>({
key: 'isRecordTableCheckboxColumnHiddenComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const isRecordTableColumnHeadersReadOnlyComponentState =
createAtomComponentState<boolean>({
key: 'isRecordTableColumnHeadersReadOnlyComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const isRecordTableColumnResizableComponentState =
createAtomComponentState<boolean>({
key: 'isRecordTableColumnResizableComponentState',
defaultValue: true,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -0,0 +1,9 @@
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
export const isRecordTableDragColumnHiddenComponentState =
createAtomComponentState<boolean>({
key: 'isRecordTableDragColumnHiddenComponentState',
defaultValue: false,
componentInstanceContext: RecordTableComponentInstanceContext,
});
@@ -0,0 +1,70 @@
import { computeLastRecordTableColumnWidth } from '@/object-record/record-table/utils/computeLastRecordTableColumnWidth';
describe('computeLastRecordTableColumnWidth', () => {
it('returns 0 when the table is narrower than the total content width', () => {
const recordFields = [{ size: 100 }, { size: 100 }];
const { lastColumnWidth } = computeLastRecordTableColumnWidth({
recordFields,
tableWidth: 200,
shouldCompactFirstColumn: false,
isDragColumnHidden: false,
isCheckboxColumnHidden: false,
});
expect(lastColumnWidth).toBe(0);
});
it('returns the remaining positive width when the table is wider than the total content width', () => {
const recordFields = [{ size: 100 }, { size: 100 }];
const { lastColumnWidth } = computeLastRecordTableColumnWidth({
recordFields,
tableWidth: 300,
shouldCompactFirstColumn: false,
isDragColumnHidden: false,
isCheckboxColumnHidden: false,
});
expect(lastColumnWidth).toBe(25);
});
it('excludes drag-and-drop and checkbox widths from the calculation when both columns are hidden', () => {
const recordFields = [{ size: 100 }, { size: 100 }];
const { lastColumnWidth } = computeLastRecordTableColumnWidth({
recordFields,
tableWidth: 300,
shouldCompactFirstColumn: false,
isDragColumnHidden: true,
isCheckboxColumnHidden: true,
});
expect(lastColumnWidth).toBe(65);
});
it('uses the compact first column width when shouldCompactFirstColumn is true', () => {
const recordFields = [{ size: 100 }, { size: 150 }];
const { lastColumnWidth: lastColumnWidthWithCompact } =
computeLastRecordTableColumnWidth({
recordFields,
tableWidth: 300,
shouldCompactFirstColumn: true,
isDragColumnHidden: false,
isCheckboxColumnHidden: false,
});
const { lastColumnWidth: lastColumnWidthWithoutCompact } =
computeLastRecordTableColumnWidth({
recordFields,
tableWidth: 300,
shouldCompactFirstColumn: false,
isDragColumnHidden: false,
isCheckboxColumnHidden: false,
});
expect(lastColumnWidthWithCompact).toBe(37);
expect(lastColumnWidthWithoutCompact).toBe(0);
});
});
@@ -0,0 +1,15 @@
import { getRecordTableCellId } from '@/object-record/record-table/utils/getRecordTableCellId';
describe('getRecordTableCellId', () => {
it('returns the correct cell id format', () => {
expect(getRecordTableCellId('my-table', 2, 5)).toBe(
'record-table-cell-my-table-2-5',
);
});
it('returns the correct cell id format with different values', () => {
expect(getRecordTableCellId('other-table', 0, 0)).toBe(
'record-table-cell-other-table-0-0',
);
});
});
@@ -0,0 +1,13 @@
import { getRecordTableHtmlId } from '@/object-record/record-table/utils/getRecordTableHtmlId';
describe('getRecordTableHtmlId', () => {
it('returns the correct html id format', () => {
expect(getRecordTableHtmlId('my-table')).toBe('record-table-my-table');
});
it('returns the correct html id format with a different id', () => {
expect(getRecordTableHtmlId('other-table')).toBe(
'record-table-other-table',
);
});
});
@@ -8,10 +8,14 @@ export const computeLastRecordTableColumnWidth = ({
recordFields,
tableWidth,
shouldCompactFirstColumn,
isDragColumnHidden,
isCheckboxColumnHidden,
}: {
recordFields: Pick<RecordField, 'size'>[];
tableWidth: number;
shouldCompactFirstColumn: boolean;
isDragColumnHidden?: boolean;
isCheckboxColumnHidden?: boolean;
}) => {
const { visibleRecordFieldsWidth } = computeVisibleRecordFieldsWidthOnTable({
shouldCompactFirstColumn,
@@ -20,9 +24,18 @@ export const computeLastRecordTableColumnWidth = ({
const widthOfBorders = recordFields.length + 1;
const dragColumnWidth = isDragColumnHidden
? 0
: RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH;
const checkboxColumnWidth = isCheckboxColumnHidden
? 0
: RECORD_TABLE_COLUMN_CHECKBOX_WIDTH;
const leftColumnsWidth = dragColumnWidth + checkboxColumnWidth;
const fixedColumnsWidth =
RECORD_TABLE_COLUMN_DRAG_AND_DROP_WIDTH +
RECORD_TABLE_COLUMN_CHECKBOX_WIDTH +
leftColumnsWidth +
RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH +
widthOfBorders;
@@ -0,0 +1,7 @@
export const getRecordTableCellId = (
recordTableId: string,
column: number,
row: number,
): string => {
return `record-table-cell-${recordTableId}-${column}-${row}`;
};
@@ -0,0 +1,3 @@
export const getRecordTableHtmlId = (recordTableId: string): string => {
return `record-table-${recordTableId}`;
};
@@ -1,10 +1,11 @@
import { RECORD_TABLE_HTML_ID } from '@/object-record/record-table/constants/RecordTableHtmlId';
import { getRecordTableHtmlId } from '@/object-record/record-table/utils/getRecordTableHtmlId';
export const updateRecordTableCSSVariable = (
recordTableId: string,
cssVariableName: string,
newValue: string,
) => {
document
.querySelector<HTMLDivElement>(`#${RECORD_TABLE_HTML_ID}`)
.querySelector<HTMLDivElement>(`#${getRecordTableHtmlId(recordTableId)}`)
?.style.setProperty(cssVariableName, newValue);
};
@@ -1,19 +1,10 @@
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableCellCheckbox } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
import { RecordTableCellDragAndDrop } from '@/object-record/record-table/record-table-cell/components/RecordTableCellDragAndDrop';
import { RecordTableLastEmptyCell } from '@/object-record/record-table/record-table-cell/components/RecordTableLastEmptyCell';
import { RecordTablePlusButtonCellPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTablePlusButtonCellPlaceholder';
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
import { RecordTableFieldsCells } from '@/object-record/record-table/record-table-row/components/RecordTableFieldsCells';
import { RecordTableRowArrowKeysEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowArrowKeysEffect';
import { RecordTableRowHotkeyEffect } from '@/object-record/record-table/record-table-row/components/RecordTableRowHotkeyEffect';
import { isRecordTableRowFocusActiveComponentState } from '@/object-record/record-table/states/isRecordTableRowFocusActiveComponentState';
import { isRecordTableRowFocusedComponentFamilyState } from '@/object-record/record-table/states/isRecordTableRowFocusedComponentFamilyState';
import { RecordTableRowCells } from '@/object-record/record-table/record-table-row/components/RecordTableRowCells';
import { RecordTableStaticTr } from '@/object-record/record-table/record-table-row/components/RecordTableStaticTr';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { RecordTableRowVirtualizedSkeleton } from '@/object-record/record-table/virtualization/components/RecordTableRowVirtualizedSkeleton';
import { recordIdByRealIndexComponentFamilySelector } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentFamilySelector';
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { isDefined } from 'twenty-shared/utils';
@@ -21,20 +12,11 @@ type RecordTableRowVirtualizedFullDataProps = {
realIndex: number;
};
// TODO: Full Data will take its full meaning when we'll have different levels of data : with relations, only identifiers, etc.
export const RecordTableRowVirtualizedFullData = ({
realIndex,
}: RecordTableRowVirtualizedFullDataProps) => {
const { recordTableId } = useRecordTableContextOrThrow();
const isRecordTableRowFocused = useAtomComponentFamilyStateValue(
isRecordTableRowFocusedComponentFamilyState,
realIndex,
);
const isRecordTableRowFocusActive = useAtomComponentStateValue(
isRecordTableRowFocusActiveComponentState,
recordTableId,
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const recordId = useAtomComponentFamilySelectorValue(
@@ -46,23 +28,21 @@ export const RecordTableRowVirtualizedFullData = ({
return <RecordTableRowVirtualizedSkeleton />;
}
if (isRecordTableDragColumnHidden) {
return (
<RecordTableStaticTr recordId={recordId} focusIndex={realIndex}>
<RecordTableRowCells rowIndexForFocus={realIndex} />
</RecordTableStaticTr>
);
}
return (
<RecordTableDraggableTr
recordId={recordId}
draggableIndex={realIndex}
focusIndex={realIndex}
>
<RecordTableCellDragAndDrop />
<RecordTableCellCheckbox />
<RecordTableFieldsCells />
<RecordTablePlusButtonCellPlaceholder />
<RecordTableLastEmptyCell />
{isRecordTableRowFocusActive && isRecordTableRowFocused && (
<>
<RecordTableRowHotkeyEffect />
<RecordTableRowArrowKeysEffect />
</>
)}
<RecordTableRowCells rowIndexForFocus={realIndex} />
</RecordTableDraggableTr>
);
};
@@ -2,20 +2,35 @@ import { RECORD_TABLE_COLUMN_ADD_COLUMN_BUTTON_WIDTH_CLASS_NAME } from '@/object
import { RECORD_TABLE_COLUMN_LAST_EMPTY_COLUMN_WIDTH_CLASS_NAME } from '@/object-record/record-table/constants/RecordTableColumnLastEmptyColumnWidthClassName';
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRecordTableCheckboxColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableCheckboxColumnHiddenComponentState';
import { isRecordTableDragColumnHiddenComponentState } from '@/object-record/record-table/states/isRecordTableDragColumnHiddenComponentState';
import { RecordTableCellCheckboxPlaceholder } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckboxPlaceholder';
import { RecordTableCellLoading } from '@/object-record/record-table/record-table-cell/components/RecordTableCellLoading';
import { RecordTableCellStyleWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellStyleWrapper';
import { RecordTableDragAndDropPlaceholderCell } from '@/object-record/record-table/record-table-cell/components/RecordTableDragAndDropPlaceholderCell';
import { RecordTableRowDiv } from '@/object-record/record-table/record-table-row/components/RecordTableRowDiv';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
export const RecordTableRowVirtualizedSkeleton = () => {
const { visibleRecordFields } = useRecordTableContextOrThrow();
const { hasUserSelectedAllRows } = useRecordTableBodyContextOrThrow();
const isRecordTableDragColumnHidden = useAtomComponentStateValue(
isRecordTableDragColumnHiddenComponentState,
);
const isRecordTableCheckboxColumnHidden = useAtomComponentStateValue(
isRecordTableCheckboxColumnHiddenComponentState,
);
return (
<RecordTableRowDiv isDragging={false}>
<RecordTableDragAndDropPlaceholderCell />
<RecordTableCellCheckboxPlaceholder />
{!isRecordTableDragColumnHidden && (
<RecordTableDragAndDropPlaceholderCell />
)}
{!isRecordTableCheckboxColumnHidden && (
<RecordTableCellCheckboxPlaceholder />
)}
{visibleRecordFields.map((recordField, index) => (
<RecordTableCellLoading
key={recordField.fieldMetadataItemId}
@@ -127,11 +127,13 @@ export const useTriggerInitialRecordTableDataLoad = () => {
resetVirtualizedRowTreadmill();
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
'hidden',
);
updateRecordTableCSSVariable(
recordTableId,
RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
'hidden',
);
@@ -226,6 +228,7 @@ export const useTriggerInitialRecordTableDataLoad = () => {
upsertRecordsInStore,
loadRecordsToVirtualRows,
reapplyRowSelection,
recordTableId,
],
);
@@ -127,16 +127,6 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
tabListInstanceId,
});
const handleLayoutChangeWithoutPendingPlaceholder = (
currentLayout: Layout[],
allLayouts: Layouts,
) => {
handleLayoutChange(
currentLayout,
filterPendingPlaceholderFromLayouts(allLayouts),
);
};
const gridContainerRef = useRef<HTMLDivElement>(null);
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
@@ -161,6 +151,16 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
const hasPendingPlaceholder = isDefined(pageLayoutDraggedArea);
const handleLayoutChangeWithoutPendingPlaceholder = (
currentLayout: Layout[],
allLayouts: Layouts,
) => {
handleLayoutChange(
currentLayout,
filterPendingPlaceholderFromLayouts(allLayouts),
);
};
const baseLayouts = isLayoutEmpty
? EMPTY_LAYOUT
: (pageLayoutCurrentLayouts[tabId] ?? EMPTY_LAYOUT);
@@ -14,4 +14,8 @@ export const WIDGET_SIZES: Partial<Record<WidgetType, WidgetSizeConfig>> = {
default: { w: 8, h: 8 },
minimum: { w: 4, h: 4 },
},
[WidgetType.RECORD_TABLE]: {
default: { w: 6, h: 6 },
minimum: { w: 4, h: 4 },
},
};
@@ -188,6 +188,10 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
... on ViewConfiguration {
configurationType
}
... on RecordTableConfiguration {
configurationType
viewId
}
... on WorkflowConfiguration {
configurationType
}
@@ -8,6 +8,24 @@ import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pa
import { createStore } from 'jotai';
import { type ReactNode } from 'react';
jest.mock(
'@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget',
() => ({
useDeleteViewForRecordTableWidget: () => ({
deleteViewForRecordTableWidget: jest.fn(),
}),
}),
);
jest.mock('@/side-panel/hooks/useSidePanelMenu', () => ({
useSidePanelMenu: () => ({
closeSidePanelMenu: jest.fn(),
openSidePanelMenu: jest.fn(),
navigateSidePanelMenu: jest.fn(),
toggleSidePanelMenu: jest.fn(),
}),
}));
describe('useDeletePageLayoutWidget', () => {
const getWrapper =
(store = createStore()) =>
@@ -22,6 +22,10 @@ export const useChangePageLayoutDragSelection = (
const changePageLayoutDragSelection = useCallback(
(cellId: string, selected: boolean) => {
if (!cellId.startsWith('cell-')) {
return;
}
store.set(pageLayoutSelectedCellsState, (prev) => {
const newSet = new Set(prev);
if (selected) {
@@ -0,0 +1,120 @@
import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes';
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
import { createDefaultRecordTableWidget } from '@/page-layout/utils/createDefaultRecordTableWidget';
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { v4 as uuidv4 } from 'uuid';
import { WidgetType } from '~/generated-metadata/graphql';
export const useCreatePageLayoutRecordTableWidget = (
pageLayoutIdFromProps?: string,
) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
PageLayoutComponentInstanceContext,
pageLayoutIdFromProps,
);
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
getTabListInstanceIdFromPageLayoutId(pageLayoutId),
);
const pageLayoutCurrentLayoutsState = useAtomComponentStateCallbackState(
pageLayoutCurrentLayoutsComponentState,
pageLayoutId,
);
const pageLayoutDraggedAreaState = useAtomComponentStateCallbackState(
pageLayoutDraggedAreaComponentState,
pageLayoutId,
);
const pageLayoutDraftState = useAtomComponentStateCallbackState(
pageLayoutDraftComponentState,
pageLayoutId,
);
const store = useStore();
const createPageLayoutRecordTableWidget =
useCallback((): PageLayoutWidget => {
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
const pageLayoutDraggedArea = store.get(pageLayoutDraggedAreaState);
if (!isDefined(activeTabId)) {
throw new Error(
'A tab must be selected to create a new record table widget',
);
}
const widgetId = uuidv4();
const recordTableSize = WIDGET_SIZES[WidgetType.RECORD_TABLE]!;
const defaultSize = recordTableSize.default;
const minimumSize = recordTableSize.minimum;
const position = getDefaultWidgetPosition(
pageLayoutDraggedArea,
defaultSize,
minimumSize,
);
const newWidget = createDefaultRecordTableWidget(
widgetId,
activeTabId,
'Record Table',
{
row: position.y,
column: position.x,
rowSpan: position.h,
columnSpan: position.w,
},
);
const newLayout = {
i: widgetId,
x: position.x,
y: position.y,
w: position.w,
h: position.h,
minW: minimumSize.w,
minH: minimumSize.h,
};
const updatedLayouts = getUpdatedTabLayouts(
allTabLayouts,
activeTabId,
newLayout,
);
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
store.set(pageLayoutDraftState, (prev) => ({
...prev,
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
}));
store.set(pageLayoutDraggedAreaState, null);
return newWidget;
}, [
activeTabId,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutDraggedAreaState,
store,
]);
return { createPageLayoutRecordTableWidget };
};
@@ -4,12 +4,14 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { WidgetType } from '~/generated-metadata/graphql';
export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
@@ -34,6 +36,9 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const { closeSidePanelMenu } = useSidePanelMenu();
const { deleteViewForRecordTableWidget } =
useDeleteViewForRecordTableWidget();
const store = useStore();
const deletePageLayoutWidget = useCallback(
@@ -44,8 +49,24 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
tab.widgets.some((w) => w.id === widgetId),
tab.widgets.some((widget) => widget.id === widgetId),
);
const widgetToDelete = tabWithWidget?.widgets.find(
(widget) => widget.id === widgetId,
);
if (
isDefined(widgetToDelete) &&
widgetToDelete.type === WidgetType.RECORD_TABLE &&
'viewId' in widgetToDelete.configuration &&
isDefined(widgetToDelete.configuration.viewId)
) {
deleteViewForRecordTableWidget(
widgetToDelete.configuration.viewId as string,
);
}
const tabId = tabWithWidget?.id;
if (isDefined(tabId)) {
@@ -72,6 +93,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
},
[
closeSidePanelMenu,
deleteViewForRecordTableWidget,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutEditingWidgetIdState,
@@ -66,7 +66,16 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
return;
}
setPageLayoutEditingWidgetId(widgetId);
if (widgetType === WidgetType.RECORD_TABLE) {
navigatePageLayoutSidePanel({
sidePanelPage: SidePanelPages.PageLayoutRecordTableSettings,
pageTitle: t`Edit Record Table`,
resetNavigationStack: true,
});
setPageLayoutEditingWidgetId(widgetId);
return;
}
setSidePanelPage(SidePanelPages.Root);
closeSidePanelMenu();
},
@@ -0,0 +1,63 @@
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useHasFieldsWidgetChanges = () => {
const store = useStore();
const hasFieldsWidgetChanges = useCallback(
(pageLayoutId: string): boolean => {
const fieldsWidgetGroupsDraft = store.get(
fieldsWidgetGroupsDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const fieldsWidgetGroupsPersisted = store.get(
fieldsWidgetGroupsPersistedComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const fieldsWidgetUngroupedFieldsDraft = store.get(
fieldsWidgetUngroupedFieldsDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const fieldsWidgetUngroupedFieldsPersisted = store.get(
fieldsWidgetUngroupedFieldsPersistedComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const fieldsWidgetEditorModeDraft = store.get(
fieldsWidgetEditorModeDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const fieldsWidgetEditorModePersisted = store.get(
fieldsWidgetEditorModePersistedComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
return (
!isDeeplyEqual(fieldsWidgetGroupsDraft, fieldsWidgetGroupsPersisted) ||
!isDeeplyEqual(
fieldsWidgetUngroupedFieldsDraft,
fieldsWidgetUngroupedFieldsPersisted,
) ||
!isDeeplyEqual(
fieldsWidgetEditorModeDraft,
fieldsWidgetEditorModePersisted,
)
);
},
[store],
);
return { hasFieldsWidgetChanges };
};
@@ -0,0 +1,29 @@
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
export const useHasRecordTableWidgets = () => {
const store = useStore();
const hasRecordTableWidgets = useCallback(
(pageLayoutId: string): boolean => {
const pageLayoutDraft = store.get(
pageLayoutDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
return pageLayoutDraft.tabs.some((tab) =>
tab.widgets.some(
(widget) =>
widget.configuration.configurationType ===
WidgetConfigurationType.RECORD_TABLE,
),
);
},
[store],
);
return { hasRecordTableWidgets };
};
@@ -5,11 +5,13 @@ import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLa
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { WidgetType } from '~/generated-metadata/graphql';
export const useRemovePageLayoutWidgetAndPreservePosition = (
pageLayoutIdFromProps?: string,
@@ -39,6 +41,9 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
pageLayoutId,
);
const { deleteViewForRecordTableWidget } =
useDeleteViewForRecordTableWidget();
const store = useStore();
const removePageLayoutWidgetAndPreservePosition = useCallback(
@@ -47,8 +52,24 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
tab.widgets.some((w) => w.id === widgetId),
tab.widgets.some((widget) => widget.id === widgetId),
);
const widgetToRemove = tabWithWidget?.widgets.find(
(widget) => widget.id === widgetId,
);
if (
isDefined(widgetToRemove) &&
widgetToRemove.type === WidgetType.RECORD_TABLE &&
'viewId' in widgetToRemove.configuration &&
isDefined(widgetToRemove.configuration.viewId)
) {
deleteViewForRecordTableWidget(
widgetToRemove.configuration.viewId as string,
);
}
const tabId = tabWithWidget?.id;
if (!isDefined(tabId)) {
@@ -86,6 +107,7 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
store.set(pageLayoutEditingWidgetIdState, null);
},
[
deleteViewForRecordTableWidget,
pageLayoutCurrentLayoutsState,
pageLayoutDraftState,
pageLayoutDraggedAreaState,
@@ -1,4 +1,5 @@
import { UPSERT_FIELDS_WIDGET } from '@/page-layout/graphql/mutations/upsertFieldsWidget';
import { useHasFieldsWidgetChanges } from '@/page-layout/hooks/useHasFieldsWidgetChanges';
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
@@ -20,10 +21,16 @@ export const useSaveFieldsWidgetGroups = () => {
{ input: UpsertFieldsWidgetInput }
>(UPSERT_FIELDS_WIDGET);
const { hasFieldsWidgetChanges } = useHasFieldsWidgetChanges();
const store = useStore();
const saveFieldsWidgetGroups = useCallback(
async (pageLayoutId: string) => {
if (!hasFieldsWidgetChanges(pageLayoutId)) {
return;
}
const allDraftGroups = store.get(
fieldsWidgetGroupsDraftComponentState.atomFamily({
instanceId: pageLayoutId,
@@ -128,7 +135,7 @@ export const useSaveFieldsWidgetGroups = () => {
allEditorModes,
);
},
[store, upsertFieldsWidgetMutation],
[hasFieldsWidgetChanges, store, upsertFieldsWidgetMutation],
);
return { saveFieldsWidgetGroups };
@@ -44,7 +44,6 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
const featureFlags = useFeatureFlagsMap();
const isRecordPageLayoutEditingEnabled =
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
const store = useStore();
const savePageLayout = useCallback(async () => {
@@ -0,0 +1,36 @@
import { useHasFieldsWidgetChanges } from '@/page-layout/hooks/useHasFieldsWidgetChanges';
import { useHasRecordTableWidgets } from '@/page-layout/hooks/useHasRecordTableWidgets';
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
import { useSaveRecordTableWidgetsViewDataOnDashboardSave } from '@/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave';
import { useCallback } from 'react';
export const useSavePageLayoutWidgetsData = () => {
const { hasFieldsWidgetChanges } = useHasFieldsWidgetChanges();
const { hasRecordTableWidgets } = useHasRecordTableWidgets();
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
const { saveRecordTableWidgetsViewDataOnDashboardSave } =
useSaveRecordTableWidgetsViewDataOnDashboardSave();
const savePageLayoutWidgetsData = useCallback(
async (pageLayoutId: string) => {
if (hasFieldsWidgetChanges(pageLayoutId)) {
await saveFieldsWidgetGroups(pageLayoutId);
}
if (hasRecordTableWidgets(pageLayoutId)) {
await saveRecordTableWidgetsViewDataOnDashboardSave(pageLayoutId);
}
},
[
hasFieldsWidgetChanges,
saveFieldsWidgetGroups,
saveRecordTableWidgetsViewDataOnDashboardSave,
hasRecordTableWidgets,
],
);
return { savePageLayoutWidgetsData };
};
@@ -0,0 +1,57 @@
import {
PageLayoutTabLayoutMode,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
import { createDefaultRecordTableWidget } from '@/page-layout/utils/createDefaultRecordTableWidget';
describe('createDefaultRecordTableWidget', () => {
const widgetId = 'widget-id-1';
const pageLayoutTabId = 'tab-id-1';
const title = 'My Record Table';
const gridPosition = { row: 1, column: 2, rowSpan: 3, columnSpan: 4 };
const widget = createDefaultRecordTableWidget(
widgetId,
pageLayoutTabId,
title,
gridPosition,
);
it('should return correct shape with all required fields', () => {
expect(widget.__typename).toBe('PageLayoutWidget');
expect(widget.id).toBe(widgetId);
expect(widget.pageLayoutTabId).toBe(pageLayoutTabId);
expect(widget.title).toBe(title);
expect(widget.createdAt).toBeDefined();
expect(widget.updatedAt).toBeDefined();
expect(widget.deletedAt).toBeNull();
});
it('should set type to WidgetType.RECORD_TABLE', () => {
expect(widget.type).toBe(WidgetType.RECORD_TABLE);
});
it('should set configuration.configurationType to WidgetConfigurationType.RECORD_TABLE', () => {
expect(widget.configuration).toEqual({
configurationType: WidgetConfigurationType.RECORD_TABLE,
});
});
it('should correctly map grid position values to both gridPosition and position fields', () => {
expect(widget.gridPosition).toEqual(gridPosition);
expect(widget.position).toEqual({
__typename: 'PageLayoutWidgetGridPosition',
layoutMode: PageLayoutTabLayoutMode.GRID,
row: gridPosition.row,
column: gridPosition.column,
rowSpan: gridPosition.rowSpan,
columnSpan: gridPosition.columnSpan,
});
});
it('should set objectMetadataId to null and isOverridden to false', () => {
expect(widget.objectMetadataId).toBeNull();
expect(widget.isOverridden).toBe(false);
});
});
@@ -0,0 +1,39 @@
import {
type GridPosition,
PageLayoutTabLayoutMode,
type PageLayoutWidget,
WidgetConfigurationType,
WidgetType,
} from '~/generated-metadata/graphql';
export const createDefaultRecordTableWidget = (
id: string,
pageLayoutTabId: string,
title: string,
gridPosition: GridPosition,
): PageLayoutWidget => {
return {
__typename: 'PageLayoutWidget',
id,
pageLayoutTabId,
title,
type: WidgetType.RECORD_TABLE,
configuration: {
configurationType: WidgetConfigurationType.RECORD_TABLE,
},
gridPosition,
position: {
__typename: 'PageLayoutWidgetGridPosition',
layoutMode: PageLayoutTabLayoutMode.GRID,
row: gridPosition.row,
column: gridPosition.column,
rowSpan: gridPosition.rowSpan,
columnSpan: gridPosition.columnSpan,
},
objectMetadataId: null,
isOverridden: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
};
};
@@ -14,6 +14,7 @@ import { TaskWidget } from '@/page-layout/widgets/tasks/components/TaskWidget';
import { TimelineWidget } from '@/page-layout/widgets/timeline/components/TimelineWidget';
import { WorkflowRunWidget } from '@/page-layout/widgets/workflow/components/WorkflowRunWidget';
import { WorkflowVersionWidget } from '@/page-layout/widgets/workflow/components/WorkflowVersionWidget';
import { RecordTableWidgetRenderer } from '@/page-layout/widgets/record-table/components/RecordTableWidgetRenderer';
import { WorkflowWidget } from '@/page-layout/widgets/workflow/components/WorkflowWidget';
import { WidgetType } from '~/generated-metadata/graphql';
@@ -73,6 +74,9 @@ export const WidgetContentRenderer = ({
case WidgetType.FRONT_COMPONENT:
return <FrontComponentWidgetRenderer widget={widget} />;
case WidgetType.RECORD_TABLE:
return <RecordTableWidgetRenderer widget={widget} />;
default:
return null;
}
@@ -0,0 +1,34 @@
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { RecordTableWidgetRendererContent } from '@/page-layout/widgets/record-table/components/RecordTableWidgetRendererContent';
import { isDefined } from 'twenty-shared/utils';
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
type RecordTableWidgetRendererProps = {
widget: PageLayoutWidget;
};
export const RecordTableWidgetRenderer = ({
widget,
}: RecordTableWidgetRendererProps) => {
const { configuration } = widget;
const isRecordTableConfiguration =
configuration.configurationType === WidgetConfigurationType.RECORD_TABLE;
const viewId =
isRecordTableConfiguration && 'viewId' in configuration
? (configuration.viewId as string | undefined)
: undefined;
if (!isDefined(widget.objectMetadataId) || !isDefined(viewId)) {
return null;
}
return (
<RecordTableWidgetRendererContent
objectMetadataId={widget.objectMetadataId}
viewId={viewId}
widgetId={widget.id}
/>
);
};
@@ -0,0 +1,29 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { RecordTableWidget } from '@/object-record/record-table-widget/components/RecordTableWidget';
import { RecordTableWidgetProvider } from '@/object-record/record-table-widget/components/RecordTableWidgetProvider';
type RecordTableWidgetRendererContentProps = {
objectMetadataId: string;
viewId: string;
widgetId: string;
};
export const RecordTableWidgetRendererContent = ({
objectMetadataId,
viewId,
widgetId,
}: RecordTableWidgetRendererContentProps) => {
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
return (
<RecordTableWidgetProvider
objectNameSingular={objectMetadataItem.nameSingular}
viewId={viewId}
widgetId={widgetId}
>
<RecordTableWidget />
</RecordTableWidgetProvider>
);
};
@@ -0,0 +1,83 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { ViewType } from '~/generated-metadata/graphql';
const DEFAULT_VIEW_FIELD_SIZE = 180;
const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6;
export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => {
const { performViewAPICreate } = usePerformViewAPIPersist();
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
const { updateCurrentWidgetConfig } =
useUpdateCurrentWidgetConfig(pageLayoutId);
const createViewForRecordTableWidget = useCallback(
async (objectMetadataItem: EnrichedObjectMetadataItem) => {
const newViewId = v4();
const viewResult = await performViewAPICreate(
{
input: {
id: newViewId,
name: `${objectMetadataItem.labelPlural} Table`,
icon: objectMetadataItem.icon ?? 'IconTable',
objectMetadataId: objectMetadataItem.id,
type: ViewType.TABLE,
},
},
objectMetadataItem.id,
);
if (viewResult.status !== 'successful') {
return;
}
const eligibleFields = objectMetadataItem.fields.filter(
filterFieldsForRecordTableViewCreation,
);
const sortedFields = eligibleFields.toSorted(
sortFieldsByRelevanceForRecordTableWidget(
objectMetadataItem.labelIdentifierFieldMetadataId,
),
);
const viewFieldInputs = sortedFields.map((field, index) => ({
id: v4(),
viewId: newViewId,
fieldMetadataId: field.id,
position: index,
size: DEFAULT_VIEW_FIELD_SIZE,
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
}));
try {
await performViewFieldAPICreate({ inputs: viewFieldInputs });
updateCurrentWidgetConfig({
configToUpdate: {
viewId: newViewId,
},
});
} catch (error) {
throw new Error(
'Failed to create view fields for record table widget',
{ cause: error },
);
}
},
[
performViewAPICreate,
performViewFieldAPICreate,
updateCurrentWidgetConfig,
],
);
return { createViewForRecordTableWidget };
};
@@ -0,0 +1,15 @@
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
import { useCallback } from 'react';
export const useDeleteViewForRecordTableWidget = () => {
const { performViewAPIDestroy } = usePerformViewAPIPersist();
const deleteViewForRecordTableWidget = useCallback(
async (viewId: string) => {
await performViewAPIDestroy({ id: viewId });
},
[performViewAPIDestroy],
);
return { deleteViewForRecordTableWidget };
};
@@ -0,0 +1,36 @@
import { type RecordField } from '@/object-record/record-field/types/RecordField';
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
import { type ViewField } from '@/views/types/ViewField';
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useMapRecordFieldToViewFieldWithCurrentAggregateOperation = () => {
const store = useStore();
const mapRecordFieldToViewFieldWithCurrentAggregateOperation = useCallback(
(recordField: RecordField): Omit<ViewField, 'definition'> => {
const viewField = mapRecordFieldToViewField(recordField);
const extendedAggregateOperation = store.get(
viewFieldAggregateOperationState.atomFamily({
viewFieldId: recordField.id,
}),
);
return {
...viewField,
aggregateOperation: isDefined(extendedAggregateOperation)
? convertExtendedAggregateOperationToAggregateOperation(
extendedAggregateOperation,
)
: viewField.aggregateOperation,
};
},
[store],
);
return { mapRecordFieldToViewFieldWithCurrentAggregateOperation };
};
@@ -0,0 +1,25 @@
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
import { type ViewField } from '@/views/types/ViewField';
import { useCallback } from 'react';
export const useMapViewFieldToRecordTableWidgetViewFieldItem = () => {
const { getFieldMetadataItemByIdOrThrow } =
useGetFieldMetadataItemByIdOrThrow();
const mapViewFieldToRecordTableWidgetViewFieldItem = useCallback(
(viewField: ViewField): RecordTableWidgetViewFieldItem | null => {
try {
const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow(
viewField.fieldMetadataId,
);
return { viewField, fieldMetadataItem };
} catch {
return null;
}
},
[getFieldMetadataItemByIdOrThrow],
);
return { mapViewFieldToRecordTableWidgetViewFieldItem };
};
@@ -0,0 +1,26 @@
import { useMapViewFieldToRecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/hooks/useMapViewFieldToRecordTableWidgetViewFieldItem';
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
import { useViewById } from '@/views/hooks/useViewById';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { sortByProperty } from '~/utils/array/sortByProperty';
export const useRecordTableWidgetViewFieldItems = (viewId: string) => {
const { view } = useViewById(viewId);
const { mapViewFieldToRecordTableWidgetViewFieldItem } =
useMapViewFieldToRecordTableWidgetViewFieldItem();
const recordTableWidgetViewFieldItems: RecordTableWidgetViewFieldItem[] =
useMemo(() => {
if (!view) {
return [];
}
return view.viewFields
.toSorted(sortByProperty('position'))
.map(mapViewFieldToRecordTableWidgetViewFieldItem)
.filter(isDefined);
}, [view, mapViewFieldToRecordTableWidgetViewFieldItem]);
return { recordTableWidgetViewFieldItems };
};
@@ -0,0 +1,35 @@
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useCallback } from 'react';
export const useReorderRecordTableWidgetFields = () => {
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
const reorderRecordTableWidgetFields = useCallback(
async (
sourceIndex: number,
destinationIndex: number,
visibleFieldItems: RecordTableWidgetViewFieldItem[],
) => {
if (sourceIndex === destinationIndex) {
return;
}
const reorderedFields = [...visibleFieldItems];
const [movedField] = reorderedFields.splice(sourceIndex, 1);
reorderedFields.splice(destinationIndex, 0, movedField);
const updates = reorderedFields.map((fieldItem, index) => ({
input: {
id: fieldItem.viewField.id,
update: { position: index },
},
}));
await performViewFieldAPIUpdate(updates);
},
[performViewFieldAPIUpdate],
);
return { reorderRecordTableWidgetFields };
};
@@ -0,0 +1,162 @@
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordTableWidgetFiltersToView = (
viewId: string,
instanceId: string,
) => {
const {
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
} = usePerformViewFilterAPIPersist();
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
} = usePerformViewFilterGroupAPIPersist();
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
currentRecordFiltersComponentState,
instanceId,
);
const currentRecordFilterGroupsCallbackState =
useAtomComponentStateCallbackState(
currentRecordFilterGroupsComponentState,
instanceId,
);
const store = useStore();
const saveRecordTableWidgetFiltersToView = useCallback(async () => {
const views = store.get(viewsSelector.atom);
const currentView = views.find((view) => view.id === viewId);
if (!isDefined(currentView)) {
return;
}
const currentRecordFilters = store.get(currentRecordFiltersCallbackState);
const currentRecordFilterGroups = store.get(
currentRecordFilterGroupsCallbackState,
);
const newViewFilterGroups = currentRecordFilterGroups.map(
(recordFilterGroup) =>
mapRecordFilterGroupToViewFilterGroup({
recordFilterGroup,
view: currentView,
}),
);
const currentViewFilterGroups = currentView.viewFilterGroups ?? [];
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
currentViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
currentViewFilterGroups,
newViewFilterGroups,
);
await performViewFilterGroupAPICreate(
viewFilterGroupsToCreate,
currentView,
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
const newViewFilters = currentRecordFilters.map(
mapRecordFilterToViewFilter,
);
const currentViewFilters = currentView.viewFilters ?? [];
const viewFiltersToCreate = getViewFiltersToCreate(
currentViewFilters,
newViewFilters,
);
const viewFiltersToUpdate = getViewFiltersToUpdate(
currentViewFilters,
newViewFilters,
);
const viewFiltersToDelete = getViewFiltersToDelete(
currentViewFilters,
newViewFilters,
);
await performViewFilterAPIDelete(
viewFiltersToDelete.map((viewFilter) => ({
input: { id: viewFilter.id },
})),
);
await performViewFilterAPIUpdate(
viewFiltersToUpdate.map((viewFilter) => ({
input: {
id: viewFilter.id,
update: {
value: viewFilter.value,
operand: viewFilter.operand,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
subFieldName: viewFilter.subFieldName ?? null,
},
},
})),
);
await performViewFilterAPICreate(
viewFiltersToCreate.map((viewFilter) => ({
input: {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: currentView.id,
value: viewFilter.value,
operand: viewFilter.operand,
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
},
})),
);
await performViewFilterGroupAPIDelete(
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
);
}, [
store,
viewId,
currentRecordFiltersCallbackState,
currentRecordFilterGroupsCallbackState,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
]);
return { saveRecordTableWidgetFiltersToView };
};
@@ -0,0 +1,86 @@
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete';
import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate';
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useSaveRecordTableWidgetSortsToView = (viewId: string) => {
const {
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
} = usePerformViewSortAPIPersist();
const store = useStore();
const saveRecordTableWidgetSortsToView = useCallback(
async (recordSorts: RecordSort[]) => {
const views = store.get(viewsSelector.atom);
const currentView = views.find((view) => view.id === viewId);
if (!isDefined(currentView)) {
return;
}
const currentViewSorts = currentView.viewSorts ?? [];
const newViewSorts = recordSorts.map(mapRecordSortToViewSort);
const viewSortsToCreate = getViewSortsToCreate(
currentViewSorts,
newViewSorts,
);
const viewSortsToUpdate = getViewSortsToUpdate(
currentViewSorts,
newViewSorts,
);
const viewSortsToDelete = getViewSortsToDelete(
currentViewSorts,
newViewSorts,
);
await performViewSortAPICreate(
viewSortsToCreate.map((viewSort) => ({
input: {
id: viewSort.id,
fieldMetadataId: viewSort.fieldMetadataId,
viewId: currentView.id,
direction: viewSort.direction,
},
})),
);
await performViewSortAPIUpdate(
viewSortsToUpdate.map((viewSort) => ({
input: {
id: viewSort.id,
update: {
direction: viewSort.direction,
},
},
})),
);
await performViewSortAPIDelete(
viewSortsToDelete.map((viewSort) => ({
input: {
id: viewSort.id,
},
})),
);
},
[
store,
viewId,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
],
);
return { saveRecordTableWidgetSortsToView };
};
@@ -0,0 +1,306 @@
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
import { useMapRecordFieldToViewFieldWithCurrentAggregateOperation } from '@/page-layout/widgets/record-table/hooks/useMapRecordFieldToViewFieldWithCurrentAggregateOperation';
import { computeViewFieldsToCreateAndUpdate } from '@/page-layout/widgets/record-table/utils/computeViewFieldsToCreateAndUpdate';
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete';
import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate';
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
const {
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
} = usePerformViewFilterAPIPersist();
const {
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
} = usePerformViewFilterGroupAPIPersist();
const {
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
} = usePerformViewSortAPIPersist();
const { performViewFieldAPICreate, performViewFieldAPIUpdate } =
usePerformViewFieldAPIPersist();
const { mapRecordFieldToViewFieldWithCurrentAggregateOperation } =
useMapRecordFieldToViewFieldWithCurrentAggregateOperation();
const store = useStore();
const saveRecordTableWidgetsViewDataOnDashboardSave = useCallback(
async (pageLayoutId: string) => {
const pageLayoutDraft = store.get(
pageLayoutDraftComponentState.atomFamily({
instanceId: pageLayoutId,
}),
);
const views = store.get(viewsSelector.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const recordTableWidgets = pageLayoutDraft.tabs.flatMap((tab) =>
tab.widgets.filter(
(widget) =>
widget.configuration.configurationType ===
WidgetConfigurationType.RECORD_TABLE,
),
);
for (const widget of recordTableWidgets) {
const configuration = widget.configuration;
if (!('viewId' in configuration) || !isDefined(configuration.viewId)) {
continue;
}
const viewId = configuration.viewId as string;
const currentView = views.find((view) => view.id === viewId);
if (!isDefined(currentView) || !isDefined(widget.objectMetadataId)) {
continue;
}
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === widget.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
continue;
}
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
objectMetadataItem.namePlural,
viewId,
);
const currentRecordFilters = store.get(
currentRecordFiltersComponentState.atomFamily({
instanceId: recordIndexId,
}),
);
const currentRecordFilterGroups = store.get(
currentRecordFilterGroupsComponentState.atomFamily({
instanceId: recordIndexId,
}),
);
const currentRecordSorts = store.get(
currentRecordSortsComponentState.atomFamily({
instanceId: recordIndexId,
}),
);
const newViewFilterGroups = currentRecordFilterGroups.map(
(recordFilterGroup) =>
mapRecordFilterGroupToViewFilterGroup({
recordFilterGroup,
view: currentView,
}),
);
const existingViewFilterGroups = currentView.viewFilterGroups ?? [];
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
existingViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
existingViewFilterGroups,
newViewFilterGroups,
);
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
existingViewFilterGroups,
newViewFilterGroups,
);
const newViewFilters = currentRecordFilters.map(
mapRecordFilterToViewFilter,
);
const existingViewFilters = currentView.viewFilters ?? [];
const viewFiltersToCreate = getViewFiltersToCreate(
existingViewFilters,
newViewFilters,
);
const viewFiltersToUpdate = getViewFiltersToUpdate(
existingViewFilters,
newViewFilters,
);
const viewFiltersToDelete = getViewFiltersToDelete(
existingViewFilters,
newViewFilters,
);
const newViewSorts = currentRecordSorts.map(mapRecordSortToViewSort);
const existingViewSorts = currentView.viewSorts ?? [];
const viewSortsToCreate = getViewSortsToCreate(
existingViewSorts,
newViewSorts,
);
const viewSortsToUpdate = getViewSortsToUpdate(
existingViewSorts,
newViewSorts,
);
const viewSortsToDelete = getViewSortsToDelete(
existingViewSorts,
newViewSorts,
);
const currentRecordFields = store.get(
currentRecordFieldsComponentState.atomFamily({
instanceId: recordIndexId,
}),
);
const newViewFields = currentRecordFields.map(
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
);
const { viewFieldsToCreate, viewFieldsToUpdate } =
computeViewFieldsToCreateAndUpdate({
newViewFields,
existingViewFields: currentView.viewFields ?? [],
viewId,
});
const hasNoRecordTableWidgetChanges =
viewFilterGroupsToCreate.length === 0 &&
viewFilterGroupsToUpdate.length === 0 &&
viewFilterGroupsToDelete.length === 0 &&
viewFiltersToCreate.length === 0 &&
viewFiltersToUpdate.length === 0 &&
viewFiltersToDelete.length === 0 &&
viewSortsToCreate.length === 0 &&
viewSortsToUpdate.length === 0 &&
viewSortsToDelete.length === 0 &&
viewFieldsToCreate.length === 0 &&
viewFieldsToUpdate.length === 0;
if (hasNoRecordTableWidgetChanges) {
continue;
}
await performViewFilterGroupAPICreate(
viewFilterGroupsToCreate,
currentView,
);
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
await performViewFilterAPIDelete(
viewFiltersToDelete.map((viewFilter) => ({
input: { id: viewFilter.id },
})),
);
await performViewFilterAPIUpdate(
viewFiltersToUpdate.map((viewFilter) => ({
input: {
id: viewFilter.id,
update: {
value: viewFilter.value,
operand: viewFilter.operand,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
subFieldName: viewFilter.subFieldName ?? null,
},
},
})),
);
await performViewFilterAPICreate(
viewFiltersToCreate.map((viewFilter) => ({
input: {
id: viewFilter.id,
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: currentView.id,
value: viewFilter.value,
operand: viewFilter.operand,
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
},
})),
);
await performViewFilterGroupAPIDelete(
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
);
await performViewSortAPICreate(
viewSortsToCreate.map((viewSort) => ({
input: {
id: viewSort.id,
fieldMetadataId: viewSort.fieldMetadataId,
viewId: currentView.id,
direction: viewSort.direction,
},
})),
);
await performViewSortAPIUpdate(
viewSortsToUpdate.map((viewSort) => ({
input: {
id: viewSort.id,
update: {
direction: viewSort.direction,
},
},
})),
);
await performViewSortAPIDelete(
viewSortsToDelete.map((viewSort) => ({
input: { id: viewSort.id },
})),
);
await Promise.all([
performViewFieldAPICreate({ inputs: viewFieldsToCreate }),
performViewFieldAPIUpdate(viewFieldsToUpdate),
]);
}
},
[
store,
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
performViewFieldAPICreate,
performViewFieldAPIUpdate,
performViewFilterAPICreate,
performViewFilterAPIUpdate,
performViewFilterAPIDelete,
performViewFilterGroupAPICreate,
performViewFilterGroupAPIUpdate,
performViewFilterGroupAPIDelete,
performViewSortAPICreate,
performViewSortAPIUpdate,
performViewSortAPIDelete,
],
);
return { saveRecordTableWidgetsViewDataOnDashboardSave };
};
@@ -0,0 +1,22 @@
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
import { useCallback } from 'react';
export const useToggleRecordTableWidgetFieldVisibility = () => {
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
const toggleRecordTableWidgetFieldVisibility = useCallback(
async (viewFieldId: string, isVisible: boolean) => {
await performViewFieldAPIUpdate([
{
input: {
id: viewFieldId,
update: { isVisible },
},
},
]);
},
[performViewFieldAPIUpdate],
);
return { toggleRecordTableWidgetFieldVisibility };
};
@@ -0,0 +1,7 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ViewField } from '@/views/types/ViewField';
export type RecordTableWidgetViewFieldItem = {
viewField: ViewField;
fieldMetadataItem: FieldMetadataItem;
};

Some files were not shown because too many files have changed in this diff Show More