Fix - Not shared message on record index (#19103)

quality-feedbacks :
https://discord.com/channels/1130383047699738754/1486711185436053514/1486711185436053514
<img width="1000" height="500" alt="Screenshot 2026-03-30 at 09 42 48"
src="https://github.com/user-attachments/assets/fe9027fe-0056-4fec-af51-5b39bb467bb5"
/>
<img width="1000" height="500" alt="Screenshot 2026-03-30 at 09 42 34"
src="https://github.com/user-attachments/assets/8cd05ea0-7eb2-4490-b87e-3a7dcd57add2"
/>
This commit is contained in:
Etienne
2026-03-30 09:30:55 +00:00
committed by GitHub
parent 4be934a192
commit 994180c72e
15 changed files with 278 additions and 30 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -12,7 +12,9 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexCalendarContainer } from '@/object-record/record-index/components/RecordIndexCalendarContainer';
import { RecordIndexEmptyStateNotShared } from '@/object-record/record-index/components/RecordIndexEmptyStateNotShared';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { useHasCurrentViewNonReadableFields } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -43,12 +45,16 @@ export const RecordIndexContainer = () => {
objectNameSingular,
} = useRecordIndexContextOrThrow();
const { hasCurrentViewNonReadableFields, nonReadableViewFieldInfo } =
useHasCurrentViewNonReadableFields(objectMetadataItem);
return (
<>
<StyledContainer>
<InformationBannerWrapper />
<SpreadsheetImportProvider>
<ViewBar
isReadOnly={hasCurrentViewNonReadableFields}
viewBarId={recordIndexId}
optionsDropdownButton={
<ObjectOptionsDropdown
@@ -63,29 +69,35 @@ export const RecordIndexContainer = () => {
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.TABLE && (
{hasCurrentViewNonReadableFields ? (
<RecordIndexEmptyStateNotShared
nonReadableViewFieldInfo={nonReadableViewFieldInfo}
/>
) : (
<>
<RecordIndexTableContainer recordTableId={recordIndexId} />
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.TABLE && (
<RecordIndexTableContainer recordTableId={recordIndexId} />
)}
{recordIndexViewType === ViewType.KANBAN && (
<StyledContainerWithPadding>
<RecordBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
</StyledContainerWithPadding>
)}
{recordIndexViewType === ViewType.CALENDAR && (
<StyledContainerWithPadding>
<RecordIndexCalendarContainer
recordCalendarInstanceId={recordIndexId}
viewBarInstanceId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</>
)}
{recordIndexViewType === ViewType.KANBAN && (
<StyledContainerWithPadding>
<RecordBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
</StyledContainerWithPadding>
)}
{recordIndexViewType === ViewType.CALENDAR && (
<StyledContainerWithPadding>
<RecordIndexCalendarContainer
recordCalendarInstanceId={recordIndexId}
viewBarInstanceId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</StyledContainer>
</>
);
@@ -1,7 +1,7 @@
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { getCommandMenuIdFromRecordIndexId } from '@/command-menu-item/utils/getCommandMenuIdFromRecordIndexId';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
@@ -9,6 +9,7 @@ import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexEmptyStateNotShared } from '@/object-record/record-index/components/RecordIndexEmptyStateNotShared';
import { RecordIndexLoadBaseOnContextStoreEffect } from '@/object-record/record-index/components/RecordIndexLoadBaseOnContextStoreEffect';
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
@@ -18,9 +19,8 @@ import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-sel
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { styled } from '@linaria/react';
import { useCallback } from 'react';
import { NotFound } from '~/pages/not-found/NotFound';
import { useStore } from 'jotai';
import { useCallback } from 'react';
const StyledIndexContainer = styled.div`
display: flex;
@@ -60,10 +60,6 @@ export const RecordIndexContainerGater = () => {
recordIndexId,
);
if (!hasObjectReadPermissions) {
return <NotFound />;
}
return (
<>
<RecordIndexContextProvider
@@ -99,8 +95,14 @@ export const RecordIndexContainerGater = () => {
<StyledIndexContainer
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
>
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<RecordIndexContainer />
{hasObjectReadPermissions ? (
<>
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<RecordIndexContainer />
</>
) : (
<RecordIndexEmptyStateNotShared />
)}
</StyledIndexContainer>
</MainContainerLayoutWithSidePanel>
</CommandMenuComponentInstanceContext.Provider>
@@ -0,0 +1,45 @@
import { type NonReadableViewFieldInfo } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
import { getNonReadableViewFieldSubTitle } from '@/object-record/record-index/utils/getNonReadableViewFieldSubTitle';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptySubTitle,
AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle,
} from 'twenty-ui/layout';
const StyledEmptyPlaceholderOuterContainer = styled.div`
height: 100%;
width: 100%;
`;
type RecordIndexEmptyStateNotSharedProps = {
nonReadableViewFieldInfo?: NonReadableViewFieldInfo;
};
export const RecordIndexEmptyStateNotShared = ({
nonReadableViewFieldInfo,
}: RecordIndexEmptyStateNotSharedProps) => {
return (
<StyledEmptyPlaceholderOuterContainer>
<AnimatedPlaceholderEmptyContainer>
<AnimatedPlaceholder type="notShared" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{isDefined(nonReadableViewFieldInfo)
? t`View not shared`
: t`Object not shared`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
{isDefined(nonReadableViewFieldInfo)
? getNonReadableViewFieldSubTitle(nonReadableViewFieldInfo)
: t`You don't have access to this object.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
</StyledEmptyPlaceholderOuterContainer>
);
};
@@ -0,0 +1,86 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { getNonReadableFieldInfoForViewField } from '@/object-record/record-index/utils/getNonReadableInfoForViewField';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
export type NonReadableViewFieldUsage = 'filter' | 'sort';
export type NonReadableViewFieldInfo = {
fieldLabel?: string;
objectLabel: string;
usage: NonReadableViewFieldUsage;
};
export const useHasCurrentViewNonReadableFields = (
objectMetadataItem: EnrichedObjectMetadataItem,
) => {
const contextStoreCurrentViewId = useAtomComponentStateValue(
contextStoreCurrentViewIdComponentState,
);
const view = useAtomFamilySelectorValue(viewFromViewIdFamilySelector, {
viewId: contextStoreCurrentViewId ?? '',
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const nonReadableViewFieldInfo = useMemo(():
| NonReadableViewFieldInfo
| undefined => {
if (!isDefined(view)) {
return undefined;
}
const readableFieldIds = new Set(
objectMetadataItem.readableFields.map((field) => field.id),
);
const params = {
objectMetadataItem,
objectMetadataItems,
readableFieldIds,
objectPermissionsByObjectMetadataId,
};
for (const sort of view.viewSorts) {
const info = getNonReadableFieldInfoForViewField({
fieldMetadataId: sort.fieldMetadataId,
...params,
});
if (isDefined(info)) {
return { ...info, usage: 'sort' };
}
}
for (const filter of view.viewFilters) {
const info = getNonReadableFieldInfoForViewField({
fieldMetadataId: filter.fieldMetadataId,
...params,
});
if (isDefined(info)) {
return { ...info, usage: 'filter' };
}
}
return undefined;
}, [
view,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
]);
return {
hasCurrentViewNonReadableFields: isDefined(nonReadableViewFieldInfo),
nonReadableViewFieldInfo,
};
};
@@ -0,0 +1,83 @@
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const getNonReadableFieldInfoForViewField = ({
fieldMetadataId,
objectMetadataItem,
objectMetadataItems,
readableFieldIds,
objectPermissionsByObjectMetadataId,
}: {
fieldMetadataId: string;
objectMetadataItem: EnrichedObjectMetadataItem;
objectMetadataItems: EnrichedObjectMetadataItem[];
readableFieldIds: Set<string>;
objectPermissionsByObjectMetadataId: Record<
string,
ObjectPermissions & { objectMetadataId: string }
>;
}): { fieldLabel?: string; objectLabel: string } | undefined => {
const { fieldMetadataItem } = getFieldMetadataItemById({
fieldMetadataId,
objectMetadataItems,
});
if (!isDefined(fieldMetadataItem)) {
return;
}
if (!readableFieldIds.has(fieldMetadataId)) {
return {
fieldLabel: fieldMetadataItem.label,
objectLabel: objectMetadataItem.labelSingular,
};
}
if (fieldMetadataItem.type !== FieldMetadataType.RELATION) {
return;
}
const targetObjectMetadataItem = objectMetadataItems.find(
(item) =>
item.nameSingular ===
fieldMetadataItem.relation?.targetObjectMetadata.nameSingular,
);
if (!isDefined(targetObjectMetadataItem)) {
return;
}
const targetObjectPermissions = getObjectPermissionsForObject(
objectPermissionsByObjectMetadataId,
targetObjectMetadataItem.id,
);
if (!targetObjectPermissions.canReadObjectRecords) {
return {
objectLabel: targetObjectMetadataItem.labelSingular,
};
}
const labelIdentifierFieldId =
targetObjectMetadataItem.labelIdentifierFieldMetadataId;
const labelIdentifierField = targetObjectMetadataItem.fields.find(
(field) => field.id === labelIdentifierFieldId,
);
const isLabelIdentifierReadable =
targetObjectMetadataItem.readableFields.some(
(field) => field.id === labelIdentifierFieldId,
);
if (!isLabelIdentifierReadable) {
return {
fieldLabel: labelIdentifierField?.label,
objectLabel: targetObjectMetadataItem.labelSingular,
};
}
};
@@ -0,0 +1,16 @@
import { type NonReadableViewFieldInfo } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
export const getNonReadableViewFieldSubTitle = (
nonReadableViewFieldInfo: NonReadableViewFieldInfo,
): string => {
const usageLabel =
nonReadableViewFieldInfo.usage === 'sort' ? t`sorting` : t`filtering`;
if (isDefined(nonReadableViewFieldInfo.fieldLabel)) {
return t`This view uses ${usageLabel} on field "${nonReadableViewFieldInfo.fieldLabel}" on "${nonReadableViewFieldInfo.objectLabel}" which is not accessible.`;
}
return t`This view uses ${usageLabel} on object "${nonReadableViewFieldInfo.objectLabel}" which is not accessible.`;
};
@@ -1,7 +1,7 @@
import { type ReactNode } from 'react';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { TopBar } from '@/ui/layout/top-bar/components/TopBar';
import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect';
import { QueryParamsSortsEffect } from '@/views/components/QueryParamsSortsEffect';
@@ -14,4 +14,5 @@ export const BACKGROUND: Record<string, string> = {
error500: '/images/placeholders/background/500_bg.png',
noDeletedRecord: '/images/placeholders/background/no_deleted_record_bg.png',
noWidgets: '/images/placeholders/background/no_widgets_bg.png',
notShared: '/images/placeholders/background/not_shared_bg.png',
};
@@ -15,4 +15,5 @@ export const DARK_BACKGROUND: Record<string, string> = {
noDeletedRecord:
'/images/placeholders/dark-background/no_deleted_record_bg.png',
noWidgets: '/images/placeholders/dark-background/no_widgets_bg.png',
notShared: '/images/placeholders/dark-background/not_shared_bg.png',
};
@@ -15,4 +15,5 @@ export const DARK_MOVING_IMAGE: Record<string, string> = {
noDeletedRecord:
'/images/placeholders/dark-moving-image/no_deleted_record.png',
noWidgets: '/images/placeholders/dark-moving-image/no_widgets.png',
notShared: '/images/placeholders/dark-moving-image/not_shared.png',
};
@@ -14,4 +14,5 @@ export const MOVING_IMAGE: Record<string, string> = {
error500: '/images/placeholders/moving-image/500.png',
noDeletedRecord: '/images/placeholders/moving-image/no_deleted_record.png',
noWidgets: '/images/placeholders/moving-image/no_widgets.png',
notShared: '/images/placeholders/moving-image/not_shared.png',
};