Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5ebe7a757 |
+2
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
+13
-7
@@ -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 />;
|
||||
|
||||
-25
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-44
@@ -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 = {};
|
||||
+26
-2
@@ -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,
|
||||
|
||||
+50
-4
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+20
-11
@@ -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` } : {}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user