Relations many in table view (#5842)
Closes #5924. Adding the "many" side of relations in the table view, and fixing some issues (glitch in Multi record select, cache update after update). --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,9 +1,10 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray, isNull } from '@sniptt/guards';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
@ -15,10 +16,17 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
|
||||
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
||||
import {
|
||||
ObjectRecordAndSelected,
|
||||
objectRecordMultiSelectComponentFamilyState,
|
||||
} from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect';
|
||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
|
||||
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
|
||||
const StyledSelectContainer = styled.div`
|
||||
@ -37,6 +45,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
activityTargetWithTargetRecords,
|
||||
}: ActivityTargetInlineCellEditModeProps) => {
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
const relationPickerScopeId = `relation-picker-${activity.id}`;
|
||||
|
||||
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
|
||||
(activityTarget) => ({
|
||||
@ -74,109 +83,181 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
|
||||
closeEditableField();
|
||||
const handleSubmit = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
const activityTargetsAfterUpdate =
|
||||
activityTargetWithTargetRecords.filter((activityTarget) => {
|
||||
const record = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: relationPickerScopeId,
|
||||
familyKey: activityTarget.targetObject.id,
|
||||
}),
|
||||
)
|
||||
.getValue() as ObjectRecordAndSelected;
|
||||
|
||||
const activityTargetsToDelete = activityTargetWithTargetRecords.filter(
|
||||
(activityTargetObjectRecord) =>
|
||||
!selectedRecords.some(
|
||||
(selectedRecord) =>
|
||||
selectedRecord.recordIdentifier.id ===
|
||||
activityTargetObjectRecord.targetObject.id,
|
||||
),
|
||||
);
|
||||
return record.selected;
|
||||
});
|
||||
setActivityFromStore((currentActivity) => {
|
||||
if (isNull(currentActivity)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedTargetObjectsToCreate = selectedRecords.filter(
|
||||
(selectedRecord) =>
|
||||
!activityTargetWithTargetRecords.some(
|
||||
(activityTargetWithTargetRecord) =>
|
||||
activityTargetWithTargetRecord.targetObject.id ===
|
||||
selectedRecord.recordIdentifier.id,
|
||||
),
|
||||
);
|
||||
|
||||
const existingActivityTargets = activityTargetWithTargetRecords.map(
|
||||
(activityTargetObjectRecord) => activityTargetObjectRecord.activityTarget,
|
||||
);
|
||||
|
||||
let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
|
||||
|
||||
const activityTargetsToCreate = selectedTargetObjectsToCreate.map(
|
||||
(selectedRecord) => {
|
||||
const emptyActivityTarget = prefillRecord<ActivityTarget>({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
input: {
|
||||
id: v4(),
|
||||
activityId: activity.id,
|
||||
activity,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
[getActivityTargetObjectFieldName({
|
||||
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
|
||||
})]: selectedRecord.record,
|
||||
[getActivityTargetObjectFieldIdName({
|
||||
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
|
||||
})]: selectedRecord.recordIdentifier.id,
|
||||
},
|
||||
return {
|
||||
...currentActivity,
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
};
|
||||
});
|
||||
|
||||
return emptyActivityTarget;
|
||||
closeEditableField();
|
||||
},
|
||||
);
|
||||
[
|
||||
activityTargetWithTargetRecords,
|
||||
closeEditableField,
|
||||
relationPickerScopeId,
|
||||
setActivityFromStore,
|
||||
],
|
||||
);
|
||||
|
||||
activityTargetsAfterUpdate.push(...activityTargetsToCreate);
|
||||
|
||||
if (isNonEmptyArray(activityTargetsToDelete)) {
|
||||
activityTargetsAfterUpdate = activityTargetsAfterUpdate.filter(
|
||||
(activityTarget) =>
|
||||
!activityTargetsToDelete.some(
|
||||
(activityTargetToDelete) =>
|
||||
activityTargetToDelete.activityTarget.id === activityTarget.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
createManyActivityTargetsInCache(activityTargetsToCreate);
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (activityTargetsToCreate.length > 0) {
|
||||
await createManyActivityTargets(activityTargetsToCreate);
|
||||
}
|
||||
|
||||
if (activityTargetsToDelete.length > 0) {
|
||||
await deleteManyActivityTargets(
|
||||
activityTargetsToDelete.map(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.activityTarget.id,
|
||||
),
|
||||
const handleChange = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (recordId: string) => {
|
||||
const existingActivityTargets = activityTargetWithTargetRecords.map(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.activityTarget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setActivityFromStore((currentActivity) => {
|
||||
if (isNull(currentActivity)) {
|
||||
return null;
|
||||
}
|
||||
let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
|
||||
|
||||
return {
|
||||
...currentActivity,
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
};
|
||||
});
|
||||
};
|
||||
const previouslyCheckedRecordsIds = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const isNewlySelected = !previouslyCheckedRecordsIds.includes(recordId);
|
||||
|
||||
if (isNewlySelected) {
|
||||
const record = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: relationPickerScopeId,
|
||||
familyKey: recordId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
`Could not find selected record with id ${recordId}`,
|
||||
);
|
||||
}
|
||||
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
}),
|
||||
(prev) => [...prev, recordId],
|
||||
);
|
||||
|
||||
const newActivityTargetId = v4();
|
||||
const fieldName = getActivityTargetObjectFieldName({
|
||||
nameSingular: record.objectMetadataItem.nameSingular,
|
||||
});
|
||||
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: record.objectMetadataItem.nameSingular,
|
||||
});
|
||||
const newActivityTarget = prefillRecord<ActivityTarget>({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
input: {
|
||||
id: newActivityTargetId,
|
||||
activityId: activity.id,
|
||||
activity,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
[fieldName]: record.record,
|
||||
[fieldNameWithIdSuffix]: recordId,
|
||||
},
|
||||
});
|
||||
|
||||
activityTargetsAfterUpdate.push(newActivityTarget);
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
createManyActivityTargetsInCache([newActivityTarget]);
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await createManyActivityTargets([newActivityTarget]);
|
||||
}
|
||||
|
||||
set(activityTargetObjectRecordFamilyState(recordId), {
|
||||
activityTargetId: newActivityTargetId,
|
||||
});
|
||||
} else {
|
||||
const activityTargetToDeleteId = snapshot
|
||||
.getLoadable(activityTargetObjectRecordFamilyState(recordId))
|
||||
.getValue().activityTargetId;
|
||||
|
||||
if (!activityTargetToDeleteId) {
|
||||
throw new Error('Could not delete this activity target.');
|
||||
}
|
||||
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
}),
|
||||
previouslyCheckedRecordsIds.filter((id) => id !== recordId),
|
||||
);
|
||||
activityTargetsAfterUpdate = activityTargetsAfterUpdate.filter(
|
||||
(activityTarget) => activityTarget.id !== activityTargetToDeleteId,
|
||||
);
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await deleteManyActivityTargets([activityTargetToDeleteId]);
|
||||
}
|
||||
|
||||
set(activityTargetObjectRecordFamilyState(recordId), {
|
||||
activityTargetId: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
activity,
|
||||
activityTargetWithTargetRecords,
|
||||
createManyActivityTargets,
|
||||
createManyActivityTargetsInCache,
|
||||
deleteManyActivityTargets,
|
||||
isActivityInCreateMode,
|
||||
objectMetadataItemActivityTarget,
|
||||
relationPickerScopeId,
|
||||
upsertActivity,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledSelectContainer>
|
||||
<MultipleObjectRecordSelect
|
||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||
<ActivityTargetObjectRecordEffect
|
||||
activityTargetWithTargetRecords={activityTargetWithTargetRecords}
|
||||
/>
|
||||
<ActivityTargetInlineCellEditModeMultiRecordsEffect
|
||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||
/>
|
||||
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
||||
</RelationPickerScope>
|
||||
</StyledSelectContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const ActivityTargetObjectRecordEffect = ({
|
||||
activityTargetWithTargetRecords,
|
||||
}: {
|
||||
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
||||
}) => {
|
||||
const updateActivityTargets = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(newActivityTargets: ActivityTargetWithTargetRecord[]) => {
|
||||
for (const newActivityTarget of newActivityTargets) {
|
||||
const objectRecordId = newActivityTarget.targetObject.id;
|
||||
const record = snapshot
|
||||
.getLoadable(activityTargetObjectRecordFamilyState(objectRecordId))
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
record.activityTargetId,
|
||||
newActivityTarget.activityTarget.id,
|
||||
)
|
||||
) {
|
||||
set(activityTargetObjectRecordFamilyState(objectRecordId), {
|
||||
activityTargetId: newActivityTarget.activityTarget.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateActivityTargets(activityTargetWithTargetRecords);
|
||||
}, [activityTargetWithTargetRecords, updateActivityTargets]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -7,6 +7,8 @@ import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTa
|
||||
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||
@ -41,33 +43,45 @@ export const ActivityTargetsInlineCell = ({
|
||||
ActivityEditorHotkeyScope.ActivityTargets,
|
||||
);
|
||||
|
||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activity.id,
|
||||
fieldMetadataName: 'activityTargets',
|
||||
fieldPosition: 3,
|
||||
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
|
||||
<FieldFocusContextProvider>
|
||||
<RecordInlineCellContainer
|
||||
buttonIcon={IconPencil}
|
||||
customEditHotkeyScope={{
|
||||
scope: ActivityEditorHotkeyScope.ActivityTargets,
|
||||
}}
|
||||
IconLabel={showLabel ? IconArrowUpRight : undefined}
|
||||
showLabel={showLabel}
|
||||
readonly={readonly}
|
||||
labelWidth={fieldDefinition?.labelWidth}
|
||||
editModeContent={
|
||||
<ActivityTargetInlineCellEditMode
|
||||
activity={activity}
|
||||
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
||||
{ActivityTargetsContextProvider && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<RecordInlineCellContainer
|
||||
buttonIcon={IconPencil}
|
||||
customEditHotkeyScope={{
|
||||
scope: ActivityEditorHotkeyScope.ActivityTargets,
|
||||
}}
|
||||
IconLabel={showLabel ? IconArrowUpRight : undefined}
|
||||
showLabel={showLabel}
|
||||
readonly={readonly}
|
||||
labelWidth={fieldDefinition?.labelWidth}
|
||||
editModeContent={
|
||||
<ActivityTargetInlineCellEditMode
|
||||
activity={activity}
|
||||
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
||||
/>
|
||||
}
|
||||
label="Relations"
|
||||
displayModeContent={
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Relations"
|
||||
displayModeContent={
|
||||
<ActivityTargetChips
|
||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
}
|
||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
||||
/>
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</FieldFocusContextProvider>
|
||||
</RecordFieldInputScope>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user