diff --git a/packages/twenty-front/public/images/placeholders/background/not_shared_bg.png b/packages/twenty-front/public/images/placeholders/background/not_shared_bg.png
new file mode 100644
index 00000000000..6c3d400c7e0
Binary files /dev/null and b/packages/twenty-front/public/images/placeholders/background/not_shared_bg.png differ
diff --git a/packages/twenty-front/public/images/placeholders/dark-background/not_shared_bg.png b/packages/twenty-front/public/images/placeholders/dark-background/not_shared_bg.png
new file mode 100644
index 00000000000..de52d1a895f
Binary files /dev/null and b/packages/twenty-front/public/images/placeholders/dark-background/not_shared_bg.png differ
diff --git a/packages/twenty-front/public/images/placeholders/dark-moving-image/not_shared.png b/packages/twenty-front/public/images/placeholders/dark-moving-image/not_shared.png
new file mode 100644
index 00000000000..ac90c966e1f
Binary files /dev/null and b/packages/twenty-front/public/images/placeholders/dark-moving-image/not_shared.png differ
diff --git a/packages/twenty-front/public/images/placeholders/moving-image/not_shared.png b/packages/twenty-front/public/images/placeholders/moving-image/not_shared.png
new file mode 100644
index 00000000000..26607bb78a2
Binary files /dev/null and b/packages/twenty-front/public/images/placeholders/moving-image/not_shared.png differ
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
index 23700f0f79f..d69c3733741 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
@@ -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 (
<>
{
viewBarId={recordIndexId}
/>
-
- {recordIndexViewType === ViewType.TABLE && (
+ {hasCurrentViewNonReadableFields ? (
+
+ ) : (
<>
-
+
+ {recordIndexViewType === ViewType.TABLE && (
+
+ )}
+ {recordIndexViewType === ViewType.KANBAN && (
+
+
+
+ )}
+ {recordIndexViewType === ViewType.CALENDAR && (
+
+
+
+ )}
>
)}
- {recordIndexViewType === ViewType.KANBAN && (
-
-
-
- )}
- {recordIndexViewType === ViewType.CALENDAR && (
-
-
-
- )}
>
);
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx
index 05876aab0b2..9c6956ab67c 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx
@@ -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 ;
- }
-
return (
<>
{
-
-
+ {hasObjectReadPermissions ? (
+ <>
+
+
+ >
+ ) : (
+
+ )}
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexEmptyStateNotShared.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexEmptyStateNotShared.tsx
new file mode 100644
index 00000000000..3f03e07c2f3
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexEmptyStateNotShared.tsx
@@ -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 (
+
+
+
+
+
+ {isDefined(nonReadableViewFieldInfo)
+ ? t`View not shared`
+ : t`Object not shared`}
+
+
+ {isDefined(nonReadableViewFieldInfo)
+ ? getNonReadableViewFieldSubTitle(nonReadableViewFieldInfo)
+ : t`You don't have access to this object.`}
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHasCurrentViewNonReadableFields.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHasCurrentViewNonReadableFields.ts
new file mode 100644
index 00000000000..11dd3ae43db
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHasCurrentViewNonReadableFields.ts
@@ -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,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/utils/getNonReadableInfoForViewField.ts b/packages/twenty-front/src/modules/object-record/record-index/utils/getNonReadableInfoForViewField.ts
new file mode 100644
index 00000000000..8397c32d38e
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/utils/getNonReadableInfoForViewField.ts
@@ -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;
+ 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,
+ };
+ }
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/utils/getNonReadableViewFieldSubTitle.ts b/packages/twenty-front/src/modules/object-record/record-index/utils/getNonReadableViewFieldSubTitle.ts
new file mode 100644
index 00000000000..73f230c9a05
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/utils/getNonReadableViewFieldSubTitle.ts
@@ -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.`;
+};
diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx
index f7625a8b469..1d970fd5ead 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx
@@ -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';
diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts
index 215388b6d0e..d485b8f9672 100644
--- a/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts
+++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts
@@ -14,4 +14,5 @@ export const BACKGROUND: Record = {
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',
};
diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts
index 534bc4cd866..3371f4990a1 100644
--- a/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts
+++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts
@@ -15,4 +15,5 @@ export const DARK_BACKGROUND: Record = {
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',
};
diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts
index 6949166672f..63633ada6f8 100644
--- a/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts
+++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts
@@ -15,4 +15,5 @@ export const DARK_MOVING_IMAGE: Record = {
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',
};
diff --git a/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts
index 6d591b5bb1f..afedead50f2 100644
--- a/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts
+++ b/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts
@@ -14,4 +14,5 @@ export const MOVING_IMAGE: Record = {
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',
};