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:
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git stash:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
**/**/.env
|
||||
.DS_Store
|
||||
/.idea
|
||||
.claude/settings.json
|
||||
**/**/node_modules/
|
||||
.cache
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+4
@@ -1,6 +1,7 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -17,6 +18,8 @@ export const SaveDashboardSingleRecordCommand = () => {
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
|
||||
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
@@ -26,6 +29,7 @@ export const SaveDashboardSingleRecordCommand = () => {
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
await savePageLayoutWidgetsData(pageLayoutId);
|
||||
closeSidePanelMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
|
||||
import { useHeadlessCommandContextApi } from '@/command-menu-item/engine-command/hooks/useHeadlessCommandContextApi';
|
||||
import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -22,7 +22,7 @@ export const SaveRecordPageLayoutSingleRecordCommand = () => {
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
|
||||
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
|
||||
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
@@ -33,7 +33,7 @@ export const SaveRecordPageLayoutSingleRecordCommand = () => {
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
await saveFieldsWidgetGroups(pageLayoutId);
|
||||
await savePageLayoutWidgetsData(pageLayoutId);
|
||||
|
||||
closeSidePanelMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
|
||||
+4
@@ -3,6 +3,7 @@ import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-re
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
|
||||
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
|
||||
import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
|
||||
@@ -15,6 +16,8 @@ export const SaveDashboardSingleRecordCommand = () => {
|
||||
|
||||
const { savePageLayout } = useSavePageLayout(pageLayoutId);
|
||||
|
||||
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
|
||||
|
||||
const { setIsPageLayoutInEditMode } =
|
||||
useSetIsPageLayoutInEditMode(pageLayoutId);
|
||||
|
||||
@@ -24,6 +27,7 @@ export const SaveDashboardSingleRecordCommand = () => {
|
||||
const result = await savePageLayout();
|
||||
|
||||
if (result.status === 'successful') {
|
||||
await savePageLayoutWidgetsData(pageLayoutId);
|
||||
closeSidePanelMenu();
|
||||
setIsPageLayoutInEditMode(false);
|
||||
}
|
||||
|
||||
+5
-1
@@ -82,9 +82,13 @@ export const useConvertBackendItemToCommandMenuItemConfig = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isEditModeItem =
|
||||
item.conditionalAvailabilityExpression?.includes('isPageInEditMode') ??
|
||||
false;
|
||||
|
||||
const isPinned =
|
||||
item.availabilityType !== CommandMenuItemAvailabilityType.FALLBACK &&
|
||||
!contextStoreIsPageInEditMode &&
|
||||
(!contextStoreIsPageInEditMode || isEditModeItem) &&
|
||||
item.isPinned;
|
||||
|
||||
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/sta
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { useSaveNavigationMenuItemsDraft } from '@/navigation-menu-item/edit/hooks/useSaveNavigationMenuItemsDraft';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSavePageLayoutWidgetsData } from '@/page-layout/hooks/useSavePageLayoutWidgetsData';
|
||||
import { useUpdatePageLayoutWithTabsAndWidgets } from '@/page-layout/hooks/useUpdatePageLayoutWithTabsAndWidgets';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
@@ -35,7 +35,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
const { updatePageLayoutWithTabsAndWidgets } =
|
||||
useUpdatePageLayoutWithTabsAndWidgets();
|
||||
const { exitLayoutCustomizationMode } = useExitLayoutCustomizationMode();
|
||||
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
|
||||
const { savePageLayoutWidgetsData } = useSavePageLayoutWidgetsData();
|
||||
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
@@ -143,7 +143,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
}
|
||||
}
|
||||
|
||||
await saveFieldsWidgetGroups(pageLayoutId);
|
||||
await savePageLayoutWidgetsData(pageLayoutId);
|
||||
}
|
||||
|
||||
if (hasAnyFailure) {
|
||||
@@ -165,7 +165,7 @@ export const useSaveLayoutCustomization = () => {
|
||||
}, [
|
||||
saveDraft,
|
||||
updatePageLayoutWithTabsAndWidgets,
|
||||
saveFieldsWidgetGroups,
|
||||
savePageLayoutWidgetsData,
|
||||
exitLayoutCustomizationMode,
|
||||
enqueueErrorSnackBar,
|
||||
isRecordPageLayoutEditingEnabled,
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const filterReadableActiveObjectMetadataItems = (
|
||||
objectMetadataItems: EnrichedObjectMetadataItem[],
|
||||
objectPermissionsByObjectMetadataId: Record<
|
||||
string,
|
||||
ObjectPermissions & { objectMetadataId: string }
|
||||
>,
|
||||
): EnrichedObjectMetadataItem[] =>
|
||||
objectMetadataItems.filter((objectMetadataItem) => {
|
||||
const objectPermissions =
|
||||
objectPermissionsByObjectMetadataId[objectMetadataItem.id];
|
||||
|
||||
return (
|
||||
isDefined(objectPermissions) &&
|
||||
objectPermissions.canReadObjectRecords &&
|
||||
objectMetadataItem.isActive
|
||||
);
|
||||
});
|
||||
+2
-6
@@ -65,9 +65,7 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadata.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return fieldMetadata.morphRelations.map((morphRelation) => ({
|
||||
@@ -100,9 +98,7 @@ export const mapObjectMetadataToGraphQLQuery = ({
|
||||
}
|
||||
|
||||
if (!isDefined(fieldMetadata.morphRelations)) {
|
||||
throw new Error(
|
||||
`Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return fieldMetadata.morphRelations.map((morphRelation) => ({
|
||||
|
||||
+2
-13
@@ -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 />}
|
||||
</>
|
||||
|
||||
+11
@@ -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);
|
||||
|
||||
+21
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+42
@@ -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;
|
||||
};
|
||||
+109
@@ -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>
|
||||
);
|
||||
};
|
||||
+56
@@ -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;
|
||||
};
|
||||
+70
@@ -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;
|
||||
};
|
||||
+12
@@ -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,
|
||||
});
|
||||
+33
-3
@@ -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;
|
||||
|
||||
+24
-4
@@ -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}
|
||||
>
|
||||
|
||||
+20
-2
@@ -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>
|
||||
);
|
||||
|
||||
+25
-6
@@ -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>
|
||||
|
||||
+9
@@ -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;
|
||||
}
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
+11
-5
@@ -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;
|
||||
};
|
||||
|
||||
+8
-2
@@ -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;
|
||||
};
|
||||
|
||||
+49
-29
@@ -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
-8
@@ -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>
|
||||
|
||||
+3
-3
@@ -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);
|
||||
+12
@@ -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 {
|
||||
|
||||
+15
-2
@@ -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}
|
||||
|
||||
+4
-4
@@ -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[
|
||||
|
||||
+8
-1
@@ -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={
|
||||
|
||||
+9
-15
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+21
-13
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
+20
-13
@@ -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,
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ export const RecordTableCellHoveredPortalContent = () => {
|
||||
|
||||
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
const isFieldInputOnly = useIsFieldInputOnly() && !isReadOnly;
|
||||
|
||||
const showButton =
|
||||
!isFieldInputOnly &&
|
||||
|
||||
+11
-1
@@ -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)) {
|
||||
|
||||
+10
-7
@@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+34
-29
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+35
-30
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-3
@@ -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};
|
||||
`;
|
||||
|
||||
+46
-10
@@ -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>
|
||||
);
|
||||
|
||||
+26
-3
@@ -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>
|
||||
);
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
+32
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-5
@@ -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
-49
@@ -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}
|
||||
|
||||
+71
@@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+24
-6
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+26
-6
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+20
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
+13
-32
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+58
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+35
@@ -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>
|
||||
);
|
||||
};
|
||||
+9
@@ -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,
|
||||
});
|
||||
+9
@@ -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,
|
||||
});
|
||||
+9
@@ -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,
|
||||
});
|
||||
+9
@@ -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,
|
||||
});
|
||||
+9
@@ -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,
|
||||
});
|
||||
+70
@@ -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);
|
||||
});
|
||||
});
|
||||
+15
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
+13
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
+15
-2
@@ -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;
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export const getRecordTableCellId = (
|
||||
recordTableId: string,
|
||||
column: number,
|
||||
row: number,
|
||||
): string => {
|
||||
return `record-table-cell-${recordTableId}-${column}-${row}`;
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const getRecordTableHtmlId = (recordTableId: string): string => {
|
||||
return `record-table-${recordTableId}`;
|
||||
};
|
||||
+3
-2
@@ -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);
|
||||
};
|
||||
|
||||
+14
-34
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+17
-2
@@ -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}
|
||||
|
||||
+3
@@ -127,11 +127,13 @@ export const useTriggerInitialRecordTableDataLoad = () => {
|
||||
resetVirtualizedRowTreadmill();
|
||||
|
||||
updateRecordTableCSSVariable(
|
||||
recordTableId,
|
||||
RECORD_TABLE_VERTICAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
|
||||
'hidden',
|
||||
);
|
||||
|
||||
updateRecordTableCSSVariable(
|
||||
recordTableId,
|
||||
RECORD_TABLE_HORIZONTAL_SCROLL_SHADOW_VISIBILITY_CSS_VARIABLE_NAME,
|
||||
'hidden',
|
||||
);
|
||||
@@ -226,6 +228,7 @@ export const useTriggerInitialRecordTableDataLoad = () => {
|
||||
upsertRecordsInStore,
|
||||
loadRecordsToVirtualRows,
|
||||
reapplyRowSelection,
|
||||
recordTableId,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -127,16 +127,6 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
|
||||
tabListInstanceId,
|
||||
});
|
||||
|
||||
const handleLayoutChangeWithoutPendingPlaceholder = (
|
||||
currentLayout: Layout[],
|
||||
allLayouts: Layouts,
|
||||
) => {
|
||||
handleLayoutChange(
|
||||
currentLayout,
|
||||
filterPendingPlaceholderFromLayouts(allLayouts),
|
||||
);
|
||||
};
|
||||
|
||||
const gridContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isPageLayoutInEditMode = useIsPageLayoutInEditMode();
|
||||
@@ -161,6 +151,16 @@ export const PageLayoutGridLayout = ({ tabId }: PageLayoutGridLayoutProps) => {
|
||||
|
||||
const hasPendingPlaceholder = isDefined(pageLayoutDraggedArea);
|
||||
|
||||
const handleLayoutChangeWithoutPendingPlaceholder = (
|
||||
currentLayout: Layout[],
|
||||
allLayouts: Layouts,
|
||||
) => {
|
||||
handleLayoutChange(
|
||||
currentLayout,
|
||||
filterPendingPlaceholderFromLayouts(allLayouts),
|
||||
);
|
||||
};
|
||||
|
||||
const baseLayouts = isLayoutEmpty
|
||||
? EMPTY_LAYOUT
|
||||
: (pageLayoutCurrentLayouts[tabId] ?? EMPTY_LAYOUT);
|
||||
|
||||
@@ -14,4 +14,8 @@ export const WIDGET_SIZES: Partial<Record<WidgetType, WidgetSizeConfig>> = {
|
||||
default: { w: 8, h: 8 },
|
||||
minimum: { w: 4, h: 4 },
|
||||
},
|
||||
[WidgetType.RECORD_TABLE]: {
|
||||
default: { w: 6, h: 6 },
|
||||
minimum: { w: 4, h: 4 },
|
||||
},
|
||||
};
|
||||
|
||||
+4
@@ -188,6 +188,10 @@ export const PAGE_LAYOUT_WIDGET_FRAGMENT = gql`
|
||||
... on ViewConfiguration {
|
||||
configurationType
|
||||
}
|
||||
... on RecordTableConfiguration {
|
||||
configurationType
|
||||
viewId
|
||||
}
|
||||
... on WorkflowConfiguration {
|
||||
configurationType
|
||||
}
|
||||
|
||||
+18
@@ -8,6 +8,24 @@ import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pa
|
||||
import { createStore } from 'jotai';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
jest.mock(
|
||||
'@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget',
|
||||
() => ({
|
||||
useDeleteViewForRecordTableWidget: () => ({
|
||||
deleteViewForRecordTableWidget: jest.fn(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@/side-panel/hooks/useSidePanelMenu', () => ({
|
||||
useSidePanelMenu: () => ({
|
||||
closeSidePanelMenu: jest.fn(),
|
||||
openSidePanelMenu: jest.fn(),
|
||||
navigateSidePanelMenu: jest.fn(),
|
||||
toggleSidePanelMenu: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useDeletePageLayoutWidget', () => {
|
||||
const getWrapper =
|
||||
(store = createStore()) =>
|
||||
|
||||
+4
@@ -22,6 +22,10 @@ export const useChangePageLayoutDragSelection = (
|
||||
|
||||
const changePageLayoutDragSelection = useCallback(
|
||||
(cellId: string, selected: boolean) => {
|
||||
if (!cellId.startsWith('cell-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(pageLayoutSelectedCellsState, (prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (selected) {
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
import { WIDGET_SIZES } from '@/page-layout/constants/WidgetSizes';
|
||||
import { PageLayoutComponentInstanceContext } from '@/page-layout/states/contexts/PageLayoutComponentInstanceContext';
|
||||
import { pageLayoutCurrentLayoutsComponentState } from '@/page-layout/states/pageLayoutCurrentLayoutsComponentState';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLayoutDraggedAreaComponentState';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { addWidgetToTab } from '@/page-layout/utils/addWidgetToTab';
|
||||
import { createDefaultRecordTableWidget } from '@/page-layout/utils/createDefaultRecordTableWidget';
|
||||
import { getDefaultWidgetPosition } from '@/page-layout/utils/getDefaultWidgetPosition';
|
||||
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
|
||||
import { getUpdatedTabLayouts } from '@/page-layout/utils/getUpdatedTabLayouts';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCreatePageLayoutRecordTableWidget = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
PageLayoutComponentInstanceContext,
|
||||
pageLayoutIdFromProps,
|
||||
);
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
getTabListInstanceIdFromPageLayoutId(pageLayoutId),
|
||||
);
|
||||
|
||||
const pageLayoutCurrentLayoutsState = useAtomComponentStateCallbackState(
|
||||
pageLayoutCurrentLayoutsComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutDraggedAreaState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraggedAreaComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutDraftState = useAtomComponentStateCallbackState(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const createPageLayoutRecordTableWidget =
|
||||
useCallback((): PageLayoutWidget => {
|
||||
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
|
||||
const pageLayoutDraggedArea = store.get(pageLayoutDraggedAreaState);
|
||||
|
||||
if (!isDefined(activeTabId)) {
|
||||
throw new Error(
|
||||
'A tab must be selected to create a new record table widget',
|
||||
);
|
||||
}
|
||||
|
||||
const widgetId = uuidv4();
|
||||
const recordTableSize = WIDGET_SIZES[WidgetType.RECORD_TABLE]!;
|
||||
const defaultSize = recordTableSize.default;
|
||||
const minimumSize = recordTableSize.minimum;
|
||||
const position = getDefaultWidgetPosition(
|
||||
pageLayoutDraggedArea,
|
||||
defaultSize,
|
||||
minimumSize,
|
||||
);
|
||||
|
||||
const newWidget = createDefaultRecordTableWidget(
|
||||
widgetId,
|
||||
activeTabId,
|
||||
'Record Table',
|
||||
{
|
||||
row: position.y,
|
||||
column: position.x,
|
||||
rowSpan: position.h,
|
||||
columnSpan: position.w,
|
||||
},
|
||||
);
|
||||
|
||||
const newLayout = {
|
||||
i: widgetId,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
w: position.w,
|
||||
h: position.h,
|
||||
minW: minimumSize.w,
|
||||
minH: minimumSize.h,
|
||||
};
|
||||
|
||||
const updatedLayouts = getUpdatedTabLayouts(
|
||||
allTabLayouts,
|
||||
activeTabId,
|
||||
newLayout,
|
||||
);
|
||||
|
||||
store.set(pageLayoutCurrentLayoutsState, updatedLayouts);
|
||||
|
||||
store.set(pageLayoutDraftState, (prev) => ({
|
||||
...prev,
|
||||
tabs: addWidgetToTab(prev.tabs, activeTabId, newWidget),
|
||||
}));
|
||||
|
||||
store.set(pageLayoutDraggedAreaState, null);
|
||||
|
||||
return newWidget;
|
||||
}, [
|
||||
activeTabId,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
pageLayoutDraftState,
|
||||
pageLayoutDraggedAreaState,
|
||||
store,
|
||||
]);
|
||||
|
||||
return { createPageLayoutRecordTableWidget };
|
||||
};
|
||||
@@ -4,12 +4,14 @@ import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDr
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
|
||||
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
const pageLayoutId = useAvailableComponentInstanceIdOrThrow(
|
||||
@@ -34,6 +36,9 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const { deleteViewForRecordTableWidget } =
|
||||
useDeleteViewForRecordTableWidget();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const deletePageLayoutWidget = useCallback(
|
||||
@@ -44,8 +49,24 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
|
||||
|
||||
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
|
||||
tab.widgets.some((w) => w.id === widgetId),
|
||||
tab.widgets.some((widget) => widget.id === widgetId),
|
||||
);
|
||||
|
||||
const widgetToDelete = tabWithWidget?.widgets.find(
|
||||
(widget) => widget.id === widgetId,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(widgetToDelete) &&
|
||||
widgetToDelete.type === WidgetType.RECORD_TABLE &&
|
||||
'viewId' in widgetToDelete.configuration &&
|
||||
isDefined(widgetToDelete.configuration.viewId)
|
||||
) {
|
||||
deleteViewForRecordTableWidget(
|
||||
widgetToDelete.configuration.viewId as string,
|
||||
);
|
||||
}
|
||||
|
||||
const tabId = tabWithWidget?.id;
|
||||
|
||||
if (isDefined(tabId)) {
|
||||
@@ -72,6 +93,7 @@ export const useDeletePageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
},
|
||||
[
|
||||
closeSidePanelMenu,
|
||||
deleteViewForRecordTableWidget,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
pageLayoutDraftState,
|
||||
pageLayoutEditingWidgetIdState,
|
||||
|
||||
@@ -66,7 +66,16 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
if (widgetType === WidgetType.RECORD_TABLE) {
|
||||
navigatePageLayoutSidePanel({
|
||||
sidePanelPage: SidePanelPages.PageLayoutRecordTableSettings,
|
||||
pageTitle: t`Edit Record Table`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
setPageLayoutEditingWidgetId(widgetId);
|
||||
return;
|
||||
}
|
||||
|
||||
setSidePanelPage(SidePanelPages.Root);
|
||||
closeSidePanelMenu();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
|
||||
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
import { fieldsWidgetGroupsPersistedComponentState } from '@/page-layout/states/fieldsWidgetGroupsPersistedComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsDraftComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsDraftComponentState';
|
||||
import { fieldsWidgetUngroupedFieldsPersistedComponentState } from '@/page-layout/states/fieldsWidgetUngroupedFieldsPersistedComponentState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useHasFieldsWidgetChanges = () => {
|
||||
const store = useStore();
|
||||
|
||||
const hasFieldsWidgetChanges = useCallback(
|
||||
(pageLayoutId: string): boolean => {
|
||||
const fieldsWidgetGroupsDraft = store.get(
|
||||
fieldsWidgetGroupsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const fieldsWidgetGroupsPersisted = store.get(
|
||||
fieldsWidgetGroupsPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const fieldsWidgetUngroupedFieldsDraft = store.get(
|
||||
fieldsWidgetUngroupedFieldsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const fieldsWidgetUngroupedFieldsPersisted = store.get(
|
||||
fieldsWidgetUngroupedFieldsPersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const fieldsWidgetEditorModeDraft = store.get(
|
||||
fieldsWidgetEditorModeDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const fieldsWidgetEditorModePersisted = store.get(
|
||||
fieldsWidgetEditorModePersistedComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
!isDeeplyEqual(fieldsWidgetGroupsDraft, fieldsWidgetGroupsPersisted) ||
|
||||
!isDeeplyEqual(
|
||||
fieldsWidgetUngroupedFieldsDraft,
|
||||
fieldsWidgetUngroupedFieldsPersisted,
|
||||
) ||
|
||||
!isDeeplyEqual(
|
||||
fieldsWidgetEditorModeDraft,
|
||||
fieldsWidgetEditorModePersisted,
|
||||
)
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { hasFieldsWidgetChanges };
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useHasRecordTableWidgets = () => {
|
||||
const store = useStore();
|
||||
|
||||
const hasRecordTableWidgets = useCallback(
|
||||
(pageLayoutId: string): boolean => {
|
||||
const pageLayoutDraft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
|
||||
return pageLayoutDraft.tabs.some((tab) =>
|
||||
tab.widgets.some(
|
||||
(widget) =>
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.RECORD_TABLE,
|
||||
),
|
||||
);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { hasRecordTableWidgets };
|
||||
};
|
||||
+23
-1
@@ -5,11 +5,13 @@ import { pageLayoutDraggedAreaComponentState } from '@/page-layout/states/pageLa
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { removeWidgetFromTab } from '@/page-layout/utils/removeWidgetFromTab';
|
||||
import { removeWidgetLayoutFromTab } from '@/page-layout/utils/removeWidgetLayoutFromTab';
|
||||
import { useDeleteViewForRecordTableWidget } from '@/page-layout/widgets/record-table/hooks/useDeleteViewForRecordTableWidget';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useRemovePageLayoutWidgetAndPreservePosition = (
|
||||
pageLayoutIdFromProps?: string,
|
||||
@@ -39,6 +41,9 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { deleteViewForRecordTableWidget } =
|
||||
useDeleteViewForRecordTableWidget();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const removePageLayoutWidgetAndPreservePosition = useCallback(
|
||||
@@ -47,8 +52,24 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
|
||||
const allTabLayouts = store.get(pageLayoutCurrentLayoutsState);
|
||||
|
||||
const tabWithWidget = pageLayoutDraft.tabs.find((tab) =>
|
||||
tab.widgets.some((w) => w.id === widgetId),
|
||||
tab.widgets.some((widget) => widget.id === widgetId),
|
||||
);
|
||||
|
||||
const widgetToRemove = tabWithWidget?.widgets.find(
|
||||
(widget) => widget.id === widgetId,
|
||||
);
|
||||
|
||||
if (
|
||||
isDefined(widgetToRemove) &&
|
||||
widgetToRemove.type === WidgetType.RECORD_TABLE &&
|
||||
'viewId' in widgetToRemove.configuration &&
|
||||
isDefined(widgetToRemove.configuration.viewId)
|
||||
) {
|
||||
deleteViewForRecordTableWidget(
|
||||
widgetToRemove.configuration.viewId as string,
|
||||
);
|
||||
}
|
||||
|
||||
const tabId = tabWithWidget?.id;
|
||||
|
||||
if (!isDefined(tabId)) {
|
||||
@@ -86,6 +107,7 @@ export const useRemovePageLayoutWidgetAndPreservePosition = (
|
||||
store.set(pageLayoutEditingWidgetIdState, null);
|
||||
},
|
||||
[
|
||||
deleteViewForRecordTableWidget,
|
||||
pageLayoutCurrentLayoutsState,
|
||||
pageLayoutDraftState,
|
||||
pageLayoutDraggedAreaState,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UPSERT_FIELDS_WIDGET } from '@/page-layout/graphql/mutations/upsertFieldsWidget';
|
||||
import { useHasFieldsWidgetChanges } from '@/page-layout/hooks/useHasFieldsWidgetChanges';
|
||||
import { fieldsWidgetEditorModeDraftComponentState } from '@/page-layout/states/fieldsWidgetEditorModeDraftComponentState';
|
||||
import { fieldsWidgetEditorModePersistedComponentState } from '@/page-layout/states/fieldsWidgetEditorModePersistedComponentState';
|
||||
import { fieldsWidgetGroupsDraftComponentState } from '@/page-layout/states/fieldsWidgetGroupsDraftComponentState';
|
||||
@@ -20,10 +21,16 @@ export const useSaveFieldsWidgetGroups = () => {
|
||||
{ input: UpsertFieldsWidgetInput }
|
||||
>(UPSERT_FIELDS_WIDGET);
|
||||
|
||||
const { hasFieldsWidgetChanges } = useHasFieldsWidgetChanges();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveFieldsWidgetGroups = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
if (!hasFieldsWidgetChanges(pageLayoutId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allDraftGroups = store.get(
|
||||
fieldsWidgetGroupsDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
@@ -128,7 +135,7 @@ export const useSaveFieldsWidgetGroups = () => {
|
||||
allEditorModes,
|
||||
);
|
||||
},
|
||||
[store, upsertFieldsWidgetMutation],
|
||||
[hasFieldsWidgetChanges, store, upsertFieldsWidgetMutation],
|
||||
);
|
||||
|
||||
return { saveFieldsWidgetGroups };
|
||||
|
||||
@@ -44,7 +44,6 @@ export const useSavePageLayout = (pageLayoutIdFromProps: string) => {
|
||||
const featureFlags = useFeatureFlagsMap();
|
||||
const isRecordPageLayoutEditingEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED];
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const savePageLayout = useCallback(async () => {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useHasFieldsWidgetChanges } from '@/page-layout/hooks/useHasFieldsWidgetChanges';
|
||||
import { useHasRecordTableWidgets } from '@/page-layout/hooks/useHasRecordTableWidgets';
|
||||
import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
|
||||
import { useSaveRecordTableWidgetsViewDataOnDashboardSave } from '@/page-layout/widgets/record-table/hooks/useSaveRecordTableWidgetsViewDataOnDashboardSave';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useSavePageLayoutWidgetsData = () => {
|
||||
const { hasFieldsWidgetChanges } = useHasFieldsWidgetChanges();
|
||||
|
||||
const { hasRecordTableWidgets } = useHasRecordTableWidgets();
|
||||
|
||||
const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
|
||||
|
||||
const { saveRecordTableWidgetsViewDataOnDashboardSave } =
|
||||
useSaveRecordTableWidgetsViewDataOnDashboardSave();
|
||||
|
||||
const savePageLayoutWidgetsData = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
if (hasFieldsWidgetChanges(pageLayoutId)) {
|
||||
await saveFieldsWidgetGroups(pageLayoutId);
|
||||
}
|
||||
|
||||
if (hasRecordTableWidgets(pageLayoutId)) {
|
||||
await saveRecordTableWidgetsViewDataOnDashboardSave(pageLayoutId);
|
||||
}
|
||||
},
|
||||
[
|
||||
hasFieldsWidgetChanges,
|
||||
saveFieldsWidgetGroups,
|
||||
saveRecordTableWidgetsViewDataOnDashboardSave,
|
||||
hasRecordTableWidgets,
|
||||
],
|
||||
);
|
||||
|
||||
return { savePageLayoutWidgetsData };
|
||||
};
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
PageLayoutTabLayoutMode,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { createDefaultRecordTableWidget } from '@/page-layout/utils/createDefaultRecordTableWidget';
|
||||
|
||||
describe('createDefaultRecordTableWidget', () => {
|
||||
const widgetId = 'widget-id-1';
|
||||
const pageLayoutTabId = 'tab-id-1';
|
||||
const title = 'My Record Table';
|
||||
const gridPosition = { row: 1, column: 2, rowSpan: 3, columnSpan: 4 };
|
||||
|
||||
const widget = createDefaultRecordTableWidget(
|
||||
widgetId,
|
||||
pageLayoutTabId,
|
||||
title,
|
||||
gridPosition,
|
||||
);
|
||||
|
||||
it('should return correct shape with all required fields', () => {
|
||||
expect(widget.__typename).toBe('PageLayoutWidget');
|
||||
expect(widget.id).toBe(widgetId);
|
||||
expect(widget.pageLayoutTabId).toBe(pageLayoutTabId);
|
||||
expect(widget.title).toBe(title);
|
||||
expect(widget.createdAt).toBeDefined();
|
||||
expect(widget.updatedAt).toBeDefined();
|
||||
expect(widget.deletedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('should set type to WidgetType.RECORD_TABLE', () => {
|
||||
expect(widget.type).toBe(WidgetType.RECORD_TABLE);
|
||||
});
|
||||
|
||||
it('should set configuration.configurationType to WidgetConfigurationType.RECORD_TABLE', () => {
|
||||
expect(widget.configuration).toEqual({
|
||||
configurationType: WidgetConfigurationType.RECORD_TABLE,
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly map grid position values to both gridPosition and position fields', () => {
|
||||
expect(widget.gridPosition).toEqual(gridPosition);
|
||||
expect(widget.position).toEqual({
|
||||
__typename: 'PageLayoutWidgetGridPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: gridPosition.row,
|
||||
column: gridPosition.column,
|
||||
rowSpan: gridPosition.rowSpan,
|
||||
columnSpan: gridPosition.columnSpan,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set objectMetadataId to null and isOverridden to false', () => {
|
||||
expect(widget.objectMetadataId).toBeNull();
|
||||
expect(widget.isOverridden).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
type GridPosition,
|
||||
PageLayoutTabLayoutMode,
|
||||
type PageLayoutWidget,
|
||||
WidgetConfigurationType,
|
||||
WidgetType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const createDefaultRecordTableWidget = (
|
||||
id: string,
|
||||
pageLayoutTabId: string,
|
||||
title: string,
|
||||
gridPosition: GridPosition,
|
||||
): PageLayoutWidget => {
|
||||
return {
|
||||
__typename: 'PageLayoutWidget',
|
||||
id,
|
||||
pageLayoutTabId,
|
||||
title,
|
||||
type: WidgetType.RECORD_TABLE,
|
||||
configuration: {
|
||||
configurationType: WidgetConfigurationType.RECORD_TABLE,
|
||||
},
|
||||
gridPosition,
|
||||
position: {
|
||||
__typename: 'PageLayoutWidgetGridPosition',
|
||||
layoutMode: PageLayoutTabLayoutMode.GRID,
|
||||
row: gridPosition.row,
|
||||
column: gridPosition.column,
|
||||
rowSpan: gridPosition.rowSpan,
|
||||
columnSpan: gridPosition.columnSpan,
|
||||
},
|
||||
objectMetadataId: null,
|
||||
isOverridden: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
};
|
||||
};
|
||||
+4
@@ -14,6 +14,7 @@ import { TaskWidget } from '@/page-layout/widgets/tasks/components/TaskWidget';
|
||||
import { TimelineWidget } from '@/page-layout/widgets/timeline/components/TimelineWidget';
|
||||
import { WorkflowRunWidget } from '@/page-layout/widgets/workflow/components/WorkflowRunWidget';
|
||||
import { WorkflowVersionWidget } from '@/page-layout/widgets/workflow/components/WorkflowVersionWidget';
|
||||
import { RecordTableWidgetRenderer } from '@/page-layout/widgets/record-table/components/RecordTableWidgetRenderer';
|
||||
import { WorkflowWidget } from '@/page-layout/widgets/workflow/components/WorkflowWidget';
|
||||
import { WidgetType } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -73,6 +74,9 @@ export const WidgetContentRenderer = ({
|
||||
case WidgetType.FRONT_COMPONENT:
|
||||
return <FrontComponentWidgetRenderer widget={widget} />;
|
||||
|
||||
case WidgetType.RECORD_TABLE:
|
||||
return <RecordTableWidgetRenderer widget={widget} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { RecordTableWidgetRendererContent } from '@/page-layout/widgets/record-table/components/RecordTableWidgetRendererContent';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type RecordTableWidgetRendererProps = {
|
||||
widget: PageLayoutWidget;
|
||||
};
|
||||
|
||||
export const RecordTableWidgetRenderer = ({
|
||||
widget,
|
||||
}: RecordTableWidgetRendererProps) => {
|
||||
const { configuration } = widget;
|
||||
|
||||
const isRecordTableConfiguration =
|
||||
configuration.configurationType === WidgetConfigurationType.RECORD_TABLE;
|
||||
|
||||
const viewId =
|
||||
isRecordTableConfiguration && 'viewId' in configuration
|
||||
? (configuration.viewId as string | undefined)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(widget.objectMetadataId) || !isDefined(viewId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableWidgetRendererContent
|
||||
objectMetadataId={widget.objectMetadataId}
|
||||
viewId={viewId}
|
||||
widgetId={widget.id}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { RecordTableWidget } from '@/object-record/record-table-widget/components/RecordTableWidget';
|
||||
import { RecordTableWidgetProvider } from '@/object-record/record-table-widget/components/RecordTableWidgetProvider';
|
||||
|
||||
type RecordTableWidgetRendererContentProps = {
|
||||
objectMetadataId: string;
|
||||
viewId: string;
|
||||
widgetId: string;
|
||||
};
|
||||
|
||||
export const RecordTableWidgetRendererContent = ({
|
||||
objectMetadataId,
|
||||
viewId,
|
||||
widgetId,
|
||||
}: RecordTableWidgetRendererContentProps) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordTableWidgetProvider
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
viewId={viewId}
|
||||
widgetId={widgetId}
|
||||
>
|
||||
<RecordTableWidget />
|
||||
</RecordTableWidgetProvider>
|
||||
);
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { filterFieldsForRecordTableViewCreation } from '@/page-layout/widgets/record-table/utils/filterFieldsForRecordTableViewCreation';
|
||||
import { sortFieldsByRelevanceForRecordTableWidget } from '@/page-layout/widgets/record-table/utils/sortFieldsByRelevanceForRecordTableWidget';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/side-panel/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
import { ViewType } from '~/generated-metadata/graphql';
|
||||
|
||||
const DEFAULT_VIEW_FIELD_SIZE = 180;
|
||||
const INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET = 6;
|
||||
|
||||
export const useCreateViewForRecordTableWidget = (pageLayoutId: string) => {
|
||||
const { performViewAPICreate } = usePerformViewAPIPersist();
|
||||
const { performViewFieldAPICreate } = usePerformViewFieldAPIPersist();
|
||||
const { updateCurrentWidgetConfig } =
|
||||
useUpdateCurrentWidgetConfig(pageLayoutId);
|
||||
|
||||
const createViewForRecordTableWidget = useCallback(
|
||||
async (objectMetadataItem: EnrichedObjectMetadataItem) => {
|
||||
const newViewId = v4();
|
||||
|
||||
const viewResult = await performViewAPICreate(
|
||||
{
|
||||
input: {
|
||||
id: newViewId,
|
||||
name: `${objectMetadataItem.labelPlural} Table`,
|
||||
icon: objectMetadataItem.icon ?? 'IconTable',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
type: ViewType.TABLE,
|
||||
},
|
||||
},
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
if (viewResult.status !== 'successful') {
|
||||
return;
|
||||
}
|
||||
|
||||
const eligibleFields = objectMetadataItem.fields.filter(
|
||||
filterFieldsForRecordTableViewCreation,
|
||||
);
|
||||
|
||||
const sortedFields = eligibleFields.toSorted(
|
||||
sortFieldsByRelevanceForRecordTableWidget(
|
||||
objectMetadataItem.labelIdentifierFieldMetadataId,
|
||||
),
|
||||
);
|
||||
|
||||
const viewFieldInputs = sortedFields.map((field, index) => ({
|
||||
id: v4(),
|
||||
viewId: newViewId,
|
||||
fieldMetadataId: field.id,
|
||||
position: index,
|
||||
size: DEFAULT_VIEW_FIELD_SIZE,
|
||||
isVisible: index < INITIAL_VISIBLE_FIELDS_COUNT_IN_WIDGET,
|
||||
}));
|
||||
|
||||
try {
|
||||
await performViewFieldAPICreate({ inputs: viewFieldInputs });
|
||||
|
||||
updateCurrentWidgetConfig({
|
||||
configToUpdate: {
|
||||
viewId: newViewId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
'Failed to create view fields for record table widget',
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
performViewAPICreate,
|
||||
performViewFieldAPICreate,
|
||||
updateCurrentWidgetConfig,
|
||||
],
|
||||
);
|
||||
|
||||
return { createViewForRecordTableWidget };
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { usePerformViewAPIPersist } from '@/views/hooks/internal/usePerformViewAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useDeleteViewForRecordTableWidget = () => {
|
||||
const { performViewAPIDestroy } = usePerformViewAPIPersist();
|
||||
|
||||
const deleteViewForRecordTableWidget = useCallback(
|
||||
async (viewId: string) => {
|
||||
await performViewAPIDestroy({ id: viewId });
|
||||
},
|
||||
[performViewAPIDestroy],
|
||||
);
|
||||
|
||||
return { deleteViewForRecordTableWidget };
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { type RecordField } from '@/object-record/record-field/types/RecordField';
|
||||
import { viewFieldAggregateOperationState } from '@/object-record/record-table/record-table-footer/states/viewFieldAggregateOperationState';
|
||||
import { convertExtendedAggregateOperationToAggregateOperation } from '@/object-record/utils/convertExtendedAggregateOperationToAggregateOperation';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { mapRecordFieldToViewField } from '@/views/utils/mapRecordFieldToViewField';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useMapRecordFieldToViewFieldWithCurrentAggregateOperation = () => {
|
||||
const store = useStore();
|
||||
|
||||
const mapRecordFieldToViewFieldWithCurrentAggregateOperation = useCallback(
|
||||
(recordField: RecordField): Omit<ViewField, 'definition'> => {
|
||||
const viewField = mapRecordFieldToViewField(recordField);
|
||||
|
||||
const extendedAggregateOperation = store.get(
|
||||
viewFieldAggregateOperationState.atomFamily({
|
||||
viewFieldId: recordField.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
...viewField,
|
||||
aggregateOperation: isDefined(extendedAggregateOperation)
|
||||
? convertExtendedAggregateOperationToAggregateOperation(
|
||||
extendedAggregateOperation,
|
||||
)
|
||||
: viewField.aggregateOperation,
|
||||
};
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
return { mapRecordFieldToViewFieldWithCurrentAggregateOperation };
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { useGetFieldMetadataItemByIdOrThrow } from '@/object-metadata/hooks/useGetFieldMetadataItemById';
|
||||
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useMapViewFieldToRecordTableWidgetViewFieldItem = () => {
|
||||
const { getFieldMetadataItemByIdOrThrow } =
|
||||
useGetFieldMetadataItemByIdOrThrow();
|
||||
|
||||
const mapViewFieldToRecordTableWidgetViewFieldItem = useCallback(
|
||||
(viewField: ViewField): RecordTableWidgetViewFieldItem | null => {
|
||||
try {
|
||||
const { fieldMetadataItem } = getFieldMetadataItemByIdOrThrow(
|
||||
viewField.fieldMetadataId,
|
||||
);
|
||||
return { viewField, fieldMetadataItem };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[getFieldMetadataItemByIdOrThrow],
|
||||
);
|
||||
|
||||
return { mapViewFieldToRecordTableWidgetViewFieldItem };
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { useMapViewFieldToRecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/hooks/useMapViewFieldToRecordTableWidgetViewFieldItem';
|
||||
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
|
||||
import { useViewById } from '@/views/hooks/useViewById';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { sortByProperty } from '~/utils/array/sortByProperty';
|
||||
|
||||
export const useRecordTableWidgetViewFieldItems = (viewId: string) => {
|
||||
const { view } = useViewById(viewId);
|
||||
const { mapViewFieldToRecordTableWidgetViewFieldItem } =
|
||||
useMapViewFieldToRecordTableWidgetViewFieldItem();
|
||||
|
||||
const recordTableWidgetViewFieldItems: RecordTableWidgetViewFieldItem[] =
|
||||
useMemo(() => {
|
||||
if (!view) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return view.viewFields
|
||||
.toSorted(sortByProperty('position'))
|
||||
.map(mapViewFieldToRecordTableWidgetViewFieldItem)
|
||||
.filter(isDefined);
|
||||
}, [view, mapViewFieldToRecordTableWidgetViewFieldItem]);
|
||||
|
||||
return { recordTableWidgetViewFieldItems };
|
||||
};
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { type RecordTableWidgetViewFieldItem } from '@/page-layout/widgets/record-table/types/RecordTableWidgetViewFieldItem';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useReorderRecordTableWidgetFields = () => {
|
||||
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
|
||||
|
||||
const reorderRecordTableWidgetFields = useCallback(
|
||||
async (
|
||||
sourceIndex: number,
|
||||
destinationIndex: number,
|
||||
visibleFieldItems: RecordTableWidgetViewFieldItem[],
|
||||
) => {
|
||||
if (sourceIndex === destinationIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reorderedFields = [...visibleFieldItems];
|
||||
const [movedField] = reorderedFields.splice(sourceIndex, 1);
|
||||
reorderedFields.splice(destinationIndex, 0, movedField);
|
||||
|
||||
const updates = reorderedFields.map((fieldItem, index) => ({
|
||||
input: {
|
||||
id: fieldItem.viewField.id,
|
||||
update: { position: index },
|
||||
},
|
||||
}));
|
||||
|
||||
await performViewFieldAPIUpdate(updates);
|
||||
},
|
||||
[performViewFieldAPIUpdate],
|
||||
);
|
||||
|
||||
return { reorderRecordTableWidgetFields };
|
||||
};
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
|
||||
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
|
||||
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
|
||||
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
|
||||
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
|
||||
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
|
||||
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
|
||||
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
||||
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSaveRecordTableWidgetFiltersToView = (
|
||||
viewId: string,
|
||||
instanceId: string,
|
||||
) => {
|
||||
const {
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
} = usePerformViewFilterAPIPersist();
|
||||
|
||||
const {
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDelete,
|
||||
} = usePerformViewFilterGroupAPIPersist();
|
||||
|
||||
const currentRecordFiltersCallbackState = useAtomComponentStateCallbackState(
|
||||
currentRecordFiltersComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const currentRecordFilterGroupsCallbackState =
|
||||
useAtomComponentStateCallbackState(
|
||||
currentRecordFilterGroupsComponentState,
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetFiltersToView = useCallback(async () => {
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRecordFilters = store.get(currentRecordFiltersCallbackState);
|
||||
const currentRecordFilterGroups = store.get(
|
||||
currentRecordFilterGroupsCallbackState,
|
||||
);
|
||||
|
||||
const newViewFilterGroups = currentRecordFilterGroups.map(
|
||||
(recordFilterGroup) =>
|
||||
mapRecordFilterGroupToViewFilterGroup({
|
||||
recordFilterGroup,
|
||||
view: currentView,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentViewFilterGroups = currentView.viewFilterGroups ?? [];
|
||||
|
||||
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
|
||||
currentViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
|
||||
await performViewFilterGroupAPICreate(
|
||||
viewFilterGroupsToCreate,
|
||||
currentView,
|
||||
);
|
||||
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
|
||||
|
||||
const newViewFilters = currentRecordFilters.map(
|
||||
mapRecordFilterToViewFilter,
|
||||
);
|
||||
|
||||
const currentViewFilters = currentView.viewFilters ?? [];
|
||||
|
||||
const viewFiltersToCreate = getViewFiltersToCreate(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToUpdate = getViewFiltersToUpdate(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToDelete = getViewFiltersToDelete(
|
||||
currentViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
|
||||
await performViewFilterAPIDelete(
|
||||
viewFiltersToDelete.map((viewFilter) => ({
|
||||
input: { id: viewFilter.id },
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterAPIUpdate(
|
||||
viewFiltersToUpdate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
update: {
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterAPICreate(
|
||||
viewFiltersToCreate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewFilterGroupAPIDelete(
|
||||
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
|
||||
);
|
||||
}, [
|
||||
store,
|
||||
viewId,
|
||||
currentRecordFiltersCallbackState,
|
||||
currentRecordFilterGroupsCallbackState,
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDelete,
|
||||
]);
|
||||
|
||||
return { saveRecordTableWidgetFiltersToView };
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
|
||||
import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete';
|
||||
import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate';
|
||||
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useSaveRecordTableWidgetSortsToView = (viewId: string) => {
|
||||
const {
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
} = usePerformViewSortAPIPersist();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetSortsToView = useCallback(
|
||||
async (recordSorts: RecordSort[]) => {
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentViewSorts = currentView.viewSorts ?? [];
|
||||
const newViewSorts = recordSorts.map(mapRecordSortToViewSort);
|
||||
|
||||
const viewSortsToCreate = getViewSortsToCreate(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToUpdate = getViewSortsToUpdate(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToDelete = getViewSortsToDelete(
|
||||
currentViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
|
||||
await performViewSortAPICreate(
|
||||
viewSortsToCreate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewSortAPIUpdate(
|
||||
viewSortsToUpdate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
update: {
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
await performViewSortAPIDelete(
|
||||
viewSortsToDelete.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
},
|
||||
})),
|
||||
);
|
||||
},
|
||||
[
|
||||
store,
|
||||
viewId,
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
],
|
||||
);
|
||||
|
||||
return { saveRecordTableWidgetSortsToView };
|
||||
};
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { useMapRecordFieldToViewFieldWithCurrentAggregateOperation } from '@/page-layout/widgets/record-table/hooks/useMapRecordFieldToViewFieldWithCurrentAggregateOperation';
|
||||
import { computeViewFieldsToCreateAndUpdate } from '@/page-layout/widgets/record-table/utils/computeViewFieldsToCreateAndUpdate';
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { usePerformViewFilterAPIPersist } from '@/views/hooks/internal/usePerformViewFilterAPIPersist';
|
||||
import { usePerformViewFilterGroupAPIPersist } from '@/views/hooks/internal/usePerformViewFilterGroupAPIPersist';
|
||||
import { usePerformViewSortAPIPersist } from '@/views/hooks/internal/usePerformViewSortAPIPersist';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { getViewFilterGroupsToCreate } from '@/views/utils/getViewFilterGroupsToCreate';
|
||||
import { getViewFilterGroupsToDelete } from '@/views/utils/getViewFilterGroupsToDelete';
|
||||
import { getViewFilterGroupsToUpdate } from '@/views/utils/getViewFilterGroupsToUpdate';
|
||||
import { getViewFiltersToCreate } from '@/views/utils/getViewFiltersToCreate';
|
||||
import { getViewFiltersToDelete } from '@/views/utils/getViewFiltersToDelete';
|
||||
import { getViewFiltersToUpdate } from '@/views/utils/getViewFiltersToUpdate';
|
||||
import { getViewSortsToCreate } from '@/views/utils/getViewSortsToCreate';
|
||||
import { getViewSortsToDelete } from '@/views/utils/getViewSortsToDelete';
|
||||
import { getViewSortsToUpdate } from '@/views/utils/getViewSortsToUpdate';
|
||||
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
|
||||
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
|
||||
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { WidgetConfigurationType } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSaveRecordTableWidgetsViewDataOnDashboardSave = () => {
|
||||
const {
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
} = usePerformViewFilterAPIPersist();
|
||||
|
||||
const {
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDelete,
|
||||
} = usePerformViewFilterGroupAPIPersist();
|
||||
|
||||
const {
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
} = usePerformViewSortAPIPersist();
|
||||
|
||||
const { performViewFieldAPICreate, performViewFieldAPIUpdate } =
|
||||
usePerformViewFieldAPIPersist();
|
||||
|
||||
const { mapRecordFieldToViewFieldWithCurrentAggregateOperation } =
|
||||
useMapRecordFieldToViewFieldWithCurrentAggregateOperation();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveRecordTableWidgetsViewDataOnDashboardSave = useCallback(
|
||||
async (pageLayoutId: string) => {
|
||||
const pageLayoutDraft = store.get(
|
||||
pageLayoutDraftComponentState.atomFamily({
|
||||
instanceId: pageLayoutId,
|
||||
}),
|
||||
);
|
||||
const views = store.get(viewsSelector.atom);
|
||||
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
|
||||
|
||||
const recordTableWidgets = pageLayoutDraft.tabs.flatMap((tab) =>
|
||||
tab.widgets.filter(
|
||||
(widget) =>
|
||||
widget.configuration.configurationType ===
|
||||
WidgetConfigurationType.RECORD_TABLE,
|
||||
),
|
||||
);
|
||||
|
||||
for (const widget of recordTableWidgets) {
|
||||
const configuration = widget.configuration;
|
||||
|
||||
if (!('viewId' in configuration) || !isDefined(configuration.viewId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const viewId = configuration.viewId as string;
|
||||
const currentView = views.find((view) => view.id === viewId);
|
||||
|
||||
if (!isDefined(currentView) || !isDefined(widget.objectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === widget.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const recordIndexId = getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
viewId,
|
||||
);
|
||||
|
||||
const currentRecordFilters = store.get(
|
||||
currentRecordFiltersComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentRecordFilterGroups = store.get(
|
||||
currentRecordFilterGroupsComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const currentRecordSorts = store.get(
|
||||
currentRecordSortsComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const newViewFilterGroups = currentRecordFilterGroups.map(
|
||||
(recordFilterGroup) =>
|
||||
mapRecordFilterGroupToViewFilterGroup({
|
||||
recordFilterGroup,
|
||||
view: currentView,
|
||||
}),
|
||||
);
|
||||
|
||||
const existingViewFilterGroups = currentView.viewFilterGroups ?? [];
|
||||
|
||||
const viewFilterGroupsToCreate = getViewFilterGroupsToCreate(
|
||||
existingViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToUpdate = getViewFilterGroupsToUpdate(
|
||||
existingViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
const viewFilterGroupsToDelete = getViewFilterGroupsToDelete(
|
||||
existingViewFilterGroups,
|
||||
newViewFilterGroups,
|
||||
);
|
||||
|
||||
const newViewFilters = currentRecordFilters.map(
|
||||
mapRecordFilterToViewFilter,
|
||||
);
|
||||
const existingViewFilters = currentView.viewFilters ?? [];
|
||||
|
||||
const viewFiltersToCreate = getViewFiltersToCreate(
|
||||
existingViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToUpdate = getViewFiltersToUpdate(
|
||||
existingViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
const viewFiltersToDelete = getViewFiltersToDelete(
|
||||
existingViewFilters,
|
||||
newViewFilters,
|
||||
);
|
||||
|
||||
const newViewSorts = currentRecordSorts.map(mapRecordSortToViewSort);
|
||||
const existingViewSorts = currentView.viewSorts ?? [];
|
||||
|
||||
const viewSortsToCreate = getViewSortsToCreate(
|
||||
existingViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToUpdate = getViewSortsToUpdate(
|
||||
existingViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
const viewSortsToDelete = getViewSortsToDelete(
|
||||
existingViewSorts,
|
||||
newViewSorts,
|
||||
);
|
||||
|
||||
const currentRecordFields = store.get(
|
||||
currentRecordFieldsComponentState.atomFamily({
|
||||
instanceId: recordIndexId,
|
||||
}),
|
||||
);
|
||||
|
||||
const newViewFields = currentRecordFields.map(
|
||||
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
|
||||
);
|
||||
|
||||
const { viewFieldsToCreate, viewFieldsToUpdate } =
|
||||
computeViewFieldsToCreateAndUpdate({
|
||||
newViewFields,
|
||||
existingViewFields: currentView.viewFields ?? [],
|
||||
viewId,
|
||||
});
|
||||
|
||||
const hasNoRecordTableWidgetChanges =
|
||||
viewFilterGroupsToCreate.length === 0 &&
|
||||
viewFilterGroupsToUpdate.length === 0 &&
|
||||
viewFilterGroupsToDelete.length === 0 &&
|
||||
viewFiltersToCreate.length === 0 &&
|
||||
viewFiltersToUpdate.length === 0 &&
|
||||
viewFiltersToDelete.length === 0 &&
|
||||
viewSortsToCreate.length === 0 &&
|
||||
viewSortsToUpdate.length === 0 &&
|
||||
viewSortsToDelete.length === 0 &&
|
||||
viewFieldsToCreate.length === 0 &&
|
||||
viewFieldsToUpdate.length === 0;
|
||||
|
||||
if (hasNoRecordTableWidgetChanges) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await performViewFilterGroupAPICreate(
|
||||
viewFilterGroupsToCreate,
|
||||
currentView,
|
||||
);
|
||||
await performViewFilterGroupAPIUpdate(viewFilterGroupsToUpdate);
|
||||
|
||||
await performViewFilterAPIDelete(
|
||||
viewFiltersToDelete.map((viewFilter) => ({
|
||||
input: { id: viewFilter.id },
|
||||
})),
|
||||
);
|
||||
await performViewFilterAPIUpdate(
|
||||
viewFiltersToUpdate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
update: {
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewFilterAPICreate(
|
||||
viewFiltersToCreate.map((viewFilter) => ({
|
||||
input: {
|
||||
id: viewFilter.id,
|
||||
fieldMetadataId: viewFilter.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
value: viewFilter.value,
|
||||
operand: viewFilter.operand,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
subFieldName: viewFilter.subFieldName ?? null,
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewFilterGroupAPIDelete(
|
||||
viewFilterGroupsToDelete.map((viewFilterGroup) => viewFilterGroup.id),
|
||||
);
|
||||
|
||||
await performViewSortAPICreate(
|
||||
viewSortsToCreate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
fieldMetadataId: viewSort.fieldMetadataId,
|
||||
viewId: currentView.id,
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewSortAPIUpdate(
|
||||
viewSortsToUpdate.map((viewSort) => ({
|
||||
input: {
|
||||
id: viewSort.id,
|
||||
update: {
|
||||
direction: viewSort.direction,
|
||||
},
|
||||
},
|
||||
})),
|
||||
);
|
||||
await performViewSortAPIDelete(
|
||||
viewSortsToDelete.map((viewSort) => ({
|
||||
input: { id: viewSort.id },
|
||||
})),
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
performViewFieldAPICreate({ inputs: viewFieldsToCreate }),
|
||||
performViewFieldAPIUpdate(viewFieldsToUpdate),
|
||||
]);
|
||||
}
|
||||
},
|
||||
[
|
||||
store,
|
||||
mapRecordFieldToViewFieldWithCurrentAggregateOperation,
|
||||
performViewFieldAPICreate,
|
||||
performViewFieldAPIUpdate,
|
||||
performViewFilterAPICreate,
|
||||
performViewFilterAPIUpdate,
|
||||
performViewFilterAPIDelete,
|
||||
performViewFilterGroupAPICreate,
|
||||
performViewFilterGroupAPIUpdate,
|
||||
performViewFilterGroupAPIDelete,
|
||||
performViewSortAPICreate,
|
||||
performViewSortAPIUpdate,
|
||||
performViewSortAPIDelete,
|
||||
],
|
||||
);
|
||||
|
||||
return { saveRecordTableWidgetsViewDataOnDashboardSave };
|
||||
};
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { usePerformViewFieldAPIPersist } from '@/views/hooks/internal/usePerformViewFieldAPIPersist';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useToggleRecordTableWidgetFieldVisibility = () => {
|
||||
const { performViewFieldAPIUpdate } = usePerformViewFieldAPIPersist();
|
||||
|
||||
const toggleRecordTableWidgetFieldVisibility = useCallback(
|
||||
async (viewFieldId: string, isVisible: boolean) => {
|
||||
await performViewFieldAPIUpdate([
|
||||
{
|
||||
input: {
|
||||
id: viewFieldId,
|
||||
update: { isVisible },
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
[performViewFieldAPIUpdate],
|
||||
);
|
||||
|
||||
return { toggleRecordTableWidgetFieldVisibility };
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
|
||||
export type RecordTableWidgetViewFieldItem = {
|
||||
viewField: ViewField;
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user