Compare commits

...
Author SHA1 Message Date
Thomas Trompette c5ebe7a757 WIP 2026-03-26 17:39:37 +01:00
7 changed files with 111 additions and 93 deletions
@@ -25,6 +25,7 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
loading,
totalCount,
fetchMoreRecords,
error,
} = useFindManyRecords({
...params,
recordGqlFields,
@@ -38,5 +39,6 @@ export const useRecordIndexTableQuery = (objectNameSingular: string) => {
queryIdentifier,
totalCount,
fetchMoreRecords,
error,
};
};
@@ -1,11 +1,12 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateAccessDenied } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateAccessDenied';
import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateReadOnly } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateReadOnly';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { isRecordTableAccessDeniedComponentState } from '@/object-record/record-table/states/isRecordTableAccessDeniedComponentState';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -13,10 +14,12 @@ export const RecordTableEmptyState = () => {
const { recordTableId, objectNameSingular, objectMetadataItem } =
useRecordTableContextOrThrow();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;
const isRecordTableAccessDenied = useAtomComponentStateValue(
isRecordTableAccessDeniedComponentState,
recordTableId,
);
const isRemote = objectMetadataItem.isRemote;
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const isSoftDeleteFilterActive = useAtomComponentStateValue(
isSoftDeleteFilterActiveComponentState,
@@ -27,15 +30,18 @@ export const RecordTableEmptyState = () => {
objectMetadataItem.id,
);
if (isRecordTableAccessDenied) {
return <RecordTableEmptyStateAccessDenied />;
}
const noRecordAtAll = totalCount === 0;
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
if (!hasObjectUpdatePermissions) {
return <RecordTableEmptyStateReadOnly />;
}
if (isRemote) {
return <RecordTableEmptyStateRemote />;
} else if (isSoftDeleteFilterActive === true) {
if (isSoftDeleteFilterActive === true) {
return <RecordTableEmptyStateSoftDelete />;
} else if (noRecordAtAll) {
return <RecordTableEmptyStateNoGroupNoRecordAtAll />;
@@ -1,25 +0,0 @@
/* oxlint-disable twenty/no-navigate-prefer-link */
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { IconSettings } from 'twenty-ui/display';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const RecordTableEmptyStateRemote = () => {
const navigate = useNavigateSettings();
const handleButtonClick = () => {
navigate(SettingsPath.Integrations);
};
return (
<RecordTableEmptyStateDisplay
buttonTitle={t`Go to Settings`}
subTitle={t`If this is unexpected, please verify your settings.`}
title={t`No Data Available for Remote Table`}
ButtonIcon={IconSettings}
animatedPlaceholderType="noRecord"
onClick={handleButtonClick}
/>
);
};
@@ -1,44 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ScrollWrapperDecorator } from '~/testing/decorators/ScrollWrapperDecorator';
import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { ComponentDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { RecordTableDecorator } from '~/testing/decorators/RecordTableDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title: 'Modules/ObjectRecord/RecordTable/RecordTableEmptyStateRemote',
component: RecordTableEmptyStateRemote,
decorators: [
(Story) => (
<RecordTableContextProvider
recordTableId="persons"
viewBarId="view-bar"
objectNameSingular="person"
>
<Story />
</RecordTableContextProvider>
),
ComponentDecorator,
MemoryRouterDecorator,
RecordTableDecorator,
ContextStoreDecorator,
SnackBarDecorator,
ObjectMetadataItemsDecorator,
ScrollWrapperDecorator,
],
parameters: {
recordTableObjectNameSingular: 'person',
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RecordTableEmptyStateRemote>;
export const Default: Story = {};
@@ -1,4 +1,7 @@
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isNonEmptyString } from '@sniptt/guards';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -10,10 +13,10 @@ import { RECORD_TABLE_ROW_HEIGHT } from '@/object-record/record-table/constants/
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useSetRecordTableData } from '@/object-record/record-table/hooks/internal/useSetRecordTableData';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isRecordTableAccessDeniedComponentState } from '@/object-record/record-table/states/isRecordTableAccessDeniedComponentState';
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { isNonEmptyString } from '@sniptt/guards';
export const RecordTableRecordGroupBodyEffect = () => {
const { objectNameSingular } = useRecordTableContextOrThrow();
@@ -27,9 +30,14 @@ export const RecordTableRecordGroupBodyEffect = () => {
isRecordTableInitialLoadingComponentState,
);
const setIsAccessDenied = useSetAtomComponentState(
isRecordTableAccessDeniedComponentState,
recordTableId,
);
const recordGroupId = useCurrentRecordGroupId();
const { records, loading, hasNextPage } =
const { records, loading, hasNextPage, error } =
useRecordIndexTableQuery(objectNameSingular);
const setRecordIndexHasFetchedAllRecordsByGroup =
@@ -43,7 +51,21 @@ export const RecordTableRecordGroupBodyEffect = () => {
const { scrollToPosition } = useScrollToPosition();
useEffect(() => {
if (isDefined(error) && CombinedGraphQLErrors.is(error)) {
const isForbidden = error.errors.some(
(graphqlError) => graphqlError.extensions?.code === 'FORBIDDEN',
);
if (isForbidden) {
setIsAccessDenied(true);
setIsRecordTableInitialLoading(false);
return;
}
}
if (!loading) {
setIsAccessDenied(false);
setRecordTableData({
records,
currentRecordGroupId: recordGroupId,
@@ -52,10 +74,12 @@ export const RecordTableRecordGroupBodyEffect = () => {
setRecordIndexHasFetchedAllRecordsByGroup(!hasNextPage);
}
}, [
error,
hasNextPage,
loading,
records,
recordGroupId,
setIsAccessDenied,
setRecordIndexHasFetchedAllRecordsByGroup,
setIsRecordTableInitialLoading,
setRecordTableData,
@@ -1,5 +1,8 @@
import { type ErrorLike } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useCallback } from 'react';
import { useStore } from 'jotai';
import { isDefined } from 'twenty-shared/utils';
import { useRecordIndexTableLazyQuery } from '@/object-record/record-index/hooks/useRecordIndexTableLazyQuery';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
@@ -11,10 +14,10 @@ import { useScrollTableToPosition } from '@/object-record/record-table/hooks/use
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { isRecordTableScrolledHorizontallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledHorizontallyComponentState';
import { isRecordTableScrolledVerticallyComponentState } from '@/object-record/record-table/states/isRecordTableScrolledVerticallyComponentState';
import { isRecordTableAccessDeniedComponentState } from '@/object-record/record-table/states/isRecordTableAccessDeniedComponentState';
import { updateRecordTableCSSVariable } from '@/object-record/record-table/utils/updateRecordTableCSSVariable';
import { useLoadRecordsToVirtualRows } from '@/object-record/record-table/virtualization/hooks/useLoadRecordsToVirtualRows';
import { useReapplyRowSelection } from '@/object-record/record-table/virtualization/hooks/useReapplyRowSelection';
import { useResetTableFocuses } from '@/object-record/record-table/virtualization/hooks/useResetTableFocuses';
import { useResetVirtualizedRowTreadmill } from '@/object-record/record-table/virtualization/hooks/useResetVirtualizedRowTreadmill';
import { dataPagesLoadedComponentState } from '@/object-record/record-table/virtualization/states/dataPagesLoadedComponentState';
@@ -31,7 +34,16 @@ import { dataLoadingStatusByRealIndexComponentState } from '@/object-record/reco
import { recordIdByRealIndexComponentState } from '@/object-record/record-table/virtualization/states/recordIdByRealIndexComponentState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
import { isDefined } from 'twenty-shared/utils';
const isForbiddenError = (error: ErrorLike): boolean => {
if (!CombinedGraphQLErrors.is(error)) {
return false;
}
return error.errors.some(
(graphqlError) => graphqlError.extensions?.code === 'FORBIDDEN',
);
};
export const useTriggerInitialRecordTableDataLoad = () => {
const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
@@ -107,6 +119,11 @@ export const useTriggerInitialRecordTableDataLoad = () => {
totalNumberOfRecordsToVirtualizeComponentState,
);
const isRecordTableAccessDenied = useAtomComponentStateCallbackState(
isRecordTableAccessDeniedComponentState,
recordTableId,
);
const triggerInitialRecordTableDataLoad = useCallback(
async ({
shouldScrollToStart = true,
@@ -120,6 +137,7 @@ export const useTriggerInitialRecordTableDataLoad = () => {
}
store.set(isInitializingVirtualTableDataLoadingCallbackState, true);
store.set(isRecordTableAccessDenied, false);
try {
resetTableFocuses();
@@ -165,8 +183,23 @@ export const useTriggerInitialRecordTableDataLoad = () => {
newDataLoadingStatusByRealIndex,
);
const { records: findManyRecords, totalCount: findManyTotalCount } =
await findManyRecordsLazy();
const {
records: findManyRecords,
totalCount: findManyTotalCount,
error: queryError,
} = await findManyRecordsLazy();
if (isDefined(queryError) && isForbiddenError(queryError)) {
store.set(isRecordTableAccessDenied, true);
store.set(totalNumberOfRecordsToVirtualizeCallbackState, 0);
store.set(recordIdByRealIndexCallbackState, new Map());
store.set(
dataLoadingStatusByRealIndexCallbackState,
new Map(),
);
return;
}
records = findManyRecords;
totalCount = findManyTotalCount;
@@ -201,6 +234,18 @@ export const useTriggerInitialRecordTableDataLoad = () => {
verticalScrollInPx: 0,
});
}
} catch (error) {
if (
isDefined(error) &&
typeof error === 'object' &&
'name' in error &&
isForbiddenError(error as ErrorLike)
) {
store.set(isRecordTableAccessDenied, true);
}
store.set(totalNumberOfRecordsToVirtualizeCallbackState, 0);
store.set(recordIdByRealIndexCallbackState, new Map());
store.set(dataLoadingStatusByRealIndexCallbackState, new Map());
} finally {
store.set(isInitializingVirtualTableDataLoadingCallbackState, false);
store.set(isRecordTableInitialLoading, false);
@@ -229,6 +274,7 @@ export const useTriggerInitialRecordTableDataLoad = () => {
loadRecordsToVirtualRows,
reapplyRowSelection,
recordTableId,
isRecordTableAccessDenied,
],
);
@@ -92,17 +92,26 @@ export const useComputeRecordRelationFilterLabelValue = ({
? `${labelValueItems.length} ${relationObjectLabelPlural.toLowerCase()}`
: labelValueItems.join(', ');
if (labelValueItems.length > 0) {
return {
labelValue: getRecordFilterChipLabelValue({
recordFilter: {
...recordFilter,
displayValue: filterDisplayValue,
},
}),
};
}
const hasUnresolvedRecords =
selectedRecordIds.length > 0 && selectedRecords.length === 0;
return {
labelValue:
labelValueItems.length > 0
? getRecordFilterChipLabelValue({
recordFilter: {
...recordFilter,
displayValue: filterDisplayValue,
},
})
: getRecordFilterChipLabelValue({
recordFilter,
}),
labelValue: getRecordFilterChipLabelValue({
recordFilter: {
...recordFilter,
...(hasUnresolvedRecords ? { displayValue: t`Not found` } : {}),
},
}),
};
};