Refactor isReadOnly + introduce restrictedFields (#13533)
Closes https://github.com/twentyhq/core-team-issues/issues/1276 We now need to take fields permissions into account when determining if a cell is editable or not. a record field (= a cell) is read only if one of the condition is met: - the object of the record is read-only (can be determined at table level) by permission setting - the field of the object is read-only (can be determined at column level) - either by permission settings or by system (some workflows fields for intance) - the record is deleted (can be determined at row level) we reorganized the code to avoid re-computing known information as much as possible. --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+7
-1
@@ -7,6 +7,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
@@ -94,6 +95,11 @@ export const CalendarEventDetails = ({
|
||||
|
||||
const { calendarEventParticipants } = calendarEvent;
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: calendarEvent.id,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
const Fields = fieldsToDisplay.map((fieldName) => (
|
||||
<StyledPropertyBox key={fieldName}>
|
||||
<FieldContext.Provider
|
||||
@@ -108,7 +114,7 @@ export const CalendarEventDetails = ({
|
||||
}),
|
||||
useUpdateRecord: () => [() => undefined, { loading: false }],
|
||||
maxWidth: 300,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
+12
-14
@@ -8,7 +8,6 @@ import { canCreateActivityState } from '@/activities/states/canCreateActivitySta
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||
import { Key } from 'ts-key-enum';
|
||||
@@ -29,7 +28,7 @@ import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly';
|
||||
import { isInlineCellInEditModeFamilyState } from '@/object-record/record-inline-cell/states/isInlineCellInEditModeFamilyState';
|
||||
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@@ -67,16 +66,9 @@ export const ActivityRichTextEditor = ({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: activityId,
|
||||
objectMetadataId: objectMetadataItemActivity.id,
|
||||
});
|
||||
|
||||
const isReadOnly = isFieldValueReadOnly({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
isRecordReadOnly,
|
||||
isCustom: objectMetadataItemActivity.isCustom,
|
||||
});
|
||||
const bodyV2FieldMetadataItemId = objectMetadataItemActivity.fields.find(
|
||||
(field) => field.name === 'bodyV2',
|
||||
)?.id;
|
||||
|
||||
const { deleteManyRecords: deleteAttachments } = useDeleteManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
@@ -106,8 +98,14 @@ export const ActivityRichTextEditor = ({
|
||||
activityObjectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const isRecordFieldReadOnly = useIsRecordFieldReadOnly({
|
||||
recordId: activityId,
|
||||
objectMetadataId: objectMetadataItemActivity.id,
|
||||
fieldMetadataId: bodyV2FieldMetadataItemId,
|
||||
});
|
||||
|
||||
const persistBodyDebounced = useDebouncedCallback((blocknote: string) => {
|
||||
if (isReadOnly) return;
|
||||
if (isRecordFieldReadOnly === true) return;
|
||||
|
||||
const input = {
|
||||
bodyV2: {
|
||||
@@ -430,7 +428,7 @@ export const ActivityRichTextEditor = ({
|
||||
onBlur={handlerBlockEditorBlur}
|
||||
onChange={handleEditorChange}
|
||||
editor={editor}
|
||||
readonly={isReadOnly}
|
||||
readonly={isRecordFieldReadOnly}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ export const ActivityTargetsInlineCell = ({
|
||||
|
||||
const { closeInlineCell } = useInlineCell(componentInstanceId);
|
||||
|
||||
const { fieldDefinition, isReadOnly } = useContext(FieldContext);
|
||||
const { fieldDefinition, isRecordFieldReadOnly: isReadOnly } =
|
||||
useContext(FieldContext);
|
||||
|
||||
const { openActivityTargetCellEditMode } =
|
||||
useOpenActivityTargetCellEditMode();
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export const EventFieldDiffValue = ({
|
||||
},
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
},
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<FieldDisplay />
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ import { createContext } from 'react';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { ObjectPermission } from '~/generated/graphql';
|
||||
|
||||
type RecordBoardContextProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
@@ -17,6 +18,7 @@ type RecordBoardContextProps = {
|
||||
}) => void;
|
||||
deleteOneRecord: (idToDelete: string) => Promise<unknown>;
|
||||
recordBoardId: string;
|
||||
objectPermissions: ObjectPermission;
|
||||
};
|
||||
|
||||
export const RecordBoardContext = createContext<RecordBoardContextProps>(
|
||||
|
||||
+20
-11
@@ -9,10 +9,10 @@ import {
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
|
||||
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { useContext } from 'react';
|
||||
@@ -24,7 +24,7 @@ export const RecordBoardCardBody = ({
|
||||
}) => {
|
||||
const { recordId, isRecordReadOnly } = useContext(RecordBoardCardContext);
|
||||
|
||||
const { updateOneRecord } = useContext(RecordBoardContext);
|
||||
const { updateOneRecord, objectPermissions } = useContext(RecordBoardContext);
|
||||
|
||||
const useUpdateOneRecordHook: RecordUpdateHook = () => {
|
||||
const updateEntity = ({ variables }: RecordUpdateHookParams) => {
|
||||
@@ -37,23 +37,32 @@ export const RecordBoardCardBody = ({
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
const fieldDefinitionsWithReadOnly = fieldDefinitions.map(
|
||||
(fieldDefinition) => ({
|
||||
...fieldDefinition,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions,
|
||||
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
fieldType: fieldDefinition.type,
|
||||
isCustom: fieldDefinition.metadata.isCustom,
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular ?? '',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordBoardCardBodyContainer>
|
||||
{fieldDefinitions.map((fieldDefinition) => (
|
||||
{fieldDefinitionsWithReadOnly.map((fieldDefinition) => (
|
||||
<StopPropagationContainer key={fieldDefinition.fieldMetadataId}>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
recordId,
|
||||
maxWidth: 156,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: isFieldValueReadOnly({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
fieldType: fieldDefinition.type,
|
||||
isRecordReadOnly,
|
||||
isCustom: fieldDefinition.metadata.isCustom,
|
||||
}),
|
||||
isRecordFieldReadOnly: fieldDefinition.isRecordFieldReadOnly,
|
||||
fieldDefinition: {
|
||||
disableTooltip: false,
|
||||
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { RecordBoardCardMultiDragPreview } from '@/object-record/record-board/re
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { isRecordBoardCardFocusedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardFocusedComponentFamilyState';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
|
||||
const StyledDraggableContainer = styled.div`
|
||||
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import {
|
||||
FieldContext,
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export const FieldContextProvider = ({
|
||||
@@ -34,10 +34,6 @@ export const FieldContextProvider = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const fieldMetadataItem = objectMetadataItem?.fields.find(
|
||||
(field) => field.name === fieldMetadataName,
|
||||
);
|
||||
@@ -57,12 +53,16 @@ export const FieldContextProvider = ({
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
const isRecordFieldReadOnly = useIsRecordFieldReadOnly({
|
||||
recordId: objectRecordId,
|
||||
fieldMetadataId: fieldMetadataItem?.id,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
if (!fieldMetadataItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isObjectReadOnly = !objectPermissions.canUpdateObjectRecords;
|
||||
|
||||
return (
|
||||
<FieldContext.Provider
|
||||
key={objectRecordId + fieldMetadataItem.id}
|
||||
@@ -80,7 +80,7 @@ export const FieldContextProvider = ({
|
||||
customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation,
|
||||
clearable,
|
||||
overridenIsFieldEmpty,
|
||||
isReadOnly: isObjectReadOnly,
|
||||
isRecordFieldReadOnly,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export type GenericFieldContextType = {
|
||||
overridenIsFieldEmpty?: boolean;
|
||||
displayedMaxRows?: number;
|
||||
isDisplayModeFixHeight?: boolean;
|
||||
isReadOnly: boolean;
|
||||
isRecordFieldReadOnly: boolean;
|
||||
disableChipClick?: boolean;
|
||||
onRecordChipClick?: (event: MouseEvent) => void;
|
||||
onOpenEditMode?: () => void;
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ const getWrapper =
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
fieldDefinition: phonesFieldDefinition,
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ const getWrapper =
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { phonesFieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
|
||||
import { useIsFieldValueReadOnly } from '../useIsFieldValueReadOnly';
|
||||
|
||||
describe('useIsFieldValueReadOnly', () => {
|
||||
it('should return true if the field is read only', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIsFieldValueReadOnly({
|
||||
fieldDefinition: phonesFieldDefinition,
|
||||
isRecordReadOnly: false,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if the record is read only', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIsFieldValueReadOnly({
|
||||
fieldDefinition: phonesFieldDefinition,
|
||||
isRecordReadOnly: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -104,7 +104,7 @@ const getWrapper =
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
useUpdateRecord: useUpdateOneRecordMutation,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
+1
-1
@@ -346,7 +346,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => {
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
useUpdateRecord: useUpdateOneRecordMutation,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
|
||||
export type UseFieldIsReadOnlyParams = {
|
||||
fieldMetadataId: string;
|
||||
objectMetadataId: string;
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const useIsRecordFieldReadOnly = ({
|
||||
fieldMetadataId,
|
||||
objectMetadataId,
|
||||
recordId,
|
||||
}: UseFieldIsReadOnlyParams) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataId,
|
||||
});
|
||||
|
||||
const fieldMetadataItem = objectMetadataItem.fields.find(
|
||||
(field) => field.id === fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!fieldMetadataItem) {
|
||||
throw new Error(
|
||||
`Could not load read only property for field ${fieldMetadataId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const objectPermissions = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId,
|
||||
);
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
return isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions,
|
||||
fieldMetadataId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
isCustom: fieldMetadataItem.isCustom ?? false,
|
||||
});
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { isRecordReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordReadOnly';
|
||||
import { useIsRecordDeleted } from '@/object-record/record-field/hooks/useIsRecordDeleted';
|
||||
|
||||
type UseIsRecordReadOnlyParams = {
|
||||
recordId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const useIsRecordReadOnly = ({
|
||||
recordId,
|
||||
objectMetadataId,
|
||||
}: UseIsRecordReadOnlyParams) => {
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const objectPermissions = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataId,
|
||||
);
|
||||
|
||||
const isRecordDeleted = useIsRecordDeleted({ recordId });
|
||||
|
||||
return isRecordReadOnly({
|
||||
objectPermissions,
|
||||
isRecordDeleted,
|
||||
});
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { isObjectReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isObjectReadOnly';
|
||||
|
||||
describe('isObjectReadOnly', () => {
|
||||
it('should return true if object is not read only', () => {
|
||||
const result = isObjectReadOnly({
|
||||
objectPermissions: {
|
||||
canUpdateObjectRecords: true,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if object is read only', () => {
|
||||
const result = isObjectReadOnly({
|
||||
objectPermissions: {
|
||||
canUpdateObjectRecords: false,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('isRecordFieldReadOnly', () => {
|
||||
const mockObjectPermissions = {
|
||||
canUpdateObjectRecords: true,
|
||||
objectMetadataId: '123',
|
||||
restrictedFields: {},
|
||||
};
|
||||
|
||||
const mockParams = {
|
||||
isRecordReadOnly: false,
|
||||
objectPermissions: mockObjectPermissions,
|
||||
fieldMetadataId: 'field-123',
|
||||
objectNameSingular: 'person',
|
||||
fieldName: 'firstName',
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
isCustom: false,
|
||||
};
|
||||
|
||||
it('should return true when record is read-only', () => {
|
||||
const result = isRecordFieldReadOnly({
|
||||
...mockParams,
|
||||
isRecordReadOnly: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when object lacks update permissions', () => {
|
||||
const result = isRecordFieldReadOnly({
|
||||
...mockParams,
|
||||
objectPermissions: {
|
||||
...mockObjectPermissions,
|
||||
canUpdateObjectRecords: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when field is restricted by permissions', () => {
|
||||
const result = isRecordFieldReadOnly({
|
||||
...mockParams,
|
||||
objectPermissions: {
|
||||
...mockObjectPermissions,
|
||||
restrictedFields: {
|
||||
'field-123': { canUpdate: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for system read-only fields like createdAt', () => {
|
||||
const result = isRecordFieldReadOnly({
|
||||
...mockParams,
|
||||
fieldName: 'createdAt',
|
||||
fieldType: FieldMetadataType.DATE_TIME,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for calendar event objects (system read-only)', () => {
|
||||
const result = isRecordFieldReadOnly({
|
||||
...mockParams,
|
||||
objectNameSingular: 'calendarEvent',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for workflow non-name fields (system read-only)', () => {
|
||||
const result = isRecordFieldReadOnly({
|
||||
...mockParams,
|
||||
objectNameSingular: 'workflow',
|
||||
fieldName: 'status',
|
||||
isCustom: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when all conditions allow editing', () => {
|
||||
const result = isRecordFieldReadOnly({
|
||||
...mockParams,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { isRecordReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordReadOnly';
|
||||
|
||||
describe('isRecordReadOnly', () => {
|
||||
it('should return false if record is not deleted and has update permissions', () => {
|
||||
const result = isRecordReadOnly({
|
||||
objectPermissions: {
|
||||
canUpdateObjectRecords: true,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
isRecordDeleted: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if record is not deleted but lacks update permissions', () => {
|
||||
const result = isRecordReadOnly({
|
||||
objectPermissions: {
|
||||
canUpdateObjectRecords: false,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
isRecordDeleted: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if record is deleted even with update permissions', () => {
|
||||
const result = isRecordReadOnly({
|
||||
objectPermissions: {
|
||||
canUpdateObjectRecords: true,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
isRecordDeleted: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if record is deleted and lacks update permissions', () => {
|
||||
const result = isRecordReadOnly({
|
||||
objectPermissions: {
|
||||
canUpdateObjectRecords: false,
|
||||
objectMetadataId: '123',
|
||||
},
|
||||
isRecordDeleted: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { isObjectReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isObjectReadOnly';
|
||||
import { ObjectPermission } from '~/generated/graphql';
|
||||
|
||||
export type IsFieldReadOnlyByPermissionParams = {
|
||||
objectPermissions: ObjectPermission;
|
||||
fieldMetadataId: string;
|
||||
};
|
||||
|
||||
export const isFieldReadOnlyByPermissions = ({
|
||||
objectPermissions,
|
||||
fieldMetadataId,
|
||||
}: IsFieldReadOnlyByPermissionParams) => {
|
||||
if (isObjectReadOnly({ objectPermissions }) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const fieldMetadataIsRestrictedForUpdate =
|
||||
objectPermissions.restrictedFields[fieldMetadataId]?.canUpdate === false;
|
||||
|
||||
return fieldMetadataIsRestrictedForUpdate;
|
||||
};
|
||||
+4
-10
@@ -7,25 +7,19 @@ import { isFieldRichText } from '@/object-record/record-field/types/guards/isFie
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type isFieldValueReadOnlyParams = {
|
||||
objectNameSingular?: string;
|
||||
export type IsFieldReadOnlyBySystemParams = {
|
||||
objectNameSingular: string;
|
||||
fieldName?: string;
|
||||
fieldType?: FieldMetadataType;
|
||||
isRecordReadOnly?: boolean;
|
||||
isCustom?: boolean;
|
||||
};
|
||||
|
||||
export const isFieldValueReadOnly = ({
|
||||
export const isFieldReadOnlyBySystem = ({
|
||||
objectNameSingular,
|
||||
fieldName,
|
||||
fieldType,
|
||||
isCustom,
|
||||
isRecordReadOnly = false,
|
||||
}: isFieldValueReadOnlyParams) => {
|
||||
if (isRecordReadOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}: IsFieldReadOnlyBySystemParams) => {
|
||||
if (
|
||||
isWorkflowRunJsonField({
|
||||
objectMetadataNameSingular: objectNameSingular,
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { ObjectPermission } from '~/generated/graphql';
|
||||
|
||||
type IsObjectReadOnlyParams = {
|
||||
objectPermissions: ObjectPermission;
|
||||
};
|
||||
|
||||
export const isObjectReadOnly = ({
|
||||
objectPermissions,
|
||||
}: IsObjectReadOnlyParams) => {
|
||||
return !objectPermissions.canUpdateObjectRecords;
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
IsFieldReadOnlyByPermissionParams,
|
||||
isFieldReadOnlyByPermissions,
|
||||
} from '@/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyByPermissions';
|
||||
import {
|
||||
IsFieldReadOnlyBySystemParams,
|
||||
isFieldReadOnlyBySystem,
|
||||
} from '@/object-record/record-field/hooks/read-only/utils/internal/isFieldReadOnlyBySystem';
|
||||
|
||||
type IsRecordFieldReadOnlyParams = {
|
||||
isRecordReadOnly: boolean;
|
||||
} & IsFieldReadOnlyByPermissionParams &
|
||||
IsFieldReadOnlyBySystemParams;
|
||||
|
||||
export const isRecordFieldReadOnly = ({
|
||||
isRecordReadOnly,
|
||||
objectPermissions,
|
||||
fieldMetadataId,
|
||||
objectNameSingular,
|
||||
fieldName,
|
||||
fieldType,
|
||||
isCustom,
|
||||
}: IsRecordFieldReadOnlyParams) => {
|
||||
const fieldReadOnlyByPermissions = isFieldReadOnlyByPermissions({
|
||||
objectPermissions,
|
||||
fieldMetadataId,
|
||||
});
|
||||
|
||||
const fieldReadOnlyBySystem = isFieldReadOnlyBySystem({
|
||||
objectNameSingular,
|
||||
fieldName,
|
||||
fieldType,
|
||||
isCustom,
|
||||
});
|
||||
|
||||
return (
|
||||
isRecordReadOnly || fieldReadOnlyByPermissions || fieldReadOnlyBySystem
|
||||
);
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { ObjectPermission } from '~/generated/graphql';
|
||||
|
||||
type IsObjectReadOnlyParams = {
|
||||
objectPermissions: ObjectPermission;
|
||||
isRecordDeleted: boolean;
|
||||
};
|
||||
|
||||
export const isRecordReadOnly = ({
|
||||
objectPermissions,
|
||||
isRecordDeleted,
|
||||
}: IsObjectReadOnlyParams) => {
|
||||
return isRecordDeleted || !objectPermissions.canUpdateObjectRecords;
|
||||
};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
|
||||
|
||||
type UseIsFieldValueReadOnlyParams = {
|
||||
isRecordReadOnly: boolean;
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
};
|
||||
|
||||
export const useIsFieldValueReadOnly = ({
|
||||
fieldDefinition,
|
||||
isRecordReadOnly,
|
||||
}: UseIsFieldValueReadOnlyParams) => {
|
||||
const { metadata, type } = fieldDefinition;
|
||||
|
||||
return isFieldValueReadOnly({
|
||||
objectNameSingular: metadata.objectMetadataNameSingular,
|
||||
fieldName: metadata.fieldName,
|
||||
fieldType: type,
|
||||
isRecordReadOnly,
|
||||
isCustom: metadata.isCustom,
|
||||
});
|
||||
};
|
||||
+3
-10
@@ -1,4 +1,3 @@
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
@@ -6,13 +5,11 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseIsRecordReadOnlyParams = {
|
||||
recordId: string;
|
||||
objectMetadataId: string;
|
||||
};
|
||||
|
||||
export const useIsRecordReadOnly = ({
|
||||
export const useIsRecordDeleted = ({
|
||||
recordId,
|
||||
objectMetadataId,
|
||||
}: UseIsRecordReadOnlyParams) => {
|
||||
}: UseIsRecordReadOnlyParams): boolean => {
|
||||
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
@@ -20,9 +17,5 @@ export const useIsRecordReadOnly = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(objectMetadataId);
|
||||
|
||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||
|
||||
return !hasObjectUpdatePermissions || isDefined(recordDeletedAt);
|
||||
return isDefined(recordDeletedAt);
|
||||
};
|
||||
+1
-1
@@ -47,7 +47,7 @@ const meta: Meta = {
|
||||
fieldDefinition: {
|
||||
...relationFromManyFieldDisplayMock.fieldDefinition,
|
||||
} as unknown as FieldDefinition<FieldMetadata>,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RelationFieldValueSetterEffect />
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ const AddressInputWithContext = ({
|
||||
},
|
||||
recordId: recordId ?? '123',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<AddressValueSetterEffect value={value} />
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ const ArrayInputWithContext = ({
|
||||
},
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
}}
|
||||
>
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ const BooleanFieldInputWithContext = ({
|
||||
},
|
||||
recordId: recordId ?? '123',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<BooleanFieldValueSetterEffect
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ const DateFieldInputWithContext = ({
|
||||
},
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ const EmailInputWithContext = ({
|
||||
},
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
}}
|
||||
>
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ const LinksInputWithContext = ({
|
||||
},
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
}}
|
||||
>
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ const NumberFieldInputWithContext = ({
|
||||
},
|
||||
recordId: '123',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{isReady && <StorybookFieldInputDropdownFocusIdSetterEffect />}
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ const PhoneInputWithContext = ({
|
||||
},
|
||||
recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
useUpdateRecord: () => [updateRecord, { loading: false }],
|
||||
}}
|
||||
>
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ const RatingFieldInputWithContext = ({
|
||||
},
|
||||
recordId: recordId ?? '123',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RatingFieldValueSetterEffect value={value} />
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ const RelationManyFieldInputWithContext = () => {
|
||||
fieldDefinition,
|
||||
recordId: 'recordId',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ const RelationToOneFieldInputWithContext = ({
|
||||
},
|
||||
recordId: recordId,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ const RichTextFieldInputWithContext = ({
|
||||
},
|
||||
},
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RichTextFieldInput
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ const TextFieldInputWithContext = ({
|
||||
},
|
||||
},
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<TextFieldValueSetterEffect value={value} />
|
||||
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('isFieldValueReadOnly', () => {
|
||||
it('should return true if record is read only', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
isRecordReadOnly: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if object is a workflow sub object', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: 'workflowRun',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if object is a calendar event', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if object is a workflow and field is not name', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
fieldName: 'description',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if object is a workflow and field is name', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
fieldName: 'name',
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if object is a workflow object and field is custom', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.Workflow,
|
||||
fieldName: 'test',
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if object is a workflow sub object and field is custom', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
fieldName: 'test',
|
||||
isCustom: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
describe('when checking field types', () => {
|
||||
it('should return true if fieldType is RICH_TEXT', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.RICH_TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if fieldType is RICH_TEXT_V2', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.RICH_TEXT_V2,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if fieldType is ACTOR', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.ACTOR,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other field types', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for standard editable fields', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: 'company',
|
||||
fieldName: 'name',
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
isRecordReadOnly: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
+6
@@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { RecordBoard } from '@/object-record/record-board/components/RecordBoard';
|
||||
import { RecordBoardBodyEscapeHotkeyEffect } from '@/object-record/record-board/components/RecordBoardBodyEscapeHotkeyEffect';
|
||||
@@ -27,6 +28,10 @@ export const RecordIndexBoardContainer = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const recordIndexKanbanFieldMetadataId = useRecoilValue(
|
||||
recordIndexKanbanFieldMetadataIdState,
|
||||
);
|
||||
@@ -60,6 +65,7 @@ export const RecordIndexBoardContainer = ({
|
||||
updateOneRecord,
|
||||
deleteOneRecord,
|
||||
recordBoardId,
|
||||
objectPermissions,
|
||||
}}
|
||||
>
|
||||
<RecordBoard />
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ export const RecordInlineCell = ({
|
||||
isDisplayModeFixHeight,
|
||||
onOpenEditMode: onOpenEditModeFromContext,
|
||||
onCloseEditMode: onCloseEditModeFromContext,
|
||||
isReadOnly,
|
||||
isRecordFieldReadOnly: isReadOnly,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const { openFieldInput, closeFieldInput } = useOpenFieldInputEditMode();
|
||||
|
||||
+26
-7
@@ -8,9 +8,9 @@ import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/uti
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader';
|
||||
@@ -135,12 +135,17 @@ export const FieldsCard = ({
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isFieldValueReadOnly({
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions:
|
||||
objectPermissionsByObjectMetadataId[
|
||||
objectMetadataItem.id
|
||||
],
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
objectNameSingular,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
isCustom: fieldMetadataItem.isCustom ?? false,
|
||||
isRecordReadOnly,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
@@ -180,12 +185,17 @@ export const FieldsCard = ({
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isFieldValueReadOnly({
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions:
|
||||
objectPermissionsByObjectMetadataId[
|
||||
objectMetadataItem.id
|
||||
],
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
objectNameSingular,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
isCustom: fieldMetadataItem.isCustom ?? false,
|
||||
isRecordReadOnly,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
@@ -227,7 +237,16 @@ export const FieldsCard = ({
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly,
|
||||
objectPermissions:
|
||||
objectPermissionsByObjectMetadataId[objectMetadataItem.id],
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
objectNameSingular,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
isCustom: fieldMetadataItem.isCustom ?? false,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordDetailRelationSection
|
||||
|
||||
+4
-3
@@ -2,7 +2,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
||||
@@ -67,9 +67,10 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
objectRecordId,
|
||||
});
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
const isLabelIdentifierReadOnly = useIsRecordFieldReadOnly({
|
||||
recordId: objectRecordId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataItem?.id,
|
||||
});
|
||||
|
||||
const { navigateToIndexView, rankInView, totalCount } =
|
||||
@@ -118,7 +119,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isCentered: false,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
isRecordFieldReadOnly: isLabelIdentifierReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCell
|
||||
|
||||
+4
-3
@@ -2,7 +2,7 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandard
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordFieldReadOnly';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
@@ -60,8 +60,9 @@ export const SummaryCard = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
const isTitleReadOnly = useIsRecordFieldReadOnly({
|
||||
recordId: objectRecordId,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataItem?.id,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
@@ -98,7 +99,7 @@ export const SummaryCard = ({
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isCentered: !isMobile,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
isRecordFieldReadOnly: isTitleReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCell
|
||||
|
||||
+22
-15
@@ -17,8 +17,8 @@ import {
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
@@ -107,7 +107,11 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
onClick,
|
||||
relationRecord,
|
||||
}: RecordDetailRelationRecordsListItemProps) => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
const {
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
isRecordFieldReadOnly: parentIsRecordFieldReadOnly,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const { openModal } = useModal();
|
||||
|
||||
@@ -175,13 +179,13 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const handleDetach = () => {
|
||||
closeDropdown(dropdownInstanceId);
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === relationFieldMetadataId,
|
||||
);
|
||||
|
||||
if (!relationFieldMetadataItem?.name) return;
|
||||
|
||||
if (isToOneObject) {
|
||||
@@ -236,16 +240,11 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
[isExpanded],
|
||||
);
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
const isRelationRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: relationRecord.id,
|
||||
objectMetadataId: relationObjectMetadataItem.id,
|
||||
});
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly({
|
||||
fieldDefinition,
|
||||
isRecordReadOnly,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledListItem isDropdownOpen={isDropdownOpen}>
|
||||
@@ -260,7 +259,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
accent="tertiary"
|
||||
/>
|
||||
</StyledClickableZone>
|
||||
{!isFieldReadOnly && (
|
||||
{!parentIsRecordFieldReadOnly && (
|
||||
<Dropdown
|
||||
dropdownId={dropdownInstanceId}
|
||||
dropdownPlacement="right-start"
|
||||
@@ -312,7 +311,15 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
labelWidth: 90,
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isReadOnly: isFieldReadOnly,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly: isRelationRecordReadOnly,
|
||||
objectPermissions: relationObjectPermissions,
|
||||
fieldMetadataId: fieldMetadataItem.id,
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
fieldName: fieldMetadataItem.name,
|
||||
fieldType: fieldMetadataItem.type,
|
||||
isCustom: relationObjectMetadataItem.isCustom,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
+11
-11
@@ -2,8 +2,7 @@ import { useContext } from 'react';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordDetailRelationSectionDropdownToMany } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToMany';
|
||||
import { RecordDetailRelationSectionDropdownToOne } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSectionDropdownToOne';
|
||||
@@ -16,7 +15,8 @@ type RecordDetailRelationSectionDropdownProps = {
|
||||
export const RecordDetailRelationSectionDropdown = ({
|
||||
loading,
|
||||
}: RecordDetailRelationSectionDropdownProps) => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
const { fieldDefinition, isRecordFieldReadOnly, recordId } =
|
||||
useContext(FieldContext);
|
||||
const {
|
||||
relationType,
|
||||
objectMetadataNameSingular,
|
||||
@@ -32,24 +32,24 @@ export const RecordDetailRelationSectionDropdown = ({
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
// TODO: use new relation type
|
||||
const isToOneObject = relationType === RelationType.MANY_TO_ONE;
|
||||
const isToManyObjects = relationType === RelationType.ONE_TO_MANY;
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
const isRecordReadOnlyFromRelatedRecordPerspective = useIsRecordReadOnly({
|
||||
recordId,
|
||||
objectMetadataId: isToOneObject
|
||||
? recordObjectMetadataItem.id
|
||||
: relationObjectMetadataItem.id,
|
||||
});
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly({
|
||||
fieldDefinition,
|
||||
isRecordReadOnly,
|
||||
});
|
||||
|
||||
if (loading || isFieldReadOnly) return null;
|
||||
if (
|
||||
loading ||
|
||||
isRecordFieldReadOnly ||
|
||||
isRecordReadOnlyFromRelatedRecordPerspective
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isToOneObject) {
|
||||
return <RecordDetailRelationSectionDropdownToOne />;
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
|
||||
)!,
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
}),
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
|
||||
+6
@@ -3,6 +3,7 @@ import { ReactNode } from 'react';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
@@ -23,6 +24,10 @@ export const RecordTableContextProvider = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const visibleTableColumns = useRecoilComponentValueV2(
|
||||
visibleTableColumnsComponentSelector,
|
||||
recordTableId,
|
||||
@@ -36,6 +41,7 @@ export const RecordTableContextProvider = ({
|
||||
visibleTableColumns,
|
||||
recordTableId,
|
||||
objectNameSingular,
|
||||
objectPermissions,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
+4
-1
@@ -70,6 +70,9 @@ const meta: Meta = {
|
||||
visibleTableColumns: mockPerformance.visibleTableColumns as any,
|
||||
objectNameSingular:
|
||||
mockPerformance.objectMetadataItem.nameSingular,
|
||||
objectPermissions: {
|
||||
objectMetadataId: mockPerformance.objectMetadataItem.id,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RecordTableComponentInstance
|
||||
@@ -120,7 +123,7 @@ const meta: Meta = {
|
||||
fieldDefinition: {
|
||||
...mockPerformance.fieldDefinition,
|
||||
},
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RelationFieldValueSetterEffect />
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { ObjectPermission } from '~/generated/graphql';
|
||||
import { createRequiredContext } from '~/utils/createRequiredContext';
|
||||
|
||||
type RecordTableContextValue = {
|
||||
@@ -9,6 +10,7 @@ type RecordTableContextValue = {
|
||||
objectNameSingular: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
|
||||
objectPermissions: ObjectPermission;
|
||||
};
|
||||
|
||||
export const [RecordTableContextProvider, useRecordTableContextOrThrow] =
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ export type RecordTableRowContextValue = {
|
||||
rowIndex: number;
|
||||
isSelected: boolean;
|
||||
inView: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isRecordReadOnly?: boolean;
|
||||
};
|
||||
|
||||
export const [RecordTableRowContextProvider, useRecordTableRowContextOrThrow] =
|
||||
|
||||
+2
-2
@@ -1,5 +1,7 @@
|
||||
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import {
|
||||
AnimatedPlaceholder,
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
@@ -8,8 +10,6 @@ import {
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
AnimatedPlaceholderType,
|
||||
} from 'twenty-ui/layout';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { IconComponent } from 'twenty-ui/display';
|
||||
|
||||
type RecordTableEmptyStateDisplayButtonComponentProps = {
|
||||
buttonComponent?: React.ReactNode;
|
||||
|
||||
+5
-2
@@ -59,8 +59,11 @@ export const RecordTableCellBaseContainer = ({
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { isReadOnly, fieldDefinition, isLabelIdentifier } =
|
||||
useContext(FieldContext);
|
||||
const {
|
||||
isRecordFieldReadOnly: isReadOnly,
|
||||
fieldDefinition,
|
||||
isLabelIdentifier,
|
||||
} = useContext(FieldContext);
|
||||
const { setIsFocused } = useFieldFocus();
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ export const RecordTableCellDisplayMode = ({
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { recordId, isReadOnly } = useContext(FieldContext);
|
||||
const { recordId, isRecordFieldReadOnly: isReadOnly } =
|
||||
useContext(FieldContext);
|
||||
|
||||
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
|
||||
|
||||
|
||||
+13
-15
@@ -1,7 +1,7 @@
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
@@ -18,26 +18,16 @@ type RecordTableCellFieldContextGenericProps = {
|
||||
export const RecordTableCellFieldContextGeneric = ({
|
||||
children,
|
||||
}: RecordTableCellFieldContextGenericProps) => {
|
||||
const { recordId, isReadOnly: isTableRowReadOnly } =
|
||||
useRecordTableRowContextOrThrow();
|
||||
const { recordId, isRecordReadOnly } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
const { objectMetadataItem, objectPermissions } =
|
||||
useRecordTableContextOrThrow();
|
||||
const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } =
|
||||
useRecordIndexContextOrThrow();
|
||||
const { columnDefinition } = useContext(RecordTableCellContext);
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly({
|
||||
fieldDefinition: columnDefinition,
|
||||
isRecordReadOnly: isTableRowReadOnly ?? false,
|
||||
});
|
||||
|
||||
const updateRecord = useContext(RecordUpdateContext);
|
||||
|
||||
const objectPermissions = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
let hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
|
||||
|
||||
if (
|
||||
@@ -70,7 +60,15 @@ export const RecordTableCellFieldContextGeneric = ({
|
||||
objectMetadataItem,
|
||||
}),
|
||||
displayedMaxRows: 1,
|
||||
isReadOnly: isFieldReadOnly,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
isRecordReadOnly: isRecordReadOnly ?? false,
|
||||
objectPermissions,
|
||||
fieldMetadataId: columnDefinition.fieldMetadataId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
fieldName: columnDefinition.metadata.fieldName,
|
||||
fieldType: columnDefinition.type,
|
||||
isCustom: objectMetadataItem.isCustom,
|
||||
}),
|
||||
isForbidden: !hasObjectReadPermissions,
|
||||
}}
|
||||
>
|
||||
|
||||
+11
-9
@@ -1,6 +1,6 @@
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { isRecordFieldReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isRecordFieldReadOnly';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
import { useOpenRecordFromIndexView } from '@/object-record/record-index/hooks/useOpenRecordFromIndexView';
|
||||
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
|
||||
@@ -26,8 +26,7 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
|
||||
}: RecordTableCellFieldContextLabelIdentifierProps) => {
|
||||
const { indexIdentifierUrl, objectPermissionsByObjectMetadataId } =
|
||||
useRecordIndexContextOrThrow();
|
||||
const { recordId, isReadOnly: isTableRowReadOnly } =
|
||||
useRecordTableRowContextOrThrow();
|
||||
const { recordId, isRecordReadOnly } = useRecordTableRowContextOrThrow();
|
||||
|
||||
const { columnDefinition } = useContext(RecordTableCellContext);
|
||||
const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow();
|
||||
@@ -40,11 +39,6 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
|
||||
isRecordTableScrolledLeftComponentState,
|
||||
);
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly({
|
||||
fieldDefinition: columnDefinition,
|
||||
isRecordReadOnly: isTableRowReadOnly ?? false,
|
||||
});
|
||||
|
||||
const objectPermissions = getObjectPermissionsForObject(
|
||||
objectPermissionsByObjectMetadataId,
|
||||
objectMetadataItem.id,
|
||||
@@ -75,7 +69,15 @@ export const RecordTableCellFieldContextLabelIdentifier = ({
|
||||
isLabelIdentifier: true,
|
||||
isLabelIdentifierCompact,
|
||||
displayedMaxRows: 1,
|
||||
isReadOnly: isFieldReadOnly,
|
||||
isRecordFieldReadOnly: isRecordFieldReadOnly({
|
||||
objectPermissions,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
fieldName: columnDefinition.metadata.fieldName,
|
||||
fieldType: columnDefinition.type,
|
||||
isCustom: objectMetadataItem.isCustom,
|
||||
fieldMetadataId: columnDefinition.fieldMetadataId,
|
||||
isRecordReadOnly: isRecordReadOnly ?? false,
|
||||
}),
|
||||
maxWidth: columnDefinition.size,
|
||||
onRecordChipClick: () => {
|
||||
activateRecordTableRow(rowIndex);
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const RecordTableCellFieldInput = () => {
|
||||
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
||||
const { isReadOnly } = useContext(FieldContext);
|
||||
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
|
||||
const instanceId = useAvailableComponentInstanceId(
|
||||
RecordFieldComponentInstanceContext,
|
||||
);
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritin
|
||||
|
||||
export const RecordTableCellHotkeysEffect = () => {
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
const { isReadOnly } = useContext(FieldContext);
|
||||
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
|
||||
const cellFocusId = useCurrentlyFocusedRecordTableCellFocusId();
|
||||
const { onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
||||
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ const RecordTableCellHoveredPortalContent = () => {
|
||||
|
||||
const isFirstColumn = hoverPosition?.column === 0;
|
||||
|
||||
const { isReadOnly } = useContext(FieldContext);
|
||||
const { isRecordFieldReadOnly: isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
@@ -28,21 +28,21 @@ export const RecordTableCellPortalWrapper = ({
|
||||
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const visibleTableColumns = useRecoilComponentValueV2(
|
||||
visibleTableColumnsComponentSelector,
|
||||
);
|
||||
|
||||
const recordId = allRecordIds.at(position.row);
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: recordId ?? '',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
if (!isDefined(anchorElement) || !isDefined(recordId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isReadOnly = !objectPermissions.canUpdateObjectRecords;
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<RecordTableRowContextProvider
|
||||
value={{
|
||||
@@ -55,7 +55,7 @@ export const RecordTableCellPortalWrapper = ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
}) + recordId,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
isReadOnly,
|
||||
isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTableCellContext.Provider
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
fieldDefinition: textfieldDefinition,
|
||||
recordId: 'recordId',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordTableRowContextProvider value={recordTableRowContextValue}>
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
fieldDefinition: textfieldDefinition,
|
||||
recordId: 'recordId',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordTableRowContextProvider value={recordTableRowContextValue}>
|
||||
|
||||
+5
-1
@@ -19,7 +19,11 @@ export type OpenTableCellArgs = {
|
||||
};
|
||||
|
||||
export const useOpenRecordTableCellFromCell = () => {
|
||||
const { recordId, fieldDefinition, isReadOnly } = useContext(FieldContext);
|
||||
const {
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
isRecordFieldReadOnly: isReadOnly,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const { pathToShowPage, objectNameSingular } =
|
||||
useRecordTableRowContextOrThrow();
|
||||
|
||||
+6
-8
@@ -2,8 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { isObjectReadOnly } from '@/object-record/record-field/hooks/read-only/utils/isObjectReadOnly';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
@@ -115,7 +114,8 @@ type RecordTableHeaderCellProps = {
|
||||
export const RecordTableHeaderCell = ({
|
||||
column,
|
||||
}: RecordTableHeaderCellProps) => {
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
const { objectMetadataItem, objectPermissions } =
|
||||
useRecordTableContextOrThrow();
|
||||
|
||||
const resizeFieldOffsetState = useRecoilComponentCallbackStateV2(
|
||||
resizeFieldOffsetComponentState,
|
||||
@@ -223,11 +223,9 @@ export const RecordTableHeaderCell = ({
|
||||
createNewIndexRecord();
|
||||
};
|
||||
|
||||
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
|
||||
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
const isReadOnly = isObjectReadOnly({
|
||||
objectPermissions,
|
||||
});
|
||||
|
||||
const hasObjectUpdatePermissions = objectPermissions.canUpdateObjectRecords;
|
||||
|
||||
|
||||
+7
-6
@@ -1,5 +1,5 @@
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/read-only/useIsRecordReadOnly';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
@@ -85,9 +85,7 @@ export const RecordTableTr = forwardRef<
|
||||
RecordTableTrProps
|
||||
>(({ children, recordId, focusIndex, isDragging = false, ...props }, ref) => {
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
const objectPermissions = useObjectPermissionsForObject(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
|
||||
const currentRowSelected = useRecoilComponentFamilyValueV2(
|
||||
isRowSelectedComponentFamilyState,
|
||||
recordId,
|
||||
@@ -125,7 +123,10 @@ export const RecordTableTr = forwardRef<
|
||||
const isNextRowActiveOrFocused =
|
||||
(isRowFocusActive && isNextRowFocused) || isNextRowActive;
|
||||
|
||||
const isReadOnly = !objectPermissions.canUpdateObjectRecords;
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordTableRowContextProvider
|
||||
@@ -139,7 +140,7 @@ export const RecordTableTr = forwardRef<
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
isSelected: currentRowSelected,
|
||||
inView: isRowVisible,
|
||||
isReadOnly,
|
||||
isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<StyledTr
|
||||
|
||||
+3
-2
@@ -31,7 +31,8 @@ export const RecordTitleCell = ({
|
||||
sizeVariant,
|
||||
containerType,
|
||||
}: RecordTitleCellProps) => {
|
||||
const { fieldDefinition, recordId, isReadOnly } = useContext(FieldContext);
|
||||
const { fieldDefinition, recordId, isRecordFieldReadOnly } =
|
||||
useContext(FieldContext);
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
@@ -90,7 +91,7 @@ export const RecordTitleCell = ({
|
||||
),
|
||||
editModeContentOnly: isFieldInputOnly,
|
||||
loading: loading,
|
||||
isReadOnly,
|
||||
isReadOnly: isRecordFieldReadOnly,
|
||||
containerType,
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ export const SettingsDataModelFieldPreview = ({
|
||||
},
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
},
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
disableChipClick: true,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -92,6 +92,8 @@ const InternalTableContextProviders = ({
|
||||
recordTableId: objectMetadataItem.namePlural,
|
||||
viewBarId: 'view-bar',
|
||||
visibleTableColumns: visibleTableColumns,
|
||||
objectPermissions:
|
||||
objectPermissionsByObjectMetadataId[objectMetadataItem.id],
|
||||
}}
|
||||
>
|
||||
<RecordTableBodyContextProvider
|
||||
|
||||
@@ -136,7 +136,7 @@ export const getFieldDecorator =
|
||||
position: 0,
|
||||
objectMetadataItem,
|
||||
}),
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordMockSetterEffect
|
||||
|
||||
@@ -51,7 +51,7 @@ export const useMockFieldContext = ({
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectMutation,
|
||||
clearable,
|
||||
isReadOnly: false,
|
||||
isRecordFieldReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -136,6 +136,7 @@ export const mockedUserData: MockedUser = {
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
})),
|
||||
},
|
||||
locale: 'en',
|
||||
@@ -165,6 +166,7 @@ export const mockedLimitedPermissionsUserData: MockedUser = {
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: true,
|
||||
canDestroyObjectRecords: true,
|
||||
restrictedFields: {},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user