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:
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 |
+32
-20
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
+11
-9
@@ -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>
|
||||
|
||||
+45
@@ -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>
|
||||
);
|
||||
};
|
||||
+86
@@ -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,
|
||||
};
|
||||
};
|
||||
+83
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
+16
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user