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
@@ -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,
],
);