Remove overlay-scroll-bar (#11258)
## What - Deprecate overlayscrollbars as we decided to follow the native behavior - rework on performances (avoid calling recoil states too much at field level which is quite expensive) - Also implements: https://github.com/twentyhq/core-team-issues/issues/569 --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -138,8 +138,6 @@
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"nodemailer": "^6.9.8",
|
||||
"openapi-types": "^12.1.3",
|
||||
"overlayscrollbars": "^2.6.1",
|
||||
"overlayscrollbars-react": "^0.5.4",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigation } from 'react-router-dom';
|
||||
|
||||
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
|
||||
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
/**
|
||||
* @deprecated We should now use useScrollToPosition instead
|
||||
* Note that `location.key` is used in the cache key, not `location.pathname`,
|
||||
* so the same path navigated to at different points in the history stack will
|
||||
* not share the same scroll position.
|
||||
*/
|
||||
export const useScrollRestoration = (viewportHeight?: number) => {
|
||||
const { state } = useNavigation();
|
||||
|
||||
const [scrollTop, setScrollTop] = useRecoilComponentStateV2(
|
||||
scrollWrapperScrollTopComponentState,
|
||||
);
|
||||
|
||||
const overlayScrollbars = useRecoilComponentValueV2(
|
||||
scrollWrapperInstanceComponentState,
|
||||
);
|
||||
|
||||
const scrollWrapper = overlayScrollbars?.elements().viewport;
|
||||
const skip = isDefined(viewportHeight) && scrollTop > viewportHeight;
|
||||
|
||||
useEffect(() => {
|
||||
if (state === 'loading') {
|
||||
setScrollTop(scrollWrapper?.scrollTop ?? 0);
|
||||
} else if (state === 'idle' && isDefined(scrollWrapper) && !skip) {
|
||||
scrollWrapper.scrollTo({ top: scrollTop });
|
||||
}
|
||||
}, [state, scrollWrapper, skip, scrollTop, setScrollTop]);
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
|
||||
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const useScrollToPosition = () => {
|
||||
const scrollWrapperInstanceState = useRecoilComponentCallbackStateV2(
|
||||
scrollWrapperInstanceComponentState,
|
||||
);
|
||||
|
||||
const scrollToPosition = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(scrollPositionInPx: number) => {
|
||||
const overlayScrollbars = snapshot
|
||||
.getLoadable(scrollWrapperInstanceState)
|
||||
.getValue();
|
||||
|
||||
const scrollWrapper = overlayScrollbars?.elements().viewport;
|
||||
|
||||
scrollWrapper?.scrollTo({ top: scrollPositionInPx });
|
||||
},
|
||||
[scrollWrapperInstanceState],
|
||||
);
|
||||
|
||||
return { scrollToPosition };
|
||||
};
|
||||
@ -17,10 +17,6 @@ form {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
html, body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
/* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */
|
||||
.grecaptcha-badge {
|
||||
visibility: hidden !important;
|
||||
|
||||
@ -107,6 +107,7 @@ export const CalendarEventDetails = ({
|
||||
}),
|
||||
useUpdateRecord: () => [() => undefined, { loading: false }],
|
||||
maxWidth: 300,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
@ -8,17 +8,14 @@ import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttac
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
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 { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
@ -26,6 +23,7 @@ import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/com
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CommandMenuHotkeyScope } from '@/command-menu/types/CommandMenuHotkeyScope';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
import { PartialBlock } from '@blocknote/core';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
@ -56,17 +54,11 @@ export const ActivityRichTextEditor = ({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const contextStoreCurrentViewType = useRecoilComponentValueV2(
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
const isRecordReadOnly = useIsRecordReadOnly({ recordId: activityId });
|
||||
|
||||
const isReadOnly = isFieldValueReadOnly({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
hasObjectReadOnlyPermission,
|
||||
contextStoreCurrentViewType,
|
||||
isRecordDeleted: activityInStore?.deletedAt !== null,
|
||||
isRecordReadOnly,
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
@ -196,10 +196,7 @@ export const AttachmentList = ({
|
||||
</StyledHeader>
|
||||
</StyledModalHeader>
|
||||
<ScrollWrapper
|
||||
contextProviderName="modalContent"
|
||||
componentInstanceId={`preview-modal-${previewedAttachment.id}`}
|
||||
scrollbarVariant="no-padding"
|
||||
heightMode="fit-content"
|
||||
>
|
||||
<StyledModalContent>
|
||||
<Suspense
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useActivityTargetObjectRecords = (
|
||||
@ -25,49 +23,11 @@ export const useActivityTargetObjectRecords = (
|
||||
return { activityTargetObjectRecords: [] };
|
||||
}
|
||||
|
||||
const targets = activityTargets
|
||||
? activityTargets
|
||||
: activity && 'noteTargets' in activity && activity.noteTargets
|
||||
? activity.noteTargets
|
||||
: activity && 'taskTargets' in activity && activity.taskTargets
|
||||
? activity.taskTargets
|
||||
: [];
|
||||
|
||||
const activityTargetObjectRecords = targets
|
||||
.map<ActivityTargetWithTargetRecord | undefined>((activityTarget) => {
|
||||
if (!isDefined(activityTarget)) {
|
||||
throw new Error(`Cannot find activity target`);
|
||||
}
|
||||
|
||||
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
|
||||
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
|
||||
objectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||
),
|
||||
);
|
||||
|
||||
if (!correspondingObjectMetadataItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const targetObjectRecord = activityTarget[
|
||||
correspondingObjectMetadataItem.nameSingular
|
||||
] as ObjectRecord | undefined;
|
||||
|
||||
if (!isDefined(targetObjectRecord)) {
|
||||
throw new Error(
|
||||
`Cannot find target object record of type ${correspondingObjectMetadataItem.nameSingular}, make sure the request for activities eagerly loads for the target objects on activity target relation.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
activityTarget,
|
||||
targetObject: targetObjectRecord,
|
||||
targetObjectMetadataItem: correspondingObjectMetadataItem,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
const activityTargetObjectRecords = getActivityTargetObjectRecords({
|
||||
activityRecord: activity as Note | Task,
|
||||
objectMetadataItems,
|
||||
activityTargets,
|
||||
});
|
||||
|
||||
return {
|
||||
activityTargetObjectRecords,
|
||||
|
||||
@ -2,13 +2,12 @@ import { useContext } from 'react';
|
||||
|
||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { useOpenActivityTargetInlineCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetInlineCellEditMode';
|
||||
import { useUpdateActivityTargetFromInlineCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromInlineCell';
|
||||
import { useOpenActivityTargetCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode';
|
||||
import { useUpdateActivityTargetFromCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromCell';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldContextProvider } from '@/object-record/record-field/components/FieldContextProvider';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RecordInlineCellContainer } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||
import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext';
|
||||
@ -39,18 +38,15 @@ export const ActivityTargetsInlineCell = ({
|
||||
|
||||
const { closeInlineCell } = useInlineCell(componentInstanceId);
|
||||
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
const { fieldDefinition, isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly();
|
||||
const { openActivityTargetCellEditMode } =
|
||||
useOpenActivityTargetCellEditMode();
|
||||
|
||||
const { openActivityTargetInlineCellEditMode } =
|
||||
useOpenActivityTargetInlineCellEditMode();
|
||||
|
||||
const { updateActivityTargetFromInlineCell } =
|
||||
useUpdateActivityTargetFromInlineCell({
|
||||
activityObjectNameSingular,
|
||||
activityId: activityRecordId,
|
||||
});
|
||||
const { updateActivityTargetFromCell } = useUpdateActivityTargetFromCell({
|
||||
activityObjectNameSingular,
|
||||
activityId: activityRecordId,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
@ -73,7 +69,7 @@ export const ActivityTargetsInlineCell = ({
|
||||
MultipleRecordPickerHotkeyScope.MultipleRecordPicker,
|
||||
IconLabel: showLabel ? IconArrowUpRight : undefined,
|
||||
showLabel: showLabel,
|
||||
readonly: isFieldReadOnly,
|
||||
readonly: isReadOnly,
|
||||
labelWidth: fieldDefinition?.labelWidth,
|
||||
editModeContent: (
|
||||
<MultipleRecordPicker
|
||||
@ -82,7 +78,7 @@ export const ActivityTargetsInlineCell = ({
|
||||
closeInlineCell();
|
||||
}}
|
||||
onChange={(morphItem) => {
|
||||
updateActivityTargetFromInlineCell({
|
||||
updateActivityTargetFromCell({
|
||||
recordPickerInstanceId: componentInstanceId,
|
||||
morphItem,
|
||||
activityTargetWithTargetRecords:
|
||||
@ -102,7 +98,7 @@ export const ActivityTargetsInlineCell = ({
|
||||
/>
|
||||
),
|
||||
onOpenEditMode: () => {
|
||||
openActivityTargetInlineCellEditMode({
|
||||
openActivityTargetCellEditMode({
|
||||
recordPickerInstanceId: componentInstanceId,
|
||||
activityTargetObjectRecords,
|
||||
});
|
||||
|
||||
@ -7,21 +7,22 @@ import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/
|
||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type OpenActivityTargetInlineCellEditModeProps = {
|
||||
type OpenActivityTargetCellEditModeProps = {
|
||||
recordPickerInstanceId: string;
|
||||
activityTargetObjectRecords: ActivityTargetWithTargetRecord[];
|
||||
};
|
||||
|
||||
export const useOpenActivityTargetInlineCellEditMode = () => {
|
||||
// TODO: deprecate this once we are supporting one to many through relations
|
||||
export const useOpenActivityTargetCellEditMode = () => {
|
||||
const { performSearch: multipleRecordPickerPerformSearch } =
|
||||
useMultipleRecordPickerPerformSearch();
|
||||
|
||||
const openActivityTargetInlineCellEditMode = useRecoilCallback(
|
||||
const openActivityTargetCellEditMode = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
({
|
||||
recordPickerInstanceId,
|
||||
activityTargetObjectRecords,
|
||||
}: OpenActivityTargetInlineCellEditModeProps) => {
|
||||
}: OpenActivityTargetCellEditModeProps) => {
|
||||
const objectMetadataItems = snapshot
|
||||
.getLoadable(objectMetadataItemsState)
|
||||
.getValue()
|
||||
@ -82,5 +83,5 @@ export const useOpenActivityTargetInlineCellEditMode = () => {
|
||||
[multipleRecordPickerPerformSearch],
|
||||
);
|
||||
|
||||
return { openActivityTargetInlineCellEditMode };
|
||||
return { openActivityTargetCellEditMode };
|
||||
};
|
||||
@ -11,16 +11,17 @@ import { RecordPickerPickableMorphItem } from '@/object-record/record-picker/typ
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
type UpdateActivityTargetFromInlineCellProps = {
|
||||
type UpdateActivityTargetFromCellProps = {
|
||||
recordPickerInstanceId: string;
|
||||
morphItem: RecordPickerPickableMorphItem;
|
||||
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
||||
};
|
||||
|
||||
export const useUpdateActivityTargetFromInlineCell = ({
|
||||
// TODO: deprecate this hook once we implement one-to-many relation through
|
||||
export const useUpdateActivityTargetFromCell = ({
|
||||
activityObjectNameSingular,
|
||||
activityId,
|
||||
}: {
|
||||
@ -29,27 +30,37 @@ export const useUpdateActivityTargetFromInlineCell = ({
|
||||
| CoreObjectNameSingular.Task;
|
||||
activityId: string;
|
||||
}) => {
|
||||
const joinObjectNameSingular = getJoinObjectNameSingular(
|
||||
activityObjectNameSingular,
|
||||
);
|
||||
|
||||
const { createOneRecord: createOneActivityTarget } = useCreateOneRecord<
|
||||
NoteTarget | TaskTarget
|
||||
>({
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
objectNameSingular:
|
||||
joinObjectNameSingular === ''
|
||||
? activityObjectNameSingular
|
||||
: joinObjectNameSingular,
|
||||
});
|
||||
|
||||
const { deleteOneRecord: deleteOneActivityTarget } = useDeleteOneRecord({
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
objectNameSingular:
|
||||
joinObjectNameSingular === ''
|
||||
? activityObjectNameSingular
|
||||
: joinObjectNameSingular,
|
||||
});
|
||||
|
||||
const setActivityFromStore = useSetRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const updateActivityTargetFromInlineCell = useRecoilCallback(
|
||||
const updateActivityTargetFromCell = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async ({
|
||||
morphItem,
|
||||
activityTargetWithTargetRecords,
|
||||
recordPickerInstanceId,
|
||||
}: UpdateActivityTargetFromInlineCellProps) => {
|
||||
}: UpdateActivityTargetFromCellProps) => {
|
||||
const targetObjectName =
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? 'task'
|
||||
@ -179,5 +190,5 @@ export const useUpdateActivityTargetFromInlineCell = ({
|
||||
],
|
||||
);
|
||||
|
||||
return { updateActivityTargetFromInlineCell };
|
||||
return { updateActivityTargetFromCell };
|
||||
};
|
||||
@ -45,7 +45,6 @@ export const EventList = ({ events, targetableObject }: EventListProps) => {
|
||||
|
||||
return (
|
||||
<ScrollWrapper
|
||||
contextProviderName="eventList"
|
||||
componentInstanceId={`scroll-wrapper-event-list-${targetableObject.id}`}
|
||||
>
|
||||
<StyledTimelineContainer>
|
||||
|
||||
@ -54,6 +54,7 @@ export const EventFieldDiffValue = ({
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
},
|
||||
hotkeyScope: 'field-event-diff',
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<FieldDisplay />
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type GetActivityTargetObjectRecordsProps = {
|
||||
activityRecord: Note | Task;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
activityTargets?: NoteTarget[] | TaskTarget[];
|
||||
};
|
||||
|
||||
export const getActivityTargetObjectRecords = ({
|
||||
activityRecord,
|
||||
objectMetadataItems,
|
||||
activityTargets,
|
||||
}: GetActivityTargetObjectRecordsProps) => {
|
||||
if (!isDefined(activityRecord) && !isDefined(activityTargets)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targets = activityTargets
|
||||
? activityTargets
|
||||
: activityRecord &&
|
||||
'noteTargets' in activityRecord &&
|
||||
activityRecord.noteTargets
|
||||
? activityRecord.noteTargets
|
||||
: activityRecord &&
|
||||
'taskTargets' in activityRecord &&
|
||||
activityRecord.taskTargets
|
||||
? activityRecord.taskTargets
|
||||
: [];
|
||||
|
||||
const activityTargetObjectRecords = targets
|
||||
.map<ActivityTargetWithTargetRecord | undefined>((activityTarget) => {
|
||||
if (!isDefined(activityTarget)) {
|
||||
throw new Error(`Cannot find activity target`);
|
||||
}
|
||||
|
||||
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
isDefined(activityTarget[objectMetadataItem.nameSingular]) &&
|
||||
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
|
||||
objectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||
),
|
||||
);
|
||||
|
||||
if (!correspondingObjectMetadataItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const targetObjectRecord = activityTarget[
|
||||
correspondingObjectMetadataItem.nameSingular
|
||||
] as ObjectRecord | undefined;
|
||||
|
||||
if (!isDefined(targetObjectRecord)) {
|
||||
throw new Error(
|
||||
`Cannot find target object record of type ${correspondingObjectMetadataItem.nameSingular}, make sure the request for activities eagerly loads for the target objects on activity target relation.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
activityTarget,
|
||||
targetObject: targetObjectRecord,
|
||||
targetObjectMetadataItem: correspondingObjectMetadataItem,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
return activityTargetObjectRecords;
|
||||
};
|
||||
@ -12,10 +12,7 @@ type AuthModalProps = { children: React.ReactNode };
|
||||
|
||||
export const AuthModal = ({ children }: AuthModalProps) => (
|
||||
<Modal padding={'none'} modalVariant="primary">
|
||||
<ScrollWrapper
|
||||
contextProviderName="modalContent"
|
||||
componentInstanceId="scroll-wrapper-modal-content"
|
||||
>
|
||||
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
|
||||
<StyledContent>{children}</StyledContent>
|
||||
</ScrollWrapper>
|
||||
</Modal>
|
||||
|
||||
@ -79,10 +79,7 @@ export const CommandMenuList = ({
|
||||
<CommandMenuDefaultSelectionEffect
|
||||
selectableItemIds={selectableItemIds}
|
||||
/>
|
||||
<ScrollWrapper
|
||||
contextProviderName="commandMenu"
|
||||
componentInstanceId={`scroll-wrapper-command-menu`}
|
||||
>
|
||||
<ScrollWrapper componentInstanceId={`scroll-wrapper-command-menu`}>
|
||||
<StyledInnerList>
|
||||
<SelectableList
|
||||
selectableListId="command-menu-list"
|
||||
|
||||
@ -24,6 +24,7 @@ const StyledMainSection = styled(NavigationDrawerSection)`
|
||||
`;
|
||||
const StyledInnerContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const MainNavigationDrawerItems = () => {
|
||||
@ -66,10 +67,8 @@ export const MainNavigationDrawerItems = () => {
|
||||
</StyledMainSection>
|
||||
)}
|
||||
<ScrollWrapper
|
||||
contextProviderName="navigationDrawer"
|
||||
componentInstanceId={`scroll-wrapper-navigation-drawer`}
|
||||
defaultEnableXScroll={false}
|
||||
scrollbarVariant="no-padding"
|
||||
>
|
||||
<StyledInnerContainer>
|
||||
<NavigationDrawerOpenedSection />
|
||||
|
||||
@ -73,7 +73,7 @@ export const useAttachRelatedRecordFromRecord = ({
|
||||
getRelatedRecordFromCache<ObjectRecord>(relatedRecordId);
|
||||
|
||||
if (!cachedRelatedRecord) {
|
||||
throw new Error('could not find cached related record');
|
||||
throw new Error('Could not find cached related record');
|
||||
}
|
||||
|
||||
const previousRecordId = cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`];
|
||||
|
||||
@ -35,7 +35,6 @@ import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { ViewType } from '@/views/types/ViewType';
|
||||
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -61,11 +60,6 @@ const StyledBoardContentContainer = styled.div`
|
||||
height: calc(100% - 48px);
|
||||
`;
|
||||
|
||||
const RecordBoardScrollRestoreEffect = () => {
|
||||
useScrollRestoration();
|
||||
return null;
|
||||
};
|
||||
|
||||
export const RecordBoard = () => {
|
||||
const { updateOneRecord, selectFieldMetadataItem, recordBoardId } =
|
||||
useContext(RecordBoardContext);
|
||||
@ -239,7 +233,6 @@ export const RecordBoard = () => {
|
||||
value={{ instanceId: recordBoardId }}
|
||||
>
|
||||
<ScrollWrapper
|
||||
contextProviderName="recordBoard"
|
||||
componentInstanceId={`scroll-wrapper-record-board-${recordBoardId}`}
|
||||
>
|
||||
<RecordBoardStickyHeaderEffect />
|
||||
@ -258,7 +251,6 @@ export const RecordBoard = () => {
|
||||
</StyledColumnContainer>
|
||||
</DragDropContext>
|
||||
</StyledContainer>
|
||||
<RecordBoardScrollRestoreEffect />
|
||||
<DragSelect
|
||||
dragSelectable={boardRef}
|
||||
onDragSelectionEnd={handleDragSelectionEnd}
|
||||
|
||||
@ -16,7 +16,7 @@ import { RecordValueSetterEffect } from '@/object-record/record-store/components
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
@ -144,10 +144,10 @@ export const RecordBoardCard = () => {
|
||||
}
|
||||
}, 800);
|
||||
|
||||
const scrollWrapperRef = useContext(RecordBoardScrollWrapperContext);
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||
|
||||
const { ref: cardRef } = useInView({
|
||||
root: scrollWrapperRef?.ref.current,
|
||||
root: scrollWrapperHTMLElement,
|
||||
rootMargin: '1000px',
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
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 { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
@ -21,7 +22,7 @@ export const RecordBoardCardBody = ({
|
||||
}: {
|
||||
fieldDefinitions: RecordBoardFieldDefinition<FieldMetadata>[];
|
||||
}) => {
|
||||
const { recordId } = useContext(RecordBoardCardContext);
|
||||
const { recordId, isRecordReadOnly } = useContext(RecordBoardCardContext);
|
||||
|
||||
const { updateOneRecord } = useContext(RecordBoardContext);
|
||||
|
||||
@ -45,6 +46,13 @@ export const RecordBoardCardBody = ({
|
||||
recordId,
|
||||
maxWidth: 156,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: isFieldValueReadOnly({
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
fieldType: fieldDefinition.type,
|
||||
isRecordReadOnly,
|
||||
}),
|
||||
fieldDefinition: {
|
||||
disableTooltip: false,
|
||||
fieldMetadataId: fieldDefinition.fieldMetadataId,
|
||||
@ -60,6 +68,7 @@ export const RecordBoardCardBody = ({
|
||||
},
|
||||
useUpdateRecord: useUpdateOneRecordHook,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isDisplayModeFixHeight: true,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
|
||||
import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||
|
||||
export const RecordBoardCardDraggableContainer = ({
|
||||
recordId,
|
||||
@ -9,22 +11,28 @@ export const RecordBoardCardDraggableContainer = ({
|
||||
recordId: string;
|
||||
index: number;
|
||||
}) => {
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Draggable key={recordId} draggableId={recordId} index={index}>
|
||||
{(draggableProvided) => (
|
||||
<div
|
||||
ref={draggableProvided?.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided?.dragHandleProps}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided?.draggableProps}
|
||||
className="record-board-card"
|
||||
data-selectable-id={recordId}
|
||||
data-select-disable
|
||||
>
|
||||
<RecordBoardCard />
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<RecordBoardCardContext.Provider value={{ recordId, isRecordReadOnly }}>
|
||||
<Draggable key={recordId} draggableId={recordId} index={index}>
|
||||
{(draggableProvided) => (
|
||||
<div
|
||||
ref={draggableProvided?.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided?.dragHandleProps}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...draggableProvided?.draggableProps}
|
||||
className="record-board-card"
|
||||
data-selectable-id={recordId}
|
||||
data-select-disable
|
||||
>
|
||||
<RecordBoardCard />
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
</RecordBoardCardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { createContext } from 'react';
|
||||
|
||||
type RecordBoardCardContextProps = {
|
||||
recordId: string;
|
||||
isRecordReadOnly: boolean;
|
||||
};
|
||||
|
||||
export const RecordBoardCardContext =
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { RecordBoardCardDraggableContainer } from '@/object-record/record-board/record-board-card/components/RecordBoardCardDraggableContainer';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
|
||||
type RecordBoardColumnCardsMemoProps = {
|
||||
recordIds: string[];
|
||||
@ -10,9 +9,11 @@ type RecordBoardColumnCardsMemoProps = {
|
||||
export const RecordBoardColumnCardsMemo = React.memo(
|
||||
({ recordIds }: RecordBoardColumnCardsMemoProps) => {
|
||||
return recordIds.map((recordId, index) => (
|
||||
<RecordBoardCardContext.Provider value={{ recordId }} key={recordId}>
|
||||
<RecordBoardCardDraggableContainer recordId={recordId} index={index} />
|
||||
</RecordBoardCardContext.Provider>
|
||||
<RecordBoardCardDraggableContainer
|
||||
key={recordId}
|
||||
recordId={recordId}
|
||||
index={index}
|
||||
/>
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
@ -11,9 +11,9 @@ import { RecordGroupDefinitionType } from '@/object-record/record-group/types/Re
|
||||
import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { IconDotsVertical, IconPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
|
||||
@ -78,6 +78,7 @@ export const FieldContextProvider = ({
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
clearable,
|
||||
overridenIsFieldEmpty,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -32,6 +32,7 @@ export type GenericFieldContextType = {
|
||||
overridenIsFieldEmpty?: boolean;
|
||||
displayedMaxRows?: number;
|
||||
isDisplayModeFixHeight?: boolean;
|
||||
isReadOnly: boolean;
|
||||
onOpenEditMode?: () => void;
|
||||
onCloseEditMode?: () => void;
|
||||
isLabelHidden?: boolean;
|
||||
|
||||
@ -23,6 +23,7 @@ const getWrapper =
|
||||
recordId,
|
||||
hotkeyScope: 'hotkeyScope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
|
||||
@ -16,6 +16,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
recordId,
|
||||
hotkeyScope: 'hotkeyScope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
|
||||
@ -22,6 +22,7 @@ const getWrapper =
|
||||
recordId,
|
||||
hotkeyScope: 'hotkeyScope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
|
||||
@ -1,79 +1,28 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
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 { phonesFieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||
|
||||
import { useIsFieldValueReadOnly } from '../useIsFieldValueReadOnly';
|
||||
|
||||
const recordId = 'recordId';
|
||||
const mockInstanceId = 'mock-instance-id';
|
||||
|
||||
const getWrapper =
|
||||
(fieldDefinition: FieldDefinition<FieldMetadata>, isRecordDeleted: boolean) =>
|
||||
({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: mockInstanceId }}
|
||||
>
|
||||
<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>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useIsFieldValueReadOnly', () => {
|
||||
it('should take fieldDefinition into account', () => {
|
||||
const { result } = renderHook(() => useIsFieldValueReadOnly(), {
|
||||
wrapper: getWrapper(phonesFieldDefinition, false),
|
||||
});
|
||||
it('should return true if the field is read only', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIsFieldValueReadOnly({
|
||||
fieldDefinition: phonesFieldDefinition,
|
||||
isRecordReadOnly: 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),
|
||||
});
|
||||
it('should return true if the record is read only', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIsFieldValueReadOnly({
|
||||
fieldDefinition: phonesFieldDefinition,
|
||||
isRecordReadOnly: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
@ -105,6 +105,7 @@ const getWrapper =
|
||||
hotkeyScope: 'hotkeyScope',
|
||||
isLabelIdentifier: false,
|
||||
useUpdateRecord: useUpdateOneRecordMutation,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -342,6 +342,7 @@ const Wrapper = ({ children }: { children: ReactNode }) => {
|
||||
hotkeyScope: 'hotkeyScope',
|
||||
isLabelIdentifier: false,
|
||||
useUpdateRecord: useUpdateOneRecordMutation,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -1,45 +1,22 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldValueReadOnly } from '../utils/isFieldValueReadOnly';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useIsFieldValueReadOnly = () => {
|
||||
const { fieldDefinition, recordId } = useContext(FieldContext);
|
||||
type UseIsFieldValueReadOnlyParams = {
|
||||
isRecordReadOnly: boolean;
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
};
|
||||
|
||||
export const useIsFieldValueReadOnly = ({
|
||||
fieldDefinition,
|
||||
isRecordReadOnly,
|
||||
}: UseIsFieldValueReadOnlyParams) => {
|
||||
const { metadata, type } = fieldDefinition;
|
||||
|
||||
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: 'deletedAt',
|
||||
}),
|
||||
);
|
||||
|
||||
const contextStoreCurrentViewType = useRecoilComponentValueV2(
|
||||
contextStoreCurrentViewTypeComponentState,
|
||||
);
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: metadata.objectMetadataNameSingular ?? '',
|
||||
});
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
return isFieldValueReadOnly({
|
||||
objectNameSingular: metadata.objectMetadataNameSingular,
|
||||
fieldName: metadata.fieldName,
|
||||
fieldType: type,
|
||||
isObjectRemote: objectMetadataItem.isRemote,
|
||||
isRecordDeleted: isDefined(recordDeletedAt),
|
||||
hasObjectReadOnlyPermission,
|
||||
contextStoreCurrentViewType,
|
||||
isRecordReadOnly,
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseIsRecordReadOnlyParams = {
|
||||
recordId: string;
|
||||
};
|
||||
|
||||
export const useIsRecordReadOnly = ({
|
||||
recordId,
|
||||
}: UseIsRecordReadOnlyParams) => {
|
||||
const recordDeletedAt = useRecoilValue<ObjectRecord | null>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: 'deletedAt',
|
||||
}),
|
||||
);
|
||||
|
||||
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
|
||||
|
||||
return hasObjectReadOnlyPermission || isDefined(recordDeletedAt);
|
||||
};
|
||||
@ -1,9 +1,23 @@
|
||||
import { useOpenActivityTargetCellEditMode } from '@/activities/inline-cell/hooks/useOpenActivityTargetCellEditMode';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
|
||||
import { useOpenRelationToOneFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useOpenRelationToOneFieldInput';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import {
|
||||
FieldMetadata,
|
||||
FieldRelationFromManyValue,
|
||||
FieldRelationValue,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useOpenFieldInputEditMode = () => {
|
||||
@ -11,33 +25,82 @@ export const useOpenFieldInputEditMode = () => {
|
||||
const { openRelationFromManyFieldInput } =
|
||||
useOpenRelationFromManyFieldInput();
|
||||
|
||||
const openFieldInput = ({
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
}: {
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
recordId: string;
|
||||
}) => {
|
||||
if (isFieldRelationToOneObject(fieldDefinition)) {
|
||||
openRelationToOneFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
const { openActivityTargetCellEditMode } =
|
||||
useOpenActivityTargetCellEditMode();
|
||||
|
||||
if (isFieldRelationFromManyObjects(fieldDefinition)) {
|
||||
if (
|
||||
isDefined(fieldDefinition.metadata.relationObjectMetadataNameSingular)
|
||||
) {
|
||||
openRelationFromManyFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
objectNameSingular:
|
||||
const openFieldInput = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
}: {
|
||||
fieldDefinition: FieldDefinition<FieldMetadata>;
|
||||
recordId: string;
|
||||
}) => {
|
||||
if (isFieldRelationToOneObject(fieldDefinition)) {
|
||||
openRelationToOneFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isFieldRelationFromManyObjects(fieldDefinition) &&
|
||||
['taskTarget', 'noteTarget'].includes(
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
)
|
||||
) {
|
||||
const fieldValue = snapshot
|
||||
.getLoadable<FieldRelationValue<FieldRelationFromManyValue>>(
|
||||
recordStoreFamilySelector({
|
||||
recordId,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const activity = snapshot
|
||||
.getLoadable(recordStoreFamilyState(recordId))
|
||||
.getValue();
|
||||
|
||||
const objectMetadataItems = snapshot
|
||||
.getLoadable(objectMetadataItemsState)
|
||||
.getValue();
|
||||
|
||||
const activityTargetObjectRecords = getActivityTargetObjectRecords({
|
||||
activityRecord: activity as Task | Note,
|
||||
objectMetadataItems,
|
||||
activityTargets: fieldValue as NoteTarget[] | TaskTarget[],
|
||||
});
|
||||
|
||||
openActivityTargetCellEditMode({
|
||||
recordPickerInstanceId: `relation-from-many-field-input-${recordId}`,
|
||||
activityTargetObjectRecords,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFieldRelationFromManyObjects(fieldDefinition)) {
|
||||
if (
|
||||
isDefined(
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
)
|
||||
) {
|
||||
openRelationFromManyFieldInput({
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
objectNameSingular:
|
||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||
recordId: recordId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
openActivityTargetCellEditMode,
|
||||
openRelationFromManyFieldInput,
|
||||
openRelationToOneFieldInput,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
openFieldInput: openFieldInput,
|
||||
|
||||
@ -70,6 +70,7 @@ const meta: Meta = {
|
||||
...relationFromManyFieldDisplayMock.fieldDefinition,
|
||||
} as unknown as FieldDefinition<FieldMetadata>,
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RelationFieldValueSetterEffect />
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { useUpdateActivityTargetFromCell } from '@/activities/inline-cell/hooks/useUpdateActivityTargetFromCell';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/states/recordFieldInputLayoutDirectionComponentState';
|
||||
@ -22,11 +28,34 @@ export const RelationFromManyFieldInput = ({
|
||||
const recordPickerInstanceId = `relation-from-many-field-input-${recordId}`;
|
||||
|
||||
const { updateRelation } = useUpdateRelationFromManyFieldInput();
|
||||
const fieldName = fieldDefinition.metadata.fieldName;
|
||||
const objectMetadataNameSingular =
|
||||
fieldDefinition.metadata.objectMetadataNameSingular;
|
||||
|
||||
const { updateActivityTargetFromCell } = useUpdateActivityTargetFromCell({
|
||||
activityObjectNameSingular: objectMetadataNameSingular as
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task,
|
||||
activityId: recordId,
|
||||
});
|
||||
|
||||
const { fieldValue } = useRelationField();
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit?.(() => {});
|
||||
};
|
||||
|
||||
const isRelationFromActivityTargets =
|
||||
(fieldName === 'noteTargets' &&
|
||||
objectMetadataNameSingular === CoreObjectNameSingular.Note) ||
|
||||
(fieldName === 'taskTargets' &&
|
||||
objectMetadataNameSingular === CoreObjectNameSingular.Task);
|
||||
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
|
||||
recordId,
|
||||
fieldValue as NoteTarget[] | TaskTarget[],
|
||||
);
|
||||
|
||||
const relationFieldDefinition =
|
||||
fieldDefinition as FieldDefinition<FieldRelationMetadata>;
|
||||
|
||||
@ -57,8 +86,22 @@ export const RelationFromManyFieldInput = ({
|
||||
<MultipleRecordPicker
|
||||
componentInstanceId={recordPickerInstanceId}
|
||||
onSubmit={handleSubmit}
|
||||
onChange={updateRelation}
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
onChange={(morphItem) => {
|
||||
if (isRelationFromActivityTargets) {
|
||||
updateActivityTargetFromCell({
|
||||
morphItem,
|
||||
activityTargetWithTargetRecords: activityTargetObjectRecords,
|
||||
recordPickerInstanceId,
|
||||
});
|
||||
} else {
|
||||
updateRelation(morphItem);
|
||||
}
|
||||
}}
|
||||
onCreate={
|
||||
!isRelationFromActivityTargets
|
||||
? createNewRecordAndOpenRightDrawer
|
||||
: undefined
|
||||
}
|
||||
onClickOutside={handleSubmit}
|
||||
layoutDirection={
|
||||
layoutDirection === 'downward'
|
||||
|
||||
@ -75,6 +75,7 @@ const AddressInputWithContext = ({
|
||||
recordId: recordId ?? '123',
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<AddressValueSetterEffect value={value} />
|
||||
|
||||
@ -66,6 +66,7 @@ const BooleanFieldInputWithContext = ({
|
||||
recordId: recordId ?? '123',
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<BooleanFieldValueSetterEffect
|
||||
|
||||
@ -93,6 +93,7 @@ const DateFieldInputWithContext = ({
|
||||
recordId: '123',
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||
|
||||
@ -70,6 +70,7 @@ const NumberFieldInputWithContext = ({
|
||||
recordId: '123',
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||
|
||||
@ -68,6 +68,7 @@ const RatingFieldInputWithContext = ({
|
||||
recordId: recordId ?? '123',
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RatingFieldValueSetterEffect value={value} />
|
||||
|
||||
@ -95,6 +95,7 @@ const RelationManyFieldInputWithContext = () => {
|
||||
recordId: 'recordId',
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
|
||||
@ -88,6 +88,7 @@ const RelationToOneFieldInputWithContext = ({
|
||||
recordId: recordId,
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
@ -63,6 +63,7 @@ const TextFieldInputWithContext = ({
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||
|
||||
@ -1,142 +1,90 @@
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
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', () => {
|
||||
it('should return true if record is read only', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldName: 'noteTargets',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
|
||||
const result2 = isFieldValueReadOnly({
|
||||
fieldName: 'taskTargets',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
expect(result2).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if fieldName is noteTargets or taskTargets but is not in table or kanban view', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldName: 'noteTargets',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.ShowPage,
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
|
||||
const result2 = isFieldValueReadOnly({
|
||||
fieldName: 'taskTargets',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.ShowPage,
|
||||
});
|
||||
|
||||
expect(result2).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if fieldName is not noteTargets or taskTargets', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldName: 'test',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if isObjectRemote is true', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
isObjectRemote: true,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
isRecordReadOnly: true,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if isObjectRemote is false', () => {
|
||||
it('should return true if object is a workflow sub object', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
isObjectRemote: false,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if isRecordDeleted is true', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
isRecordDeleted: true,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
objectNameSingular: 'workflowRun',
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if isRecordDeleted is false', () => {
|
||||
it('should return true if object is a calendar event', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
isRecordDeleted: false,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if objectNameSingular is Workflow and fieldName is not name', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: 'workflow',
|
||||
fieldName: 'test',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if objectNameSingular is Workflow and fieldName is name', () => {
|
||||
it('should return true if object is a workflow and field is not name', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: 'Workflow',
|
||||
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',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if isWorkflowSubObjectMetadata is true', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: 'workflowVersion',
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
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);
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it('should return true if fieldType is RICH_TEXT_V2', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.RICH_TEXT_V2,
|
||||
});
|
||||
|
||||
it('should return true if fieldType is FieldMetadataType.ACTOR', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.ACTOR,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it('should return true if fieldType is ACTOR', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.ACTOR,
|
||||
});
|
||||
|
||||
it('should return true if fieldType is FieldMetadataType.RICH_TEXT', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
fieldType: FieldMetadataType.RICH_TEXT,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
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 if fieldType is not FieldMetadataType.ACTOR or FieldMetadataType.RICH_TEXT', () => {
|
||||
it('should return false for standard editable fields', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
objectNameSingular: 'company',
|
||||
fieldName: 'name',
|
||||
fieldType: FieldMetadataType.TEXT,
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if none of the conditions are met', () => {
|
||||
const result = isFieldValueReadOnly({
|
||||
contextStoreCurrentViewType: ContextStoreViewType.Table,
|
||||
isRecordReadOnly: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
|
||||
@ -1,51 +1,25 @@
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
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 { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type isFieldValueReadOnlyParams = {
|
||||
objectNameSingular?: string;
|
||||
fieldName?: string;
|
||||
fieldType?: FieldMetadataType;
|
||||
isObjectRemote?: boolean;
|
||||
isRecordDeleted?: boolean;
|
||||
hasObjectReadOnlyPermission?: boolean;
|
||||
contextStoreCurrentViewType: ContextStoreViewType | null;
|
||||
isRecordReadOnly?: boolean;
|
||||
};
|
||||
|
||||
export const isFieldValueReadOnly = ({
|
||||
objectNameSingular,
|
||||
fieldName,
|
||||
fieldType,
|
||||
isObjectRemote = false,
|
||||
isRecordDeleted = false,
|
||||
hasObjectReadOnlyPermission = false,
|
||||
contextStoreCurrentViewType,
|
||||
isRecordReadOnly = false,
|
||||
}: isFieldValueReadOnlyParams) => {
|
||||
const isTableViewOrKanbanView =
|
||||
contextStoreCurrentViewType === ContextStoreViewType.Table ||
|
||||
contextStoreCurrentViewType === ContextStoreViewType.Kanban;
|
||||
|
||||
const isTargetField =
|
||||
fieldName === 'noteTargets' || fieldName === 'taskTargets';
|
||||
|
||||
if (isTableViewOrKanbanView && isTargetField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObjectRemote) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isRecordDeleted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasObjectReadOnlyPermission) {
|
||||
if (isRecordReadOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -64,6 +38,20 @@ export const isFieldValueReadOnly = ({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
objectNameSingular !== CoreObjectNameSingular.Note &&
|
||||
fieldName === 'noteTargets'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
objectNameSingular !== CoreObjectNameSingular.Task &&
|
||||
fieldName === 'taskTargets'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(fieldType) &&
|
||||
(isFieldActor({ type: fieldType }) ||
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||
|
||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
|
||||
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
@ -46,14 +45,13 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
isDisplayModeFixHeight,
|
||||
onOpenEditMode,
|
||||
onCloseEditMode,
|
||||
isReadOnly,
|
||||
} = useContext(FieldContext);
|
||||
|
||||
const buttonIcon = useGetButtonIcon();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly();
|
||||
|
||||
const { closeInlineCell } = useInlineCell();
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
@ -134,7 +132,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
};
|
||||
|
||||
const RecordInlineCellContextValue: RecordInlineCellContextProps = {
|
||||
readonly: isFieldReadOnly,
|
||||
readonly: isReadOnly,
|
||||
buttonIcon: buttonIcon,
|
||||
IconLabel: fieldDefinition.iconName
|
||||
? getIcon(fieldDefinition.iconName)
|
||||
@ -152,7 +150,7 @@ export const RecordInlineCell = ({ loading }: RecordInlineCellProps) => {
|
||||
onTab={handleTab}
|
||||
onShiftTab={handleShiftTab}
|
||||
onClickOutside={handleClickOutside}
|
||||
isReadOnly={isFieldReadOnly}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
),
|
||||
displayModeContent: <FieldDisplay />,
|
||||
|
||||
@ -9,13 +9,13 @@ import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInput
|
||||
|
||||
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { useRecordInlineCellContext } from './RecordInlineCellContext';
|
||||
import {
|
||||
AppTooltip,
|
||||
OverflowingTextWithTooltip,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui/display';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { useRecordInlineCellContext } from './RecordInlineCellContext';
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -38,7 +38,6 @@ const StyledLabelAndIconContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 18px;
|
||||
padding-top: 3px;
|
||||
`;
|
||||
|
||||
const StyledValueContainer = styled.div`
|
||||
|
||||
@ -63,13 +63,8 @@ export const RecordInlineCellDisplayMode = ({
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const {
|
||||
editModeContentOnly,
|
||||
|
||||
showLabel,
|
||||
label,
|
||||
buttonIcon,
|
||||
} = useRecordInlineCellContext();
|
||||
const { editModeContentOnly, showLabel, label, buttonIcon } =
|
||||
useRecordInlineCellContext();
|
||||
|
||||
const isDisplayModeContentEmpty = useIsFieldEmpty();
|
||||
const showEditButton =
|
||||
|
||||
@ -6,6 +6,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/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';
|
||||
@ -84,6 +85,10 @@ export const FieldsCard = ({
|
||||
),
|
||||
);
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: objectRecordId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PropertyBox>
|
||||
@ -109,6 +114,7 @@ export const FieldsCard = ({
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<ActivityTargetsInlineCell
|
||||
@ -146,6 +152,7 @@ export const FieldsCard = ({
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
@ -182,6 +189,7 @@ export const FieldsCard = ({
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordDetailRelationSection
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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 { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
|
||||
@ -55,6 +56,10 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
objectRecordId,
|
||||
});
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: objectRecordId,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
@ -87,6 +92,7 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isCentered: false,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCell sizeVariant="xs" />
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
||||
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 { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
|
||||
@ -10,8 +11,8 @@ import { RecordTitleCell } from '@/object-record/record-title-cell/components/Re
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
type SummaryCardProps = {
|
||||
objectNameSingular: string;
|
||||
@ -54,6 +55,10 @@ export const SummaryCard = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: objectRecordId,
|
||||
});
|
||||
|
||||
return (
|
||||
<ShowPageSummaryCard
|
||||
isMobile={isMobile}
|
||||
@ -88,6 +93,7 @@ export const SummaryCard = ({
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isCentered: !isMobile,
|
||||
isDisplayModeFixHeight: true,
|
||||
isReadOnly: isRecordReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCell sizeVariant="md" />
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
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 { 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';
|
||||
@ -209,7 +210,14 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
[isExpanded],
|
||||
);
|
||||
|
||||
const isReadOnly = useIsFieldValueReadOnly();
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId: relationRecord.id,
|
||||
});
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly({
|
||||
fieldDefinition,
|
||||
isRecordReadOnly,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -226,7 +234,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
accent="tertiary"
|
||||
/>
|
||||
</StyledClickableZone>
|
||||
{!isReadOnly && (
|
||||
{!isFieldReadOnly && (
|
||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||
<Dropdown
|
||||
dropdownId={dropdownScopeId}
|
||||
@ -279,6 +287,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
}),
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordFieldComponentInstanceContext.Provider
|
||||
|
||||
@ -6,6 +6,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
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 { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||
import { useAddNewRecordAndOpenRightDrawer } from '@/object-record/record-field/meta-types/input/hooks/useAddNewRecordAndOpenRightDrawer';
|
||||
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||
@ -190,7 +191,14 @@ export const RecordDetailRelationSection = ({
|
||||
recordId,
|
||||
});
|
||||
|
||||
const isReadOnly = useIsFieldValueReadOnly();
|
||||
const isRecordReadOnly = useIsRecordReadOnly({
|
||||
recordId,
|
||||
});
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly({
|
||||
fieldDefinition,
|
||||
isRecordReadOnly,
|
||||
});
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
@ -250,7 +258,7 @@ export const RecordDetailRelationSection = ({
|
||||
hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile}
|
||||
areRecordsAvailable={relationRecords.length > 0}
|
||||
rightAdornment={
|
||||
!isReadOnly && (
|
||||
!isFieldReadOnly && (
|
||||
<DropdownScope dropdownScopeId={dropdownId}>
|
||||
<StyledAddDropdown
|
||||
dropdownId={dropdownId}
|
||||
|
||||
@ -46,6 +46,7 @@ const meta: Meta<typeof RecordDetailRelationSection> = {
|
||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||
}),
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState';
|
||||
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
export const RecordTableStickyBottomEffect = () => {
|
||||
|
||||
@ -102,7 +102,6 @@ export const RecordTableWithWrappers = ({
|
||||
>
|
||||
<EntityDeleteContext.Provider value={deleteOneRecord}>
|
||||
<ScrollWrapper
|
||||
contextProviderName="recordTableWithWrappers"
|
||||
componentInstanceId={`record-table-scroll-${recordTableId}`}
|
||||
>
|
||||
<RecordUpdateContext.Provider value={updateRecordMutation}>
|
||||
|
||||
@ -137,6 +137,7 @@ const meta: Meta = {
|
||||
...mockPerformance.fieldDefinition,
|
||||
},
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RelationFieldValueSetterEffect />
|
||||
|
||||
@ -8,6 +8,7 @@ export type RecordTableRowContextValue = {
|
||||
isSelected: boolean;
|
||||
inView: boolean;
|
||||
isPendingRow?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
|
||||
export const [RecordTableRowContextProvider, useRecordTableRowContextOrThrow] =
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
|
||||
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { GRAY_SCALE } from 'twenty-ui/theme';
|
||||
|
||||
@ -38,9 +37,7 @@ export const RecordTableBodyFetchMoreLoader = () => {
|
||||
[setRecordTableLastRowVisible, isRecordTableLoadMoreLocked],
|
||||
);
|
||||
|
||||
const scrollWrapperRef = useContext(
|
||||
RecordTableWithWrappersScrollWrapperContext,
|
||||
);
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||
|
||||
const hasRecordTableFetchedAllRecordsComponents = useRecoilComponentValueV2(
|
||||
hasRecordTableFetchedAllRecordsComponentStateV2,
|
||||
@ -53,9 +50,7 @@ export const RecordTableBodyFetchMoreLoader = () => {
|
||||
onChange: onLastRowVisible,
|
||||
delay: 1000,
|
||||
rootMargin: '1000px',
|
||||
root: scrollWrapperRef?.ref.current?.querySelector(
|
||||
'[data-overlayscrollbars-viewport]',
|
||||
),
|
||||
root: scrollWrapperHTMLElement,
|
||||
});
|
||||
|
||||
if (!showLoadingMoreRow) {
|
||||
|
||||
@ -11,11 +11,11 @@ import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record
|
||||
import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
|
||||
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
|
||||
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
|
||||
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
|
||||
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { isNonEmptyString, isNull } from '@sniptt/guards';
|
||||
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
|
||||
|
||||
export const RecordTableNoRecordGroupBodyEffect = () => {
|
||||
const { objectNameSingular } = useRecordTableContextOrThrow();
|
||||
|
||||
@ -8,9 +8,9 @@ import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/
|
||||
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
|
||||
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useScrollToPosition } from '@/ui/utilities/scroll/hooks/useScrollToPosition';
|
||||
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
|
||||
import { isNonEmptyString, isNull } from '@sniptt/guards';
|
||||
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
|
||||
|
||||
export const RecordTableRecordGroupBodyEffect = () => {
|
||||
const { objectNameSingular } = useRecordTableContextOrThrow();
|
||||
|
||||
@ -3,7 +3,6 @@ import { ReactNode, useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
@ -52,11 +51,11 @@ export const RecordTableCellBaseContainer = ({
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { isReadOnly } = useContext(FieldContext);
|
||||
const { setIsFocused } = useFieldFocus();
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const isReadOnly = useIsFieldValueReadOnly();
|
||||
const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext);
|
||||
|
||||
const { onMoveSoftFocusToCurrentCell, onCellMouseEnter } =
|
||||
@ -98,7 +97,7 @@ export const RecordTableCellBaseContainer = ({
|
||||
fontColorExtraLight={theme.font.color.extraLight}
|
||||
fontColorMedium={theme.border.color.medium}
|
||||
hasSoftFocus={hasSoftFocus}
|
||||
isReadOnly={isReadOnly}
|
||||
isReadOnly={isReadOnly ?? false}
|
||||
>
|
||||
{children}
|
||||
</StyledBaseContainer>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ReactNode, useContext } from 'react';
|
||||
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 { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
@ -8,16 +8,17 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
|
||||
import { MultipleRecordPickerHotkeyScope } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerHotkeyScope';
|
||||
import { SingleRecordPickerHotkeyScope } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerHotkeyScope';
|
||||
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { SelectFieldHotkeyScope } from '@/object-record/select/types/SelectFieldHotkeyScope';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ReactNode, useContext } from 'react';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { RelationDefinitionType } from '~/generated-metadata/graphql';
|
||||
import { isRecordTableScrolledLeftComponentState } from '../../states/isRecordTableScrolledLeftComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
export const RecordTableCellFieldContext = ({
|
||||
children,
|
||||
@ -27,7 +28,8 @@ export const RecordTableCellFieldContext = ({
|
||||
const { objectMetadataItem } = useRecordTableContextOrThrow();
|
||||
const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
|
||||
const { columnDefinition } = useContext(RecordTableCellContext);
|
||||
const { recordId } = useRecordTableRowContextOrThrow();
|
||||
const { recordId, isReadOnly: isTableRowReadOnly } =
|
||||
useRecordTableRowContextOrThrow();
|
||||
const updateRecord = useContext(RecordUpdateContext);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@ -70,6 +72,11 @@ export const RecordTableCellFieldContext = ({
|
||||
|
||||
const customHotkeyScope = computedHotkeyScope(columnDefinition);
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly({
|
||||
fieldDefinition: columnDefinition,
|
||||
isRecordReadOnly: isTableRowReadOnly ?? false,
|
||||
});
|
||||
|
||||
return (
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
@ -87,6 +94,7 @@ export const RecordTableCellFieldContext = ({
|
||||
}),
|
||||
displayedMaxRows: 1,
|
||||
isLabelHidden,
|
||||
isReadOnly: isFieldReadOnly,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { ReactNode, useContext } from 'react';
|
||||
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/states/contexts/RecordFieldComponentInstanceContext';
|
||||
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
|
||||
import { useRecordTableRowContextOrThrow } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableCellFieldContext } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContext';
|
||||
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
|
||||
import { ReactNode, useContext } from 'react';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { RecordTableCellFieldContext } from './RecordTableCellFieldContext';
|
||||
|
||||
export const RecordTableCellFieldContextWrapper = ({
|
||||
children,
|
||||
@ -12,6 +12,7 @@ export const RecordTableCellFieldContextWrapper = ({
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { columnDefinition } = useContext(RecordTableCellContext);
|
||||
|
||||
const { recordId } = useRecordTableRowContextOrThrow();
|
||||
|
||||
if (isUndefinedOrNull(columnDefinition)) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import {
|
||||
FieldInputClickOutsideEvent,
|
||||
FieldInputEvent,
|
||||
@ -7,12 +7,13 @@ import {
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
export const RecordTableCellFieldInput = () => {
|
||||
const { onMoveFocus, onCloseTableCell } = useRecordTableBodyContextOrThrow();
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly();
|
||||
const { isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const handleEnter: FieldInputEvent = (persistField) => {
|
||||
persistField();
|
||||
@ -77,7 +78,7 @@ export const RecordTableCellFieldInput = () => {
|
||||
onShiftTab={handleShiftTab}
|
||||
onSubmit={handleSubmit}
|
||||
onTab={handleTab}
|
||||
isReadOnly={isFieldReadOnly}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,12 +10,11 @@ import { useOpenRecordTableCellFromCell } from '@/object-record/record-table/rec
|
||||
import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/isSoftFocusUsingMouseState';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
|
||||
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconArrowUpRight } from 'twenty-ui/display';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
|
||||
|
||||
type RecordTableCellSoftFocusModeProps = {
|
||||
editModeContent: ReactElement;
|
||||
@ -27,12 +26,10 @@ export const RecordTableCellSoftFocusMode = ({
|
||||
nonEditModeContent,
|
||||
}: RecordTableCellSoftFocusModeProps) => {
|
||||
const { columnIndex, columnDefinition } = useContext(RecordTableCellContext);
|
||||
const { recordId } = useContext(FieldContext);
|
||||
const { recordId, isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly();
|
||||
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
|
||||
const editModeContentOnly = useIsFieldInputOnly();
|
||||
@ -52,7 +49,7 @@ export const RecordTableCellSoftFocusMode = ({
|
||||
}, [isSoftFocusUsingMouse]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isFieldInputOnly && !isFieldReadOnly) {
|
||||
if (!isFieldInputOnly && !isReadOnly) {
|
||||
openTableCell();
|
||||
}
|
||||
};
|
||||
@ -85,13 +82,13 @@ export const RecordTableCellSoftFocusMode = ({
|
||||
const showButton =
|
||||
isDefined(buttonIcon) &&
|
||||
!editModeContentOnly &&
|
||||
!isFieldReadOnly &&
|
||||
!isReadOnly &&
|
||||
!(isMobile && isFirstColumn);
|
||||
|
||||
const dontShowContent = isEmpty && isFieldReadOnly;
|
||||
const dontShowContent = isEmpty && isReadOnly;
|
||||
|
||||
const showPlaceholder =
|
||||
!editModeContentOnly && !isFieldReadOnly && isFirstColumn && isEmpty;
|
||||
!editModeContentOnly && !isReadOnly && isFirstColumn && isEmpty;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
@ -11,14 +11,12 @@ import { isSoftFocusUsingMouseState } from '@/object-record/record-table/states/
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
|
||||
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
|
||||
export const RecordTableCellSoftFocusModeHotkeysSetterEffect = () => {
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly();
|
||||
|
||||
const { openTableCell } = useOpenRecordTableCellFromCell();
|
||||
const { isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
@ -50,7 +48,7 @@ export const RecordTableCellSoftFocusModeHotkeysSetterEffect = () => {
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
if (isFieldReadOnly) {
|
||||
if (isReadOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -67,7 +65,7 @@ export const RecordTableCellSoftFocusModeHotkeysSetterEffect = () => {
|
||||
useScopedHotkeys(
|
||||
'*',
|
||||
(keyboardEvent) => {
|
||||
if (isFieldReadOnly) {
|
||||
if (isReadOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -54,6 +54,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
recordId: 'recordId',
|
||||
hotkeyScope: TableHotkeyScope.Table,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordTableRowContextProvider value={recordTableRowContextValue}>
|
||||
|
||||
@ -54,6 +54,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
recordId: 'recordId',
|
||||
hotkeyScope: TableHotkeyScope.Table,
|
||||
isLabelIdentifier: false,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordTableRowContextProvider value={recordTableRowContextValue}>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
|
||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
|
||||
@ -29,7 +28,7 @@ export type OpenTableCellArgs = {
|
||||
|
||||
export const useOpenRecordTableCellFromCell = () => {
|
||||
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
const { recordId, fieldDefinition, isReadOnly } = useContext(FieldContext);
|
||||
|
||||
const { pathToShowPage, objectNameSingular } =
|
||||
useRecordTableRowContextOrThrow();
|
||||
@ -38,8 +37,6 @@ export const useOpenRecordTableCellFromCell = () => {
|
||||
|
||||
const cellPosition = useCurrentTableCellPosition();
|
||||
|
||||
const isFieldReadOnly = useIsFieldValueReadOnly();
|
||||
|
||||
const openTableCell = (
|
||||
initialValue?: string,
|
||||
isActionButtonClick = false,
|
||||
@ -50,7 +47,7 @@ export const useOpenRecordTableCellFromCell = () => {
|
||||
customCellHotkeyScope,
|
||||
recordId,
|
||||
fieldDefinition,
|
||||
isReadOnly: isFieldReadOnly,
|
||||
isReadOnly,
|
||||
pathToShowPage,
|
||||
objectNameSingular,
|
||||
initialValue,
|
||||
|
||||
@ -4,7 +4,7 @@ import { RecordTableAggregateFooterCell } from '@/object-record/record-table/rec
|
||||
import { RecordTableColumnAggregateFooterCellContext } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterCellContext';
|
||||
import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
@ -86,14 +86,11 @@ export const RecordTableAggregateFooter = ({
|
||||
visibleTableColumnsComponentSelector,
|
||||
);
|
||||
|
||||
const overlayScrollbarsInstance = useRecoilComponentValueV2(
|
||||
scrollWrapperInstanceComponentState,
|
||||
);
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||
|
||||
const hasHorizontalOverflow = overlayScrollbarsInstance
|
||||
? overlayScrollbarsInstance.elements().scrollOffsetElement.scrollWidth >
|
||||
overlayScrollbarsInstance.elements().scrollOffsetElement.clientWidth
|
||||
: false;
|
||||
const hasHorizontalOverflow =
|
||||
(scrollWrapperHTMLElement?.scrollWidth ?? 0) >
|
||||
(scrollWrapperHTMLElement?.clientWidth ?? 0);
|
||||
|
||||
return (
|
||||
<StyledTableRow
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import { Theme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { HIDDEN_TABLE_COLUMN_DROPDOWN_ID } from '@/object-record/record-table/constants/HiddenTableColumnDropdownId';
|
||||
import { RecordTableHeaderPlusButtonContent } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme';
|
||||
|
||||
const StyledPlusIconHeaderCell = styled.th<{
|
||||
theme: Theme;
|
||||
isTableWiderThanScreen: boolean;
|
||||
}>`
|
||||
${({ theme }) => {
|
||||
@ -54,20 +51,17 @@ const HIDDEN_TABLE_COLUMN_DROPDOWN_HOTKEY_SCOPE_ID =
|
||||
'hidden-table-columns-dropdown-hotkey-scope-id';
|
||||
|
||||
export const RecordTableHeaderLastColumn = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const theme = useTheme();
|
||||
|
||||
const scrollWrapper = useScrollWrapperScopedRef('recordTableWithWrappers');
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||
|
||||
const isTableWiderThanScreen =
|
||||
(scrollWrapper.ref.current?.clientWidth ?? 0) <
|
||||
(scrollWrapper.ref.current?.scrollWidth ?? 0);
|
||||
(scrollWrapperHTMLElement?.clientWidth ?? 0) <
|
||||
(scrollWrapperHTMLElement?.scrollWidth ?? 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledPlusIconHeaderCell
|
||||
theme={theme}
|
||||
isTableWiderThanScreen={isTableWiderThanScreen}
|
||||
>
|
||||
<StyledPlusIconHeaderCell isTableWiderThanScreen={isTableWiderThanScreen}>
|
||||
<Dropdown
|
||||
dropdownId={HIDDEN_TABLE_COLUMN_DROPDOWN_ID}
|
||||
clickableComponent={
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReactNode, useContext, useEffect } from 'react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
|
||||
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
|
||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
||||
@ -6,7 +6,7 @@ import { useRecordTableContextOrThrow } from '@/object-record/record-table/conte
|
||||
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { RecordTableDraggableTr } from '@/object-record/record-table/record-table-row/components/RecordTableDraggableTr';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
@ -34,14 +34,10 @@ export const RecordTableRowWrapper = ({
|
||||
|
||||
const { onIndexRecordsLoaded } = useRecordIndexContextOrThrow();
|
||||
|
||||
const scrollWrapperRef = useContext(
|
||||
RecordTableWithWrappersScrollWrapperContext,
|
||||
);
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||
|
||||
const { ref: elementRef, inView } = useInView({
|
||||
root: scrollWrapperRef.ref.current?.querySelector(
|
||||
'[data-overlayscrollbars-viewport]',
|
||||
),
|
||||
root: scrollWrapperHTMLElement,
|
||||
rootMargin: '1000px',
|
||||
});
|
||||
|
||||
|
||||
@ -41,10 +41,7 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
|
||||
return (
|
||||
<ScrollWrapper
|
||||
contextProviderName="navigationDrawer"
|
||||
componentInstanceId={`scroll-wrapper-settings-navigation-drawer`}
|
||||
scrollbarVariant="no-padding"
|
||||
heightMode="fit-content"
|
||||
defaultEnableXScroll={false}
|
||||
>
|
||||
<StyledInnerContainer>
|
||||
|
||||
@ -30,11 +30,7 @@ export const SettingsPageContainer = ({
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) => (
|
||||
<ScrollWrapper
|
||||
contextProviderName="settingsPageContainer"
|
||||
heightMode="full"
|
||||
componentInstanceId={'scroll-wrapper-settings-page-container'}
|
||||
>
|
||||
<ScrollWrapper componentInstanceId={'scroll-wrapper-settings-page-container'}>
|
||||
<StyledSettingsPageContainer>{children}</StyledSettingsPageContainer>
|
||||
</ScrollWrapper>
|
||||
);
|
||||
|
||||
@ -146,6 +146,7 @@ export const SettingsDataModelFieldPreview = ({
|
||||
defaultValue: fieldMetadataItem.defaultValue,
|
||||
},
|
||||
hotkeyScope: 'field-preview',
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{fieldMetadataItem.type === FieldMetadataType.BOOLEAN ? (
|
||||
|
||||
@ -4,11 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import {
|
||||
ImportedRow,
|
||||
ImportedStructuredRow,
|
||||
SpreadsheetImportField,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { ImportedRow, ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
|
||||
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
|
||||
@ -21,13 +17,14 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
|
||||
import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
|
||||
import { UnmatchColumn } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/UnmatchColumn';
|
||||
import { initialComputedColumnsSelector } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
|
||||
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
|
||||
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
|
||||
import { SpreadsheetColumn } from '@/spreadsheet-import/types/SpreadsheetColumn';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
|
||||
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
|
||||
import { SpreadsheetImportField } from '@/spreadsheet-import/types/SpreadsheetImportField';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@ -273,11 +270,7 @@ export const MatchColumnsStep = <T extends string>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollWrapper
|
||||
contextProviderName="modalContent"
|
||||
componentInstanceId="scroll-wrapper-modal-content"
|
||||
heightMode="full"
|
||||
>
|
||||
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
|
||||
<StyledContent>
|
||||
<Heading
|
||||
title={t`Match Columns`}
|
||||
|
||||
@ -55,7 +55,6 @@ export const DropdownMenuItemsContainer = ({
|
||||
>
|
||||
{hasMaxHeight ? (
|
||||
<StyledScrollWrapper
|
||||
contextProviderName="dropdownMenuItemsContainer"
|
||||
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
|
||||
>
|
||||
<StyledDropdownMenuItemsInternalContainer>
|
||||
@ -69,10 +68,7 @@ export const DropdownMenuItemsContainer = ({
|
||||
)}
|
||||
</StyledDropdownMenuItemsExternalContainer>
|
||||
) : (
|
||||
<ScrollWrapper
|
||||
contextProviderName="dropdownMenuItemsContainer"
|
||||
componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}
|
||||
>
|
||||
<ScrollWrapper componentInstanceId={`scroll-wrapper-dropdown-menu-${id}`}>
|
||||
<StyledDropdownMenuItemsExternalContainer
|
||||
hasMaxHeight={hasMaxHeight}
|
||||
className={className}
|
||||
|
||||
@ -30,17 +30,7 @@ const StyledLayout = styled.div`
|
||||
scrollbar-width: 4px;
|
||||
width: 100%;
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
@ -32,7 +32,6 @@ export const ShowPageContainer = ({ children }: ShowPageContainerProps) => {
|
||||
return isMobile ? (
|
||||
<StyledOuterContainer>
|
||||
<StyledScrollWrapper
|
||||
contextProviderName="showPageContainer"
|
||||
componentInstanceId={'scroll-wrapper-show-page-container'}
|
||||
>
|
||||
<StyledInnerContainer>{children}</StyledInnerContainer>
|
||||
|
||||
@ -70,7 +70,6 @@ export const ShowPageActivityContainer = ({
|
||||
|
||||
return (
|
||||
<ScrollWrapper
|
||||
contextProviderName="showPageActivityContainer"
|
||||
componentInstanceId={`scroll-wrapper-tab-list-${targetableObject.id}`}
|
||||
>
|
||||
<StyledShowPageActivityContainer>
|
||||
|
||||
@ -47,7 +47,6 @@ export const ShowPageLeftContainer = ({
|
||||
</StyledInnerContainer>
|
||||
) : (
|
||||
<ScrollWrapper
|
||||
contextProviderName="showPageLeftContainer"
|
||||
componentInstanceId={`scroll-wrapper-show-page-left-container`}
|
||||
>
|
||||
<StyledIntermediateContainer>
|
||||
|
||||
@ -79,7 +79,6 @@ export const TabList = ({
|
||||
/>
|
||||
<ScrollWrapper
|
||||
defaultEnableYScroll={false}
|
||||
contextProviderName="tabList"
|
||||
componentInstanceId={`scroll-wrapper-tab-list-${componentInstanceId}`}
|
||||
>
|
||||
<StyledContainer className={className}>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
import { INITIAL_HOTKEYS_SCOPE } from '../../constants/InitialHotkeysScope';
|
||||
import { HotkeyScope } from '../../types/HotkeyScope';
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const currentHotkeyScopeState = createState<HotkeyScope>({
|
||||
key: 'currentHotkeyScopeState',
|
||||
|
||||
@ -1,102 +1,44 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import {
|
||||
ContextProviderName,
|
||||
getContextByProviderName,
|
||||
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
|
||||
import { ScrollWrapperInitEffect } from '@/ui/utilities/scroll/components/internal/ScrollWrapperInitEffect';
|
||||
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
|
||||
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrappeScrollBottomComponentState';
|
||||
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
|
||||
import { scrollWrapperScrollBottomComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollBottomComponentState';
|
||||
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
|
||||
import { scrollWrapperScrollTopComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollTopComponentState';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { css } from '@emotion/react';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
type HeightMode = 'full' | 'fit-content';
|
||||
|
||||
const StyledScrollWrapper = styled.div<{
|
||||
heightMode: HeightMode;
|
||||
scrollbarVariant: 'with-padding' | 'no-padding';
|
||||
}>`
|
||||
const StyledScrollWrapper = styled.div`
|
||||
&.scroll-wrapper-x-enabled {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
&.scroll-wrapper-y-enabled {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
display: flex;
|
||||
height: ${({ heightMode }) => {
|
||||
switch (heightMode) {
|
||||
case 'full':
|
||||
return '100%';
|
||||
case 'fit-content':
|
||||
return 'fit-content';
|
||||
}
|
||||
}};
|
||||
width: 100%;
|
||||
|
||||
.os-scrollbar-handle {
|
||||
background-color: ${({ theme }) => theme.border.color.strong};
|
||||
}
|
||||
|
||||
// Keep horizontal scrollbar always visible
|
||||
.os-scrollbar-horizontal {
|
||||
&.os-scrollbar-auto-hide {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
.os-scrollbar-track {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.os-scrollbar {
|
||||
transition:
|
||||
opacity 300ms,
|
||||
visibility 300ms,
|
||||
top 300ms,
|
||||
right 300ms,
|
||||
bottom 300ms,
|
||||
left 300ms;
|
||||
}
|
||||
|
||||
${({ scrollbarVariant }) =>
|
||||
scrollbarVariant === 'no-padding' &&
|
||||
css`
|
||||
.os-scrollbar {
|
||||
--os-size: 6px;
|
||||
padding: 0px;
|
||||
}
|
||||
`}
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledInnerContainer = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export type ScrollWrapperProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
heightMode?: HeightMode;
|
||||
defaultEnableXScroll?: boolean;
|
||||
defaultEnableYScroll?: boolean;
|
||||
contextProviderName: ContextProviderName;
|
||||
componentInstanceId: string;
|
||||
scrollbarVariant?: 'with-padding' | 'no-padding';
|
||||
};
|
||||
|
||||
export const ScrollWrapper = ({
|
||||
componentInstanceId,
|
||||
children,
|
||||
className,
|
||||
heightMode = 'full',
|
||||
defaultEnableXScroll = true,
|
||||
defaultEnableYScroll = true,
|
||||
contextProviderName,
|
||||
scrollbarVariant = 'with-padding',
|
||||
}: ScrollWrapperProps) => {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const Context = getContextByProviderName(contextProviderName);
|
||||
|
||||
const setScrollTop = useSetRecoilComponentStateV2(
|
||||
scrollWrapperScrollTopComponentState,
|
||||
componentInstanceId,
|
||||
@ -112,8 +54,8 @@ export const ScrollWrapper = ({
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const handleScroll = (overlayScroll: OverlayScrollbars) => {
|
||||
const target = overlayScroll.elements().scrollOffsetElement;
|
||||
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
|
||||
const target = event.currentTarget;
|
||||
setScrollTop(target.scrollTop);
|
||||
setScrollLeft(target.scrollLeft);
|
||||
setScrollBottom(
|
||||
@ -121,103 +63,21 @@ export const ScrollWrapper = ({
|
||||
);
|
||||
};
|
||||
|
||||
const setOverlayScrollbars = useSetRecoilComponentStateV2(
|
||||
scrollWrapperInstanceComponentState,
|
||||
componentInstanceId,
|
||||
);
|
||||
|
||||
const [initialize, instance] = useOverlayScrollbars({
|
||||
options: {
|
||||
scrollbars: {
|
||||
autoHide: 'scroll',
|
||||
autoHideDelay: 500,
|
||||
},
|
||||
overflow: {
|
||||
x: defaultEnableXScroll ? undefined : 'hidden',
|
||||
y: defaultEnableYScroll ? undefined : 'hidden',
|
||||
},
|
||||
},
|
||||
events: {
|
||||
updated: (osInstance) => {
|
||||
const {
|
||||
scrollOffsetElement: target,
|
||||
scrollbarVertical,
|
||||
scrollbarHorizontal,
|
||||
} = osInstance.elements();
|
||||
|
||||
if (scrollbarVertical !== null) {
|
||||
scrollbarVertical.track.dataset.selectDisable = 'true';
|
||||
}
|
||||
if (scrollbarHorizontal !== null) {
|
||||
scrollbarHorizontal.track.dataset.selectDisable = 'true';
|
||||
}
|
||||
setScrollBottom(
|
||||
target.scrollHeight - target.clientHeight - target.scrollTop,
|
||||
);
|
||||
},
|
||||
scroll: (osInstance) => {
|
||||
const { scrollOffsetElement: target, scrollbarVertical } =
|
||||
osInstance.elements();
|
||||
// Hide vertical scrollbar by default
|
||||
if (scrollbarVertical !== null) {
|
||||
scrollbarVertical.track.style.visibility = 'hidden';
|
||||
}
|
||||
|
||||
// Show vertical scrollbar based on scroll direction
|
||||
const isVerticalScroll =
|
||||
target.scrollTop !== Number(target.dataset.lastScrollTop || '0');
|
||||
|
||||
if (
|
||||
isVerticalScroll === true &&
|
||||
scrollbarVertical !== null &&
|
||||
target.scrollHeight > target.clientHeight
|
||||
) {
|
||||
scrollbarVertical.track.style.visibility = 'visible';
|
||||
}
|
||||
// Update vertical scroll positions
|
||||
target.dataset.lastScrollTop = target.scrollTop.toString();
|
||||
|
||||
handleScroll(osInstance);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = scrollableRef.current;
|
||||
if (currentRef !== null) {
|
||||
initialize(currentRef);
|
||||
}
|
||||
return () => {
|
||||
// Reset vertical scroll component-specific Recoil state
|
||||
setScrollTop(0);
|
||||
setOverlayScrollbars(null);
|
||||
instance()?.destroy();
|
||||
};
|
||||
}, [initialize, instance, setScrollTop, setOverlayScrollbars]);
|
||||
|
||||
useEffect(() => {
|
||||
setOverlayScrollbars(instance());
|
||||
}, [instance, setOverlayScrollbars]);
|
||||
|
||||
return (
|
||||
<ScrollWrapperComponentInstanceContext.Provider
|
||||
value={{ instanceId: componentInstanceId }}
|
||||
>
|
||||
<Context.Provider
|
||||
value={{
|
||||
ref: scrollableRef,
|
||||
id: contextProviderName,
|
||||
}}
|
||||
<ScrollWrapperInitEffect
|
||||
defaultEnableXScroll={defaultEnableXScroll}
|
||||
defaultEnableYScroll={defaultEnableYScroll}
|
||||
/>
|
||||
<StyledScrollWrapper
|
||||
id={`scroll-wrapper-${componentInstanceId}`}
|
||||
className={className}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<StyledScrollWrapper
|
||||
ref={scrollableRef}
|
||||
className={className}
|
||||
heightMode={heightMode}
|
||||
scrollbarVariant={scrollbarVariant}
|
||||
>
|
||||
<StyledInnerContainer>{children}</StyledInnerContainer>
|
||||
</StyledScrollWrapper>
|
||||
</Context.Provider>
|
||||
<StyledInnerContainer>{children}</StyledInnerContainer>
|
||||
</StyledScrollWrapper>
|
||||
</ScrollWrapperComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { useToggleScrollWrapper } from '@/ui/utilities/scroll/hooks/useToggleScrollWrapper';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export type ScrollWrapperInitEffectProps = {
|
||||
defaultEnableXScroll?: boolean;
|
||||
defaultEnableYScroll?: boolean;
|
||||
};
|
||||
|
||||
export const ScrollWrapperInitEffect = ({
|
||||
defaultEnableXScroll = true,
|
||||
defaultEnableYScroll = true,
|
||||
}: ScrollWrapperInitEffectProps) => {
|
||||
const { toggleScrollXWrapper, toggleScrollYWrapper } =
|
||||
useToggleScrollWrapper();
|
||||
|
||||
useEffect(() => {
|
||||
toggleScrollXWrapper(defaultEnableXScroll);
|
||||
toggleScrollYWrapper(defaultEnableYScroll);
|
||||
}, [
|
||||
defaultEnableXScroll,
|
||||
defaultEnableYScroll,
|
||||
toggleScrollXWrapper,
|
||||
toggleScrollYWrapper,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,98 +0,0 @@
|
||||
import { createContext, RefObject } from 'react';
|
||||
|
||||
type ScrollWrapperContextValue = {
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ContextProviderName =
|
||||
| 'eventList'
|
||||
| 'commandMenu'
|
||||
| 'recordBoard'
|
||||
| 'recordTableWithWrappers'
|
||||
| 'settingsPageContainer'
|
||||
| 'dropdownMenuItemsContainer'
|
||||
| 'showPageContainer'
|
||||
| 'showPageLeftContainer'
|
||||
| 'tabList'
|
||||
| 'releases'
|
||||
| 'test'
|
||||
| 'showPageActivityContainer'
|
||||
| 'navigationDrawer'
|
||||
| 'aggregateFooterCell'
|
||||
| 'modalContent';
|
||||
|
||||
const createScrollWrapperContext = (id: string) =>
|
||||
createContext<ScrollWrapperContextValue>({
|
||||
ref: { current: null },
|
||||
id,
|
||||
});
|
||||
|
||||
export const EventListScrollWrapperContext =
|
||||
createScrollWrapperContext('eventList');
|
||||
export const CommandMenuScrollWrapperContext =
|
||||
createScrollWrapperContext('commandMenu');
|
||||
export const RecordBoardScrollWrapperContext =
|
||||
createScrollWrapperContext('recordBoard');
|
||||
export const RecordTableWithWrappersScrollWrapperContext =
|
||||
createScrollWrapperContext('recordTableWithWrappers');
|
||||
export const SettingsPageContainerScrollWrapperContext =
|
||||
createScrollWrapperContext('settingsPageContainer');
|
||||
export const DropdownMenuItemsContainerScrollWrapperContext =
|
||||
createScrollWrapperContext('dropdownMenuItemsContainer');
|
||||
export const ShowPageContainerScrollWrapperContext =
|
||||
createScrollWrapperContext('showPageContainer');
|
||||
export const ShowPageLeftContainerScrollWrapperContext =
|
||||
createScrollWrapperContext('showPageLeftContainer');
|
||||
export const TabListScrollWrapperContext =
|
||||
createScrollWrapperContext('tabList');
|
||||
export const ReleasesScrollWrapperContext =
|
||||
createScrollWrapperContext('releases');
|
||||
export const ShowPageActivityContainerScrollWrapperContext =
|
||||
createScrollWrapperContext('showPageActivityContainer');
|
||||
export const NavigationDrawerScrollWrapperContext =
|
||||
createScrollWrapperContext('navigationDrawer');
|
||||
export const TestScrollWrapperContext = createScrollWrapperContext('test');
|
||||
export const AggregateFooterCellScrollWrapperContext =
|
||||
createScrollWrapperContext('aggregateFooterCell');
|
||||
export const ModalContentScrollWrapperContext =
|
||||
createScrollWrapperContext('modalContent');
|
||||
|
||||
export const getContextByProviderName = (
|
||||
contextProviderName: ContextProviderName,
|
||||
) => {
|
||||
switch (contextProviderName) {
|
||||
case 'eventList':
|
||||
return EventListScrollWrapperContext;
|
||||
case 'commandMenu':
|
||||
return CommandMenuScrollWrapperContext;
|
||||
case 'recordBoard':
|
||||
return RecordBoardScrollWrapperContext;
|
||||
case 'recordTableWithWrappers':
|
||||
return RecordTableWithWrappersScrollWrapperContext;
|
||||
case 'settingsPageContainer':
|
||||
return SettingsPageContainerScrollWrapperContext;
|
||||
case 'dropdownMenuItemsContainer':
|
||||
return DropdownMenuItemsContainerScrollWrapperContext;
|
||||
case 'showPageContainer':
|
||||
return ShowPageContainerScrollWrapperContext;
|
||||
case 'showPageLeftContainer':
|
||||
return ShowPageLeftContainerScrollWrapperContext;
|
||||
case 'tabList':
|
||||
return TabListScrollWrapperContext;
|
||||
case 'releases':
|
||||
return ReleasesScrollWrapperContext;
|
||||
case 'test':
|
||||
return TestScrollWrapperContext;
|
||||
case 'showPageActivityContainer':
|
||||
return ShowPageActivityContainerScrollWrapperContext;
|
||||
case 'navigationDrawer':
|
||||
return NavigationDrawerScrollWrapperContext;
|
||||
case 'aggregateFooterCell':
|
||||
return AggregateFooterCellScrollWrapperContext;
|
||||
case 'modalContent':
|
||||
return ModalContentScrollWrapperContext;
|
||||
default:
|
||||
throw new Error('Context Provider not available');
|
||||
}
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef';
|
||||
|
||||
jest.mock('react', () => {
|
||||
const originalModule = jest.requireActual('react');
|
||||
return {
|
||||
...originalModule,
|
||||
useContext: () => ({ current: {} }),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useScrollWrapperScopedRef', () => {
|
||||
it('should return the scrollWrapperRef if available', () => {
|
||||
const { result } = renderHook(() => useScrollWrapperScopedRef('test'));
|
||||
|
||||
expect(result.current).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperElement';
|
||||
|
||||
export const useScrollToPosition = () => {
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperElement();
|
||||
|
||||
const scrollToPosition = (scrollPositionInPx: number) => {
|
||||
scrollWrapperHTMLElement?.scrollTo({ top: scrollPositionInPx });
|
||||
};
|
||||
|
||||
return { scrollToPosition };
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
|
||||
export const useScrollWrapperElement = (targetComponentInstanceId?: string) => {
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
ScrollWrapperComponentInstanceContext,
|
||||
targetComponentInstanceId,
|
||||
);
|
||||
|
||||
const scrollWrapperHTMLElement = document.getElementById(
|
||||
`scroll-wrapper-${instanceId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
scrollWrapperHTMLElement,
|
||||
};
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import {
|
||||
ContextProviderName,
|
||||
getContextByProviderName,
|
||||
} from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
|
||||
export const useScrollWrapperScopedRef = (
|
||||
contextProviderName: ContextProviderName,
|
||||
) => {
|
||||
const Context = getContextByProviderName(contextProviderName);
|
||||
const scrollWrapperRef = useContext(Context);
|
||||
|
||||
if (isUndefinedOrNull(scrollWrapperRef))
|
||||
throw new Error(
|
||||
`Using a scroll ref without a ScrollWrapper : verify that you are using a ScrollWrapper if you intended to do so.`,
|
||||
);
|
||||
|
||||
return scrollWrapperRef;
|
||||
};
|
||||
@ -1,34 +1,38 @@
|
||||
import { scrollWrapperInstanceComponentState } from '@/ui/utilities/scroll/states/scrollWrapperInstanceComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
|
||||
export const useToggleScrollWrapper = () => {
|
||||
const instanceOverlay = useRecoilComponentValueV2(
|
||||
scrollWrapperInstanceComponentState,
|
||||
export const useToggleScrollWrapper = (targetComponentInstanceId?: string) => {
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
ScrollWrapperComponentInstanceContext,
|
||||
targetComponentInstanceId,
|
||||
);
|
||||
|
||||
const toggleScrollXWrapper = (isEnabled: boolean) => {
|
||||
if (!instanceOverlay) {
|
||||
return;
|
||||
if (isEnabled) {
|
||||
document
|
||||
.getElementById(`scroll-wrapper-${instanceId}`)
|
||||
?.classList.add('scroll-wrapper-x-enabled');
|
||||
} else {
|
||||
document
|
||||
.getElementById(`scroll-wrapper-${instanceId}`)
|
||||
?.classList.remove('scroll-wrapper-x-enabled');
|
||||
}
|
||||
|
||||
instanceOverlay.options({
|
||||
overflow: {
|
||||
x: isEnabled ? 'scroll' : 'hidden',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleScrollYWrapper = (isEnabled: boolean) => {
|
||||
if (!instanceOverlay) {
|
||||
return;
|
||||
if (isEnabled) {
|
||||
document
|
||||
.getElementById(`scroll-wrapper-${instanceId}`)
|
||||
?.classList.add('scroll-wrapper-y-enabled');
|
||||
} else {
|
||||
document
|
||||
.getElementById(`scroll-wrapper-${instanceId}`)
|
||||
?.classList.remove('scroll-wrapper-y-enabled');
|
||||
}
|
||||
|
||||
instanceOverlay.options({
|
||||
overflow: {
|
||||
y: isEnabled ? 'scroll' : 'hidden',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { toggleScrollXWrapper, toggleScrollYWrapper };
|
||||
return {
|
||||
toggleScrollXWrapper,
|
||||
toggleScrollYWrapper,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { ScrollWrapperComponentInstanceContext } from '@/ui/utilities/scroll/states/contexts/ScrollWrapperComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
|
||||
export const scrollWrapperInstanceComponentState =
|
||||
createComponentStateV2<OverlayScrollbars | null>({
|
||||
key: 'scrollWrapperInstanceComponentState',
|
||||
defaultValue: null,
|
||||
componentInstanceContext: ScrollWrapperComponentInstanceContext,
|
||||
});
|
||||
@ -118,10 +118,7 @@ export const Releases = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<ScrollWrapper
|
||||
contextProviderName="releases"
|
||||
componentInstanceId="scroll-wrapper-releases"
|
||||
>
|
||||
<ScrollWrapper componentInstanceId="scroll-wrapper-releases">
|
||||
<StyledReleaseContainer>
|
||||
{releases.map((release) => (
|
||||
<React.Fragment key={release.slug}>
|
||||
|
||||
@ -12,11 +12,11 @@ import {
|
||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getCompaniesMock } from '~/testing/mock-data/companies';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { getPeopleRecordConnectionMock } from '~/testing/mock-data/people';
|
||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const RecordMockSetterEffect = ({
|
||||
companies,
|
||||
@ -141,6 +141,7 @@ export const getFieldDecorator =
|
||||
objectMetadataItem,
|
||||
}),
|
||||
hotkeyScope: 'hotkey-scope',
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
<RecordMockSetterEffect
|
||||
|
||||
@ -56,6 +56,7 @@ export const useMockFieldContext = ({
|
||||
hotkeyScope:
|
||||
customHotkeyScope ?? InlineCellHotkeyScope.InlineCell,
|
||||
clearable,
|
||||
isReadOnly: false,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user