fix: soft deleted records are read only (#8198)

TODO:
- [ ] It should not be possible to add tasks, notes, files, etc.

Fix for https://github.com/twentyhq/twenty/issues/7172

Note for reviewer:
- With these changes, `deletedAt` is now always included in
`recordGqlFields`.

--- Edit from Charles --
In this PR:
1) As mentionned by @pau-not-paul, we are adding deletedAt to our
recordGqlFields for board and table
2) I'm removing cellReadOnly logic, it is now fully computed using
useIsFieldValueReadonly like it's done in other places, there is no need
to maintain two read only systems
3) refactoring useIsFieldValueReadonly to take all the business logics
into one place together. It's now a function of the 5 factors (isRemote,
isDeleted, objectName, fieldName, etc...). Later it's likely to get back
to a function of Pick<FieldMetadata>, Pick<ObjectMetadata>,
record.isDeleted but we are not there yet

Note: as all cells are listening to the record (for isDeleted), updating
a field will trigger a re-rendering of the row which is not an issue
right now. It will be if we introduce bulk edition. In this case we
would need to use a selector on fields on top of record store

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
pau-not-paul 2024-11-21 14:02:09 +01:00 committed by GitHub
parent 3c5eb539bb
commit ae4fb7d113
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 327 additions and 135 deletions

View File

@ -108,5 +108,6 @@ export const actorFieldDefinition: FieldDefinition<FieldActorMetadata> = {
defaultValue: { source: 'MANUAL', name: '' }, defaultValue: { source: 'MANUAL', name: '' },
metadata: { metadata: {
fieldName: 'actor', fieldName: 'actor',
objectMetadataNameSingular: 'person',
}, },
}; };

View File

@ -1,50 +0,0 @@
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import {
actorFieldDefinition,
phonesFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
const recordId = 'recordId';
const getWrapper =
(fieldDefinition: FieldDefinition<FieldMetadata>) =>
({ children }: { children: ReactNode }) => (
<FieldContext.Provider
value={{
fieldDefinition,
recordId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
<RecoilRoot>{children}</RecoilRoot>
</FieldContext.Provider>
);
const ActorWrapper = getWrapper(actorFieldDefinition);
const PhoneWrapper = getWrapper(phonesFieldDefinition);
describe('useIsFieldReadOnly', () => {
it('should return true', () => {
const { result } = renderHook(() => useIsFieldReadOnly(), {
wrapper: ActorWrapper,
});
expect(result.current).toBe(true);
});
it('should return false', () => {
const { result } = renderHook(() => useIsFieldReadOnly(), {
wrapper: PhoneWrapper,
});
expect(result.current).toBe(false);
});
});

View File

@ -0,0 +1,74 @@
import { renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import {
actorFieldDefinition,
phonesFieldDefinition,
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { JestRecordStoreSetter } from '~/testing/jest/JestRecordStoreSetter';
import { useIsFieldValueReadOnly } from '../useIsFieldValueReadOnly';
const recordId = 'recordId';
const getWrapper =
(fieldDefinition: FieldDefinition<FieldMetadata>, isRecordDeleted: boolean) =>
({ children }: { children: ReactNode }) => {
return (
<RecoilRoot>
<JestObjectMetadataItemSetter>
<JestRecordStoreSetter
records={[
{
id: recordId,
deletedAt: isRecordDeleted ? new Date().toISOString() : null,
__typename: 'standardObject',
} as ObjectRecord,
]}
>
<FieldContext.Provider
value={{
fieldDefinition,
recordId,
hotkeyScope: 'hotkeyScope',
isLabelIdentifier: false,
}}
>
{children}
</FieldContext.Provider>
</JestRecordStoreSetter>
</JestObjectMetadataItemSetter>
</RecoilRoot>
);
};
describe('useIsFieldValueReadOnly', () => {
it('should take fieldDefinition into account', () => {
const { result } = renderHook(() => useIsFieldValueReadOnly(), {
wrapper: getWrapper(phonesFieldDefinition, false),
});
expect(result.current).toBe(false);
const { result: result2 } = renderHook(() => useIsFieldValueReadOnly(), {
wrapper: getWrapper(actorFieldDefinition, false),
});
expect(result2.current).toBe(true);
});
it('should take isRecordDeleted into account', () => {
const { result } = renderHook(() => useIsFieldValueReadOnly(), {
wrapper: getWrapper(phonesFieldDefinition, true),
});
expect(result.current).toBe(true);
});
});

View File

@ -1,18 +0,0 @@
import { useContext } from 'react';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldMetadataReadOnly } from '../utils/isFieldMetadataReadOnly';
export const useIsFieldReadOnly = () => {
const { fieldDefinition } = useContext(FieldContext);
const { metadata } = fieldDefinition;
return (
isFieldActor(fieldDefinition) ||
isFieldRichText(fieldDefinition) ||
isFieldMetadataReadOnly(metadata)
);
};

View File

@ -0,0 +1,30 @@
import { useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilValue } from 'recoil';
import { FieldContext } from '../contexts/FieldContext';
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
export const useIsFieldValueReadOnly = () => {
const { fieldDefinition, recordId } = useContext(FieldContext);
const { metadata, type } = fieldDefinition;
const recordFromStore = useRecoilValue<ObjectRecord | null>(
recordStoreFamilyState(recordId),
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: metadata.objectMetadataNameSingular ?? '',
});
return isFieldValueReadOnly({
objectNameSingular: metadata.objectMetadataNameSingular,
fieldName: metadata.fieldName,
fieldType: type,
isObjectRemote: objectMetadataItem.isRemote,
isRecordDeleted: recordFromStore?.deletedAt,
});
};

View File

@ -0,0 +1,113 @@
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { FieldMetadataType } from '~/generated/graphql';
describe('isFieldValueReadOnly', () => {
it('should return true if fieldName is noteTargets or taskTargets', () => {
const result = isFieldValueReadOnly({
fieldName: 'noteTargets',
});
expect(result).toBe(true);
const result2 = isFieldValueReadOnly({
fieldName: 'taskTargets',
});
expect(result2).toBe(true);
});
it('should return false if fieldName is not noteTargets or taskTargets', () => {
const result = isFieldValueReadOnly({
fieldName: 'test',
});
expect(result).toBe(false);
});
it('should return true if isObjectRemote is true', () => {
const result = isFieldValueReadOnly({
isObjectRemote: true,
});
expect(result).toBe(true);
});
it('should return false if isObjectRemote is false', () => {
const result = isFieldValueReadOnly({
isObjectRemote: false,
});
expect(result).toBe(false);
});
it('should return true if isRecordDeleted is true', () => {
const result = isFieldValueReadOnly({
isRecordDeleted: true,
});
expect(result).toBe(true);
});
it('should return false if isRecordDeleted is false', () => {
const result = isFieldValueReadOnly({
isRecordDeleted: false,
});
expect(result).toBe(false);
});
it('should return true if objectNameSingular is Workflow and fieldName is not name', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'workflow',
fieldName: 'test',
});
expect(result).toBe(true);
});
it('should return false if objectNameSingular is Workflow and fieldName is name', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'Workflow',
fieldName: 'name',
});
expect(result).toBe(false);
});
it('should return true if isWorkflowSubObjectMetadata is true', () => {
const result = isFieldValueReadOnly({
objectNameSingular: 'workflowVersion',
});
expect(result).toBe(true);
});
it('should return true if fieldType is FieldMetadataType.Actor', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.Actor,
});
expect(result).toBe(true);
});
it('should return true if fieldType is FieldMetadataType.RichText', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.RichText,
});
expect(result).toBe(true);
});
it('should return false if fieldType is not FieldMetadataType.Actor or FieldMetadataType.RichText', () => {
const result = isFieldValueReadOnly({
fieldType: FieldMetadataType.Text,
});
expect(result).toBe(false);
});
it('should return false if none of the conditions are met', () => {
const result = isFieldValueReadOnly({});
expect(result).toBe(false);
});
});

View File

@ -1,19 +0,0 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
export const isFieldMetadataReadOnly = (fieldMetadata: FieldMetadata) => {
if (
fieldMetadata.fieldName === 'noteTargets' ||
fieldMetadata.fieldName === 'taskTargets'
) {
return true;
}
return (
isWorkflowSubObjectMetadata(fieldMetadata.objectMetadataNameSingular) ||
(fieldMetadata.objectMetadataNameSingular ===
CoreObjectNameSingular.Workflow &&
fieldMetadata.fieldName !== 'name')
);
};

View File

@ -0,0 +1,54 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
import { isDefined } from 'twenty-ui';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type isFieldValueReadOnlyParams = {
objectNameSingular?: string;
fieldName?: string;
fieldType?: FieldMetadataType;
isObjectRemote?: boolean;
isRecordDeleted?: boolean;
};
export const isFieldValueReadOnly = ({
objectNameSingular,
fieldName,
fieldType,
isObjectRemote = false,
isRecordDeleted = false,
}: isFieldValueReadOnlyParams) => {
if (fieldName === 'noteTargets' || fieldName === 'taskTargets') {
return true;
}
if (isObjectRemote) {
return true;
}
if (isRecordDeleted) {
return true;
}
if (isWorkflowSubObjectMetadata(objectNameSingular)) {
return true;
}
if (
objectNameSingular === CoreObjectNameSingular.Workflow &&
fieldName !== 'name'
) {
return true;
}
if (
isDefined(fieldType) &&
(isFieldActor({ type: fieldType }) || isFieldRichText({ type: fieldType }))
) {
return true;
}
return false;
};

View File

@ -36,6 +36,7 @@ export const useRecordBoardRecordGqlFields = ({
const recordGqlFields: Record<string, any> = { const recordGqlFields: Record<string, any> = {
id: true, id: true,
deletedAt: true,
...Object.fromEntries( ...Object.fromEntries(
visibleFieldDefinitions.map((visibleFieldDefinition) => [ visibleFieldDefinitions.map((visibleFieldDefinition) => [
visibleFieldDefinition.metadata.fieldName, visibleFieldDefinition.metadata.fieldName,

View File

@ -41,6 +41,7 @@ export const useRecordTableRecordGqlFields = ({
const recordGqlFields: Record<string, any> = { const recordGqlFields: Record<string, any> = {
id: true, id: true,
deletedAt: true,
...Object.fromEntries( ...Object.fromEntries(
visibleTableColumns.map((column) => [column.metadata.fieldName, true]), visibleTableColumns.map((column) => [column.metadata.fieldName, true]),
), ),

View File

@ -13,7 +13,7 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types
import { useInlineCell } from '../hooks/useInlineCell'; import { useInlineCell } from '../hooks/useInlineCell';
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId'; import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { RecordInlineCellContainer } from './RecordInlineCellContainer'; import { RecordInlineCellContainer } from './RecordInlineCellContainer';
import { import {
@ -26,21 +26,16 @@ type RecordInlineCellProps = {
loading?: boolean; loading?: boolean;
}; };
export const RecordInlineCell = ({ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
readonly,
loading,
}: RecordInlineCellProps) => {
const { fieldDefinition, recordId, isCentered } = useContext(FieldContext); const { fieldDefinition, recordId, isCentered } = useContext(FieldContext);
const buttonIcon = useGetButtonIcon(); const buttonIcon = useGetButtonIcon();
const isFieldInputOnly = useIsFieldInputOnly(); const isFieldInputOnly = useIsFieldInputOnly();
const isFieldReadOnly = useIsFieldReadOnly(); const isFieldReadOnly = useIsFieldValueReadOnly();
const { closeInlineCell } = useInlineCell(); const { closeInlineCell } = useInlineCell();
const cellIsReadOnly = readonly || isFieldReadOnly;
const handleEnter: FieldInputEvent = (persistField) => { const handleEnter: FieldInputEvent = (persistField) => {
persistField(); persistField();
closeInlineCell(); closeInlineCell();
@ -77,7 +72,7 @@ export const RecordInlineCell = ({
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const RecordInlineCellContextValue: RecordInlineCellContextProps = { const RecordInlineCellContextValue: RecordInlineCellContextProps = {
readonly: cellIsReadOnly, readonly: isFieldReadOnly,
buttonIcon: buttonIcon, buttonIcon: buttonIcon,
customEditHotkeyScope: isFieldRelation(fieldDefinition) customEditHotkeyScope: isFieldRelation(fieldDefinition)
? { scope: RelationPickerHotkeyScope.RelationPicker } ? { scope: RelationPickerHotkeyScope.RelationPicker }
@ -102,7 +97,7 @@ export const RecordInlineCell = ({
onTab={handleTab} onTab={handleTab}
onShiftTab={handleShiftTab} onShiftTab={handleShiftTab}
onClickOutside={handleClickOutside} onClickOutside={handleClickOutside}
isReadOnly={cellIsReadOnly} isReadOnly={isFieldReadOnly}
/> />
), ),
displayModeContent: <FieldDisplay />, displayModeContent: <FieldDisplay />,

View File

@ -84,7 +84,6 @@ export const FieldsCard = ({
fieldMetadataItem.name === 'taskTargets') fieldMetadataItem.name === 'taskTargets')
), ),
); );
const isReadOnly = objectMetadataItem.isRemote;
return ( return (
<> <>
@ -149,10 +148,7 @@ export const FieldsCard = ({
hotkeyScope: InlineCellHotkeyScope.InlineCell, hotkeyScope: InlineCellHotkeyScope.InlineCell,
}} }}
> >
<RecordInlineCell <RecordInlineCell loading={recordLoading} />
loading={recordLoading}
readonly={isReadOnly}
/>
</FieldContext.Provider> </FieldContext.Provider>
))} ))}
</> </>

View File

@ -1,6 +1,7 @@
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon'; import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { RightDrawerTitleRecordInlineCell } from '@/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell'; import { RightDrawerTitleRecordInlineCell } from '@/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell';
@ -29,7 +30,6 @@ export const SummaryCard = ({
const { const {
recordFromStore, recordFromStore,
recordLoading, recordLoading,
objectMetadataItem,
labelIdentifierFieldMetadataItem, labelIdentifierFieldMetadataItem,
isPrefetchLoading, isPrefetchLoading,
recordIdentifier, recordIdentifier,
@ -47,7 +47,11 @@ export const SummaryCard = ({
const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular); const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular);
const isMobile = useIsMobile() || isInRightDrawer; const isMobile = useIsMobile() || isInRightDrawer;
const isReadOnly = objectMetadataItem.isRemote;
const isReadOnly = isFieldValueReadOnly({
objectNameSingular,
isRecordDeleted: recordFromStore?.isDeleted,
});
if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) { if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) {
return <ShowPageSummaryCardSkeletonLoader />; return <ShowPageSummaryCardSkeletonLoader />;

View File

@ -25,9 +25,9 @@ import {
RecordUpdateHook, RecordUpdateHook,
RecordUpdateHookParams, RecordUpdateHookParams,
} from '@/object-record/record-field/contexts/FieldContext'; } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldMetadataReadOnly } from '@/object-record/record-field/utils/isFieldMetadataReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
@ -189,7 +189,7 @@ export const RecordDetailRelationRecordsListItem = ({
[isExpanded], [isExpanded],
); );
const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata); const isReadOnly = useIsFieldValueReadOnly();
return ( return (
<> <>
@ -206,7 +206,7 @@ export const RecordDetailRelationRecordsListItem = ({
accent="tertiary" accent="tertiary"
/> />
</StyledClickableZone> </StyledClickableZone>
{canEdit && ( {!isReadOnly && (
<DropdownScope dropdownScopeId={dropdownScopeId}> <DropdownScope dropdownScopeId={dropdownScopeId}>
<Dropdown <Dropdown
dropdownId={dropdownScopeId} dropdownId={dropdownScopeId}

View File

@ -7,11 +7,11 @@ import { IconForbid, IconPencil, IconPlus, LightIconButton } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect'; import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { isFieldMetadataReadOnly } from '@/object-record/record-field/utils/isFieldMetadataReadOnly';
import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList'; import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList';
import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection'; import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection';
import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader'; import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader';
@ -34,7 +34,6 @@ import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams
import { View } from '@/views/types/View'; import { View } from '@/views/types/View';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { RelationDefinitionType } from '~/generated-metadata/graphql';
type RecordDetailRelationSectionProps = { type RecordDetailRelationSectionProps = {
loading: boolean; loading: boolean;
}; };
@ -158,7 +157,7 @@ export const RecordDetailRelationSection = ({
recordId, recordId,
}); });
const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata); const isReadOnly = useIsFieldValueReadOnly();
if (loading) return null; if (loading) return null;
@ -180,7 +179,7 @@ export const RecordDetailRelationSection = ({
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile} hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
areRecordsAvailable={relationRecords.length > 0} areRecordsAvailable={relationRecords.length > 0}
rightAdornment={ rightAdornment={
canEdit && ( !isReadOnly && (
<DropdownScope dropdownScopeId={dropdownId}> <DropdownScope dropdownScopeId={dropdownId}>
<StyledAddDropdown <StyledAddDropdown
dropdownId={dropdownId} dropdownId={dropdownId}

View File

@ -94,7 +94,6 @@ const meta: Meta = {
mockPerformance.entityValue.__typename.toLocaleLowerCase(), mockPerformance.entityValue.__typename.toLocaleLowerCase(),
}) + mockPerformance.recordId, }) + mockPerformance.recordId,
isSelected: false, isSelected: false,
isReadOnly: false,
isDragging: false, isDragging: false,
dragHandleProps: null, dragHandleProps: null,
inView: true, inView: true,

View File

@ -7,7 +7,6 @@ export type RecordTableRowContextProps = {
recordId: string; recordId: string;
rowIndex: number; rowIndex: number;
isSelected: boolean; isSelected: boolean;
isReadOnly: boolean;
isPendingRow?: boolean; isPendingRow?: boolean;
isDragging: boolean; isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps | null; dragHandleProps: DraggableProvidedDragHandleProps | null;

View File

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { FieldInput } from '@/object-record/record-field/components/FieldInput'; import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent'; import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId'; import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
@ -21,7 +21,7 @@ export const RecordTableCellFieldInput = () => {
useContext(RecordTableContext); useContext(RecordTableContext);
const { recordId, fieldDefinition } = useContext(FieldContext); const { recordId, fieldDefinition } = useContext(FieldContext);
const isFieldReadOnly = useIsFieldReadOnly(); const isFieldReadOnly = useIsFieldValueReadOnly();
const handleEnter: FieldInputEvent = (persistField) => { const handleEnter: FieldInputEvent = (persistField) => {
onUpsertRecord({ onUpsertRecord({

View File

@ -10,7 +10,6 @@ import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEm
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly'; import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput'; import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext'; import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode'; import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton'; import { RecordTableCellButton } from '@/object-record/record-table/record-table-cell/components/RecordTableCellButton';
import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell'; import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell';
@ -22,7 +21,7 @@ import { isDefined } from '~/utils/isDefined';
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer'; import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
type RecordTableCellSoftFocusModeProps = { type RecordTableCellSoftFocusModeProps = {
@ -36,11 +35,8 @@ export const RecordTableCellSoftFocusMode = ({
}: RecordTableCellSoftFocusModeProps) => { }: RecordTableCellSoftFocusModeProps) => {
const { columnIndex } = useContext(RecordTableCellContext); const { columnIndex } = useContext(RecordTableCellContext);
const closeCurrentTableCell = useCloseCurrentTableCellInEditMode(); const closeCurrentTableCell = useCloseCurrentTableCellInEditMode();
const { isReadOnly: isRowReadOnly } = useContext(RecordTableRowContext);
const isFieldReadOnly = useIsFieldReadOnly(); const isFieldReadOnly = useIsFieldValueReadOnly();
const isCellReadOnly = isFieldReadOnly || isRowReadOnly;
const { openTableCell } = useOpenRecordTableCellFromCell(); const { openTableCell } = useOpenRecordTableCellFromCell();
@ -78,7 +74,7 @@ export const RecordTableCellSoftFocusMode = ({
useScopedHotkeys( useScopedHotkeys(
Key.Enter, Key.Enter,
() => { () => {
if (isCellReadOnly) { if (isFieldReadOnly) {
return; return;
} }
@ -95,7 +91,7 @@ export const RecordTableCellSoftFocusMode = ({
useScopedHotkeys( useScopedHotkeys(
'*', '*',
(keyboardEvent) => { (keyboardEvent) => {
if (isCellReadOnly) { if (isFieldReadOnly) {
return; return;
} }
@ -124,7 +120,7 @@ export const RecordTableCellSoftFocusMode = ({
); );
const handleClick = () => { const handleClick = () => {
if (!isFieldInputOnly && !isCellReadOnly) { if (!isFieldInputOnly && !isFieldReadOnly) {
openTableCell(); openTableCell();
} }
}; };
@ -156,9 +152,9 @@ export const RecordTableCellSoftFocusMode = ({
isDefined(buttonIcon) && isDefined(buttonIcon) &&
!editModeContentOnly && !editModeContentOnly &&
(!isFirstColumn || !isEmpty) && (!isFirstColumn || !isEmpty) &&
!isCellReadOnly; !isFieldReadOnly;
const dontShowContent = isEmpty && isCellReadOnly; const dontShowContent = isEmpty && isFieldReadOnly;
return ( return (
<> <>

View File

@ -8,7 +8,6 @@ export const recordTableRow: RecordTableRowContextProps = {
recordId: 'recordId', recordId: 'recordId',
pathToShowPage: '/', pathToShowPage: '/',
objectNameSingular: 'objectNameSingular', objectNameSingular: 'objectNameSingular',
isReadOnly: false,
dragHandleProps: {} as any, dragHandleProps: {} as any,
isDragging: false, isDragging: false,
inView: true, inView: true,

View File

@ -1,7 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly'; import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext'; import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
@ -32,11 +32,10 @@ export const useOpenRecordTableCellFromCell = () => {
const cellPosition = useCurrentTableCellPosition(); const cellPosition = useCurrentTableCellPosition();
const customCellHotkeyScope = useContext(CellHotkeyScopeContext); const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const { recordId, fieldDefinition } = useContext(FieldContext); const { recordId, fieldDefinition } = useContext(FieldContext);
const { isReadOnly, pathToShowPage, objectNameSingular } = useContext( const { pathToShowPage, objectNameSingular } = useContext(
RecordTableRowContext, RecordTableRowContext,
); );
const isFieldReadOnly = useIsFieldReadOnly(); const isFieldReadOnly = useIsFieldValueReadOnly();
const cellIsReadOnly = isReadOnly || isFieldReadOnly;
const openTableCell = ( const openTableCell = (
initialValue?: string, initialValue?: string,
@ -47,7 +46,7 @@ export const useOpenRecordTableCellFromCell = () => {
customCellHotkeyScope, customCellHotkeyScope,
recordId, recordId,
fieldDefinition, fieldDefinition,
isReadOnly: cellIsReadOnly, isReadOnly: isFieldReadOnly,
pathToShowPage, pathToShowPage,
objectNameSingular, objectNameSingular,
initialValue, initialValue,

View File

@ -12,7 +12,7 @@ import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/
import { tableCellWidthsComponentState } from '@/object-record/record-table/states/tableCellWidthsComponentState'; import { tableCellWidthsComponentState } from '@/object-record/record-table/states/tableCellWidthsComponentState';
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
export const RecordTableRowWrapper = ({ export const RecordTableRowWrapper = ({
recordId, recordId,
@ -48,7 +48,7 @@ export const RecordTableRowWrapper = ({
rootMargin: '1000px', rootMargin: '1000px',
}); });
const [, setTableCellWidths] = useRecoilComponentStateV2( const setTableCellWidths = useSetRecoilComponentStateV2(
tableCellWidthsComponentState, tableCellWidthsComponentState,
); );
@ -108,7 +108,6 @@ export const RecordTableRowWrapper = ({
}) + recordId, }) + recordId,
objectNameSingular: objectMetadataItem.nameSingular, objectNameSingular: objectMetadataItem.nameSingular,
isSelected: currentRowSelected, isSelected: currentRowSelected,
isReadOnly: objectMetadataItem.isRemote ?? false,
isPendingRow, isPendingRow,
isDragging: draggableSnapshot.isDragging, isDragging: draggableSnapshot.isDragging,
dragHandleProps: draggableProvided.dragHandleProps, dragHandleProps: draggableProvided.dragHandleProps,

View File

@ -0,0 +1,20 @@
import { ReactNode, useEffect } from 'react';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
export const JestRecordStoreSetter = ({
children,
records,
}: {
children: ReactNode;
records: ObjectRecord[];
}) => {
const { upsertRecords } = useUpsertRecordsInStore();
useEffect(() => {
upsertRecords(records);
});
return <>{children}</>;
};