mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-26 13:31:45 +03:00
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:
parent
dcb709feee
commit
7eb69a78ef
@ -5,17 +5,17 @@ import { FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE } from '@/activities/calend
|
|||||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
|
|
||||||
export const RightDrawerCalendarEvent = () => {
|
export const RightDrawerCalendarEvent = () => {
|
||||||
const { setRecords } = useSetRecordInStore();
|
const { upsertRecords } = useUpsertRecordsInStore();
|
||||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||||
const { record: calendarEvent } = useFindOneRecord<CalendarEvent>({
|
const { record: calendarEvent } = useFindOneRecord<CalendarEvent>({
|
||||||
objectNameSingular:
|
objectNameSingular:
|
||||||
FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular,
|
FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular,
|
||||||
objectRecordId: viewableRecordId ?? '',
|
objectRecordId: viewableRecordId ?? '',
|
||||||
recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields,
|
recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields,
|
||||||
onCompleted: (record) => setRecords([record]),
|
onCompleted: (record) => upsertRecords([record]),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!calendarEvent) {
|
if (!calendarEvent) {
|
||||||
|
@ -86,14 +86,6 @@ export const ActivityEditorFields = ({
|
|||||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
|
||||||
useFieldContext({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
|
||||||
objectRecordId: activityId,
|
|
||||||
fieldMetadataName: 'activityTargets',
|
|
||||||
fieldPosition: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledPropertyBox>
|
<StyledPropertyBox>
|
||||||
{activity.type === 'Task' &&
|
{activity.type === 'Task' &&
|
||||||
@ -112,16 +104,12 @@ export const ActivityEditorFields = ({
|
|||||||
</AssigneeFieldContextProvider>
|
</AssigneeFieldContextProvider>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{ActivityTargetsContextProvider &&
|
{isDefined(activityFromCache) && isRightDrawerAnimationCompleted && (
|
||||||
isDefined(activityFromCache) &&
|
<ActivityTargetsInlineCell
|
||||||
isRightDrawerAnimationCompleted && (
|
activity={activityFromCache}
|
||||||
<ActivityTargetsContextProvider>
|
maxWidth={340}
|
||||||
<ActivityTargetsInlineCell
|
/>
|
||||||
activity={activityFromCache}
|
)}
|
||||||
maxWidth={340}
|
|
||||||
/>
|
|
||||||
</ActivityTargetsContextProvider>
|
|
||||||
)}
|
|
||||||
</StyledPropertyBox>
|
</StyledPropertyBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,11 +8,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
|
|
||||||
export const useRightDrawerEmailThread = () => {
|
export const useRightDrawerEmailThread = () => {
|
||||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||||
const { setRecords } = useSetRecordInStore();
|
const { upsertRecords } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
const { record: thread } = useFindOneRecord<EmailThread>({
|
const { record: thread } = useFindOneRecord<EmailThread>({
|
||||||
objectNameSingular: CoreObjectNameSingular.MessageThread,
|
objectNameSingular: CoreObjectNameSingular.MessageThread,
|
||||||
@ -20,7 +20,7 @@ export const useRightDrawerEmailThread = () => {
|
|||||||
recordGqlFields: {
|
recordGqlFields: {
|
||||||
id: true,
|
id: true,
|
||||||
},
|
},
|
||||||
onCompleted: (record) => setRecords([record]),
|
onCompleted: (record) => upsertRecords([record]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE =
|
const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE =
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import { objectRecordsIdsMultiSelecComponentState } from '@/activities/states/objectRecordsIdsMultiSelectComponentState';
|
||||||
|
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
||||||
|
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
|
||||||
|
import { recordMultiSelectIsLoadingComponentState } from '@/object-record/record-field/states/recordMultiSelectIsLoadingComponentState';
|
||||||
|
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
||||||
|
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||||
|
|
||||||
|
export const useObjectRecordMultiSelectScopedStates = (scopeId: string) => {
|
||||||
|
const objectRecordsIdsMultiSelectState = extractComponentState(
|
||||||
|
objectRecordsIdsMultiSelecComponentState,
|
||||||
|
scopeId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectRecordMultiSelectCheckedRecordsIdsState = extractComponentState(
|
||||||
|
objectRecordMultiSelectCheckedRecordsIdsComponentState,
|
||||||
|
scopeId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const objectRecordMultiSelectFamilyState = extractComponentFamilyState(
|
||||||
|
objectRecordMultiSelectComponentFamilyState,
|
||||||
|
scopeId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const recordMultiSelectIsLoadingState = extractComponentState(
|
||||||
|
recordMultiSelectIsLoadingComponentState,
|
||||||
|
scopeId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
objectRecordsIdsMultiSelectState,
|
||||||
|
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||||
|
objectRecordMultiSelectFamilyState,
|
||||||
|
recordMultiSelectIsLoadingState,
|
||||||
|
};
|
||||||
|
};
|
@ -1,9 +1,10 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyArray, isNull } from '@sniptt/guards';
|
import { isNull } from '@sniptt/guards';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||||
|
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
|
||||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Activity } from '@/activities/types/Activity';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
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 { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
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 { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect';
|
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
|
||||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
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';
|
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||||
|
|
||||||
const StyledSelectContainer = styled.div`
|
const StyledSelectContainer = styled.div`
|
||||||
@ -37,6 +45,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
activityTargetWithTargetRecords,
|
activityTargetWithTargetRecords,
|
||||||
}: ActivityTargetInlineCellEditModeProps) => {
|
}: ActivityTargetInlineCellEditModeProps) => {
|
||||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||||
|
const relationPickerScopeId = `relation-picker-${activity.id}`;
|
||||||
|
|
||||||
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
|
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
|
||||||
(activityTarget) => ({
|
(activityTarget) => ({
|
||||||
@ -74,109 +83,181 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
|
const handleSubmit = useRecoilCallback(
|
||||||
closeEditableField();
|
({ 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(
|
return record.selected;
|
||||||
(activityTargetObjectRecord) =>
|
});
|
||||||
!selectedRecords.some(
|
setActivityFromStore((currentActivity) => {
|
||||||
(selectedRecord) =>
|
if (isNull(currentActivity)) {
|
||||||
selectedRecord.recordIdentifier.id ===
|
return null;
|
||||||
activityTargetObjectRecord.targetObject.id,
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedTargetObjectsToCreate = selectedRecords.filter(
|
return {
|
||||||
(selectedRecord) =>
|
...currentActivity,
|
||||||
!activityTargetWithTargetRecords.some(
|
activityTargets: activityTargetsAfterUpdate,
|
||||||
(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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
closeEditableField();
|
||||||
return emptyActivityTarget;
|
|
||||||
},
|
},
|
||||||
);
|
[
|
||||||
|
activityTargetWithTargetRecords,
|
||||||
|
closeEditableField,
|
||||||
|
relationPickerScopeId,
|
||||||
|
setActivityFromStore,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
activityTargetsAfterUpdate.push(...activityTargetsToCreate);
|
const handleChange = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
if (isNonEmptyArray(activityTargetsToDelete)) {
|
async (recordId: string) => {
|
||||||
activityTargetsAfterUpdate = activityTargetsAfterUpdate.filter(
|
const existingActivityTargets = activityTargetWithTargetRecords.map(
|
||||||
(activityTarget) =>
|
(activityTargetObjectRecord) =>
|
||||||
!activityTargetsToDelete.some(
|
activityTargetObjectRecord.activityTarget,
|
||||||
(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,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setActivityFromStore((currentActivity) => {
|
let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
|
||||||
if (isNull(currentActivity)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
const previouslyCheckedRecordsIds = snapshot
|
||||||
...currentActivity,
|
.getLoadable(
|
||||||
activityTargets: activityTargetsAfterUpdate,
|
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 (
|
return (
|
||||||
<StyledSelectContainer>
|
<StyledSelectContainer>
|
||||||
<MultipleObjectRecordSelect
|
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
<ActivityTargetObjectRecordEffect
|
||||||
onSubmit={handleSubmit}
|
activityTargetWithTargetRecords={activityTargetWithTargetRecords}
|
||||||
/>
|
/>
|
||||||
|
<ActivityTargetInlineCellEditModeMultiRecordsEffect
|
||||||
|
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||||
|
/>
|
||||||
|
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
||||||
|
</RelationPickerScope>
|
||||||
</StyledSelectContainer>
|
</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 { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Activity } from '@/activities/types/Activity';
|
||||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
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 { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||||
@ -41,33 +43,45 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
ActivityEditorHotkeyScope.ActivityTargets,
|
ActivityEditorHotkeyScope.ActivityTargets,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||||
|
useFieldContext({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||||
|
objectRecordId: activity.id,
|
||||||
|
fieldMetadataName: 'activityTargets',
|
||||||
|
fieldPosition: 3,
|
||||||
|
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
|
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
|
||||||
<FieldFocusContextProvider>
|
<FieldFocusContextProvider>
|
||||||
<RecordInlineCellContainer
|
{ActivityTargetsContextProvider && (
|
||||||
buttonIcon={IconPencil}
|
<ActivityTargetsContextProvider>
|
||||||
customEditHotkeyScope={{
|
<RecordInlineCellContainer
|
||||||
scope: ActivityEditorHotkeyScope.ActivityTargets,
|
buttonIcon={IconPencil}
|
||||||
}}
|
customEditHotkeyScope={{
|
||||||
IconLabel={showLabel ? IconArrowUpRight : undefined}
|
scope: ActivityEditorHotkeyScope.ActivityTargets,
|
||||||
showLabel={showLabel}
|
}}
|
||||||
readonly={readonly}
|
IconLabel={showLabel ? IconArrowUpRight : undefined}
|
||||||
labelWidth={fieldDefinition?.labelWidth}
|
showLabel={showLabel}
|
||||||
editModeContent={
|
readonly={readonly}
|
||||||
<ActivityTargetInlineCellEditMode
|
labelWidth={fieldDefinition?.labelWidth}
|
||||||
activity={activity}
|
editModeContent={
|
||||||
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
<ActivityTargetInlineCellEditMode
|
||||||
|
activity={activity}
|
||||||
|
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Relations"
|
||||||
|
displayModeContent={
|
||||||
|
<ActivityTargetChips
|
||||||
|
activityTargetObjectRecords={activityTargetObjectRecords}
|
||||||
|
maxWidth={maxWidth}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
</ActivityTargetsContextProvider>
|
||||||
label="Relations"
|
)}
|
||||||
displayModeContent={
|
|
||||||
<ActivityTargetChips
|
|
||||||
activityTargetObjectRecords={activityTargetObjectRecords}
|
|
||||||
maxWidth={maxWidth}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
|
||||||
/>
|
|
||||||
</FieldFocusContextProvider>
|
</FieldFocusContextProvider>
|
||||||
</RecordFieldInputScope>
|
</RecordFieldInputScope>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||||
|
|
||||||
|
export const objectRecordsIdsMultiSelecComponentState = createComponentState<
|
||||||
|
string[]
|
||||||
|
>({
|
||||||
|
key: 'objectRecordsIdsMultiSelectComponentState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
@ -4,7 +4,7 @@ import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
|||||||
import { useActivities } from '@/activities/hooks/useActivities';
|
import { useActivities } from '@/activities/hooks/useActivities';
|
||||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||||
import { timelineActivitiesFammilyState } from '@/activities/timeline/states/timelineActivitiesFamilyState';
|
import { timelineActivitiesFamilyState } from '@/activities/timeline/states/timelineActivitiesFamilyState';
|
||||||
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
|
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
|
||||||
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityWithoutTargetsFamilyState';
|
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityWithoutTargetsFamilyState';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Activity } from '@/activities/types/Activity';
|
||||||
@ -68,11 +68,11 @@ export const TimelineQueryEffect = ({
|
|||||||
(newActivities: Activity[]) => {
|
(newActivities: Activity[]) => {
|
||||||
for (const newActivity of newActivities) {
|
for (const newActivity of newActivities) {
|
||||||
const currentActivity = snapshot
|
const currentActivity = snapshot
|
||||||
.getLoadable(timelineActivitiesFammilyState(newActivity.id))
|
.getLoadable(timelineActivitiesFamilyState(newActivity.id))
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
if (!isDeeplyEqual(newActivity, currentActivity)) {
|
if (!isDeeplyEqual(newActivity, currentActivity)) {
|
||||||
set(timelineActivitiesFammilyState(newActivity.id), newActivity);
|
set(timelineActivitiesFamilyState(newActivity.id), newActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentActivityWithoutTarget = snapshot
|
const currentActivityWithoutTarget = snapshot
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Activity } from '@/activities/types/Activity';
|
import { Activity } from '@/activities/types/Activity';
|
||||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||||
|
|
||||||
export const timelineActivitiesFammilyState = createFamilyState<
|
export const timelineActivitiesFamilyState = createFamilyState<
|
||||||
Activity | null,
|
Activity | null,
|
||||||
string
|
string
|
||||||
>({
|
>({
|
||||||
key: 'timelineActivitiesFammilyState',
|
key: 'timelineActivitiesFamilyState',
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,7 @@ import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-dra
|
|||||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import {
|
import {
|
||||||
formatToHumanReadableDay,
|
formatToHumanReadableDay,
|
||||||
formatToHumanReadableMonth,
|
formatToHumanReadableMonth,
|
||||||
@ -85,7 +85,7 @@ export const EventCardCalendarEvent = ({
|
|||||||
}: {
|
}: {
|
||||||
calendarEventId: string;
|
calendarEventId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { setRecords } = useSetRecordInStore();
|
const { upsertRecords } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
record: calendarEvent,
|
record: calendarEvent,
|
||||||
@ -101,7 +101,7 @@ export const EventCardCalendarEvent = ({
|
|||||||
endsAt: true,
|
endsAt: true,
|
||||||
},
|
},
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
setRecords([data]);
|
upsertRecords([data]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage
|
|||||||
import { EventCardMessageNotShared } from '@/activities/timelineActivities/rows/message/components/EventCardMessageNotShared';
|
import { EventCardMessageNotShared } from '@/activities/timelineActivities/rows/message/components/EventCardMessageNotShared';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
const StyledEventCardMessageContainer = styled.div`
|
const StyledEventCardMessageContainer = styled.div`
|
||||||
@ -56,7 +56,7 @@ export const EventCardMessage = ({
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
authorFullName: string;
|
authorFullName: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { setRecords } = useSetRecordInStore();
|
const { upsertRecords } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
record: message,
|
record: message,
|
||||||
@ -75,7 +75,7 @@ export const EventCardMessage = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
setRecords([data]);
|
upsertRecords([data]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
|||||||
isDeeplyEqual(
|
isDeeplyEqual(
|
||||||
currentFieldValueOnSourceRecord,
|
currentFieldValueOnSourceRecord,
|
||||||
updatedFieldValueOnSourceRecord,
|
updatedFieldValueOnSourceRecord,
|
||||||
|
{ strict: true },
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import {
|
||||||
import { isDefined } from '~/utils/isDefined';
|
FieldMetadataType,
|
||||||
|
RelationDefinitionType,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
||||||
|
|
||||||
@ -10,6 +12,15 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
|||||||
fields: Array<ObjectMetadataItem['fields'][0]>;
|
fields: Array<ObjectMetadataItem['fields'][0]>;
|
||||||
}): FilterDefinition[] =>
|
}): FilterDefinition[] =>
|
||||||
fields.reduce((acc, field) => {
|
fields.reduce((acc, field) => {
|
||||||
|
if (
|
||||||
|
field.type === FieldMetadataType.Relation &&
|
||||||
|
field.relationDefinition?.direction !==
|
||||||
|
RelationDefinitionType.ManyToOne &&
|
||||||
|
field.relationDefinition?.direction !== RelationDefinitionType.OneToOne
|
||||||
|
) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
![
|
![
|
||||||
FieldMetadataType.DateTime,
|
FieldMetadataType.DateTime,
|
||||||
@ -33,12 +44,6 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === FieldMetadataType.Relation) {
|
|
||||||
if (isDefined(field.fromRelationMetadata)) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...acc, formatFieldMetadataItemAsFilterDefinition({ field })];
|
return [...acc, formatFieldMetadataItemAsFilterDefinition({ field })];
|
||||||
}, [] as FilterDefinition[]);
|
}, [] as FilterDefinition[]);
|
||||||
|
|
||||||
@ -51,9 +56,9 @@ export const formatFieldMetadataItemAsFilterDefinition = ({
|
|||||||
label: field.label,
|
label: field.label,
|
||||||
iconName: field.icon ?? 'Icon123',
|
iconName: field.icon ?? 'Icon123',
|
||||||
relationObjectMetadataNamePlural:
|
relationObjectMetadataNamePlural:
|
||||||
field.toRelationMetadata?.fromObjectMetadata.namePlural,
|
field.relationDefinition?.targetObjectMetadata.namePlural,
|
||||||
relationObjectMetadataNameSingular:
|
relationObjectMetadataNameSingular:
|
||||||
field.toRelationMetadata?.fromObjectMetadata.nameSingular,
|
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
||||||
type: getFilterTypeFromFieldType(field.type),
|
type: getFilterTypeFromFieldType(field.type),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|||||||
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
||||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||||
|
import { getRefName } from '@/object-record/cache/utils/getRefName';
|
||||||
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import {
|
import {
|
||||||
@ -39,7 +40,7 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
|||||||
|
|
||||||
if (!isRootLevel && computeReferences) {
|
if (!isRootLevel && computeReferences) {
|
||||||
return {
|
return {
|
||||||
__ref: `${nodeTypeName}:${record.id}`,
|
__ref: getRefName(objectMetadataItem.nameSingular, record.id),
|
||||||
} as unknown as RecordGqlNode; // Fix typing: we want a Reference in computeReferences mode
|
} as unknown as RecordGqlNode; // Fix typing: we want a Reference in computeReferences mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
packages/twenty-front/src/modules/object-record/cache/utils/getRefName.ts
vendored
Normal file
7
packages/twenty-front/src/modules/object-record/cache/utils/getRefName.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
||||||
|
|
||||||
|
export const getRefName = (objectNameSingular: string, id: string) => {
|
||||||
|
const nodeTypeName = getNodeTypename(objectNameSingular);
|
||||||
|
|
||||||
|
return `${nodeTypeName}:${id}`;
|
||||||
|
};
|
@ -13,11 +13,13 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
|
|||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
cache,
|
cache,
|
||||||
|
recordGqlFields = undefined,
|
||||||
record,
|
record,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItems: ObjectMetadataItem[];
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
cache: ApolloCache<object>;
|
cache: ApolloCache<object>;
|
||||||
|
recordGqlFields?: Record<string, any>;
|
||||||
record: T;
|
record: T;
|
||||||
}) => {
|
}) => {
|
||||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||||
@ -32,6 +34,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
|
|||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
computeReferences: true,
|
computeReferences: true,
|
||||||
|
recordGqlFields,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
`;
|
`;
|
||||||
|
@ -1,14 +1,31 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const generateDepthOneRecordGqlFields = ({
|
export const generateDepthOneRecordGqlFields = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
|
record,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
record?: Record<string, any>;
|
||||||
}) => {
|
}) => {
|
||||||
return objectMetadataItem.fields.reduce((acc, field) => {
|
const gqlFieldsFromObjectMetadataItem = objectMetadataItem.fields.reduce(
|
||||||
return {
|
(acc, field) => {
|
||||||
...acc,
|
return {
|
||||||
[field.name]: true,
|
...acc,
|
||||||
};
|
[field.name]: true,
|
||||||
}, {});
|
};
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDefined(record)) {
|
||||||
|
return Object.keys(gqlFieldsFromObjectMetadataItem).reduce((acc, key) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: Object.keys(record).includes(key),
|
||||||
|
};
|
||||||
|
}, gqlFieldsFromObjectMetadataItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return gqlFieldsFromObjectMetadataItem;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
|
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||||
|
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||||
|
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||||
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type useAttachRelatedRecordFromRecordProps = {
|
||||||
|
recordObjectNameSingular: string;
|
||||||
|
fieldNameOnRecordObject: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAttachRelatedRecordFromRecord = ({
|
||||||
|
recordObjectNameSingular,
|
||||||
|
fieldNameOnRecordObject,
|
||||||
|
}: useAttachRelatedRecordFromRecordProps) => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular: recordObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldOnObject = objectMetadataItem.fields.find((field) => {
|
||||||
|
return field.name === fieldNameOnRecordObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const relatedRecordObjectNameSingular =
|
||||||
|
fieldOnObject?.relationDefinition?.targetObjectMetadata.nameSingular;
|
||||||
|
|
||||||
|
if (!relatedRecordObjectNameSingular) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find record related to ${recordObjectNameSingular}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { objectMetadataItem: relatedObjectMetadataItem } =
|
||||||
|
useObjectMetadataItem({
|
||||||
|
objectNameSingular: relatedRecordObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldOnRelatedObject =
|
||||||
|
fieldOnObject?.relationDefinition?.targetFieldMetadata.name;
|
||||||
|
|
||||||
|
if (!fieldOnRelatedObject) {
|
||||||
|
throw new Error(`Missing target field for ${fieldNameOnRecordObject}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateOneRecord } = useUpdateOneRecord({
|
||||||
|
objectNameSingular: relatedRecordObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRecordFromCache = useGetRecordFromCache({
|
||||||
|
objectNameSingular: recordObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRelatedRecordFromCache = useGetRecordFromCache({
|
||||||
|
objectNameSingular: relatedRecordObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
|
|
||||||
|
const updateOneRecordAndAttachRelations = async ({
|
||||||
|
recordId,
|
||||||
|
relatedRecordId,
|
||||||
|
}: {
|
||||||
|
recordId: string;
|
||||||
|
relatedRecordId: string;
|
||||||
|
}) => {
|
||||||
|
const cachedRelatedRecord =
|
||||||
|
getRelatedRecordFromCache<ObjectRecord>(relatedRecordId);
|
||||||
|
|
||||||
|
if (!cachedRelatedRecord) {
|
||||||
|
throw new Error('could not find cached related record');
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousRecordId = cachedRelatedRecord?.[`${fieldOnRelatedObject}Id`];
|
||||||
|
|
||||||
|
if (isDefined(previousRecordId)) {
|
||||||
|
const previousRecord = getRecordFromCache<ObjectRecord>(previousRecordId);
|
||||||
|
|
||||||
|
const previousRecordWithRelation = {
|
||||||
|
...cachedRelatedRecord,
|
||||||
|
[fieldOnRelatedObject]: previousRecord,
|
||||||
|
};
|
||||||
|
const gqlFields = generateDepthOneRecordGqlFields({
|
||||||
|
objectMetadataItem: relatedObjectMetadataItem,
|
||||||
|
record: previousRecordWithRelation,
|
||||||
|
});
|
||||||
|
updateRecordFromCache({
|
||||||
|
objectMetadataItems,
|
||||||
|
objectMetadataItem: relatedObjectMetadataItem,
|
||||||
|
cache: apolloClient.cache,
|
||||||
|
record: {
|
||||||
|
...cachedRelatedRecord,
|
||||||
|
[fieldOnRelatedObject]: previousRecord,
|
||||||
|
},
|
||||||
|
recordGqlFields: gqlFields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateOneRecord({
|
||||||
|
idToUpdate: relatedRecordId,
|
||||||
|
updateOneRecordInput: {
|
||||||
|
[`${fieldOnRelatedObject}Id`]: recordId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { updateOneRecordAndAttachRelations };
|
||||||
|
};
|
@ -0,0 +1,88 @@
|
|||||||
|
import { Reference, useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { getRefName } from '@/object-record/cache/utils/getRefName';
|
||||||
|
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||||
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
|
||||||
|
type useDetachRelatedRecordFromRecordProps = {
|
||||||
|
recordObjectNameSingular: string;
|
||||||
|
fieldNameOnRecordObject: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDetachRelatedRecordFromRecord = ({
|
||||||
|
recordObjectNameSingular,
|
||||||
|
fieldNameOnRecordObject,
|
||||||
|
}: useDetachRelatedRecordFromRecordProps) => {
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular: recordObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldOnObject = objectMetadataItem.fields.find((field) => {
|
||||||
|
return field.name === fieldNameOnRecordObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const relatedRecordObjectNameSingular =
|
||||||
|
fieldOnObject?.relationDefinition?.targetObjectMetadata.nameSingular;
|
||||||
|
|
||||||
|
const fieldOnRelatedObject =
|
||||||
|
fieldOnObject?.relationDefinition?.targetFieldMetadata.name;
|
||||||
|
|
||||||
|
if (!relatedRecordObjectNameSingular) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find record related to ${recordObjectNameSingular}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateOneRecord } = useUpdateOneRecord({
|
||||||
|
objectNameSingular: relatedRecordObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateOneRecordAndDetachRelations = async ({
|
||||||
|
recordId,
|
||||||
|
relatedRecordId,
|
||||||
|
}: {
|
||||||
|
recordId: string;
|
||||||
|
relatedRecordId: string;
|
||||||
|
}) => {
|
||||||
|
modifyRecordFromCache({
|
||||||
|
objectMetadataItem,
|
||||||
|
cache: apolloClient.cache,
|
||||||
|
fieldModifiers: {
|
||||||
|
[fieldNameOnRecordObject]: (
|
||||||
|
fieldNameOnRecordObjectConnection,
|
||||||
|
{ readField },
|
||||||
|
) => {
|
||||||
|
const edges = readField<{ node: Reference }[]>(
|
||||||
|
'edges',
|
||||||
|
fieldNameOnRecordObjectConnection,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!edges) return fieldNameOnRecordObjectConnection;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fieldNameOnRecordObjectConnection,
|
||||||
|
edges: edges.filter(
|
||||||
|
(edge) =>
|
||||||
|
!(
|
||||||
|
edge.node.__ref ===
|
||||||
|
getRefName(relatedRecordObjectNameSingular, relatedRecordId)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recordId,
|
||||||
|
});
|
||||||
|
await updateOneRecord({
|
||||||
|
idToUpdate: relatedRecordId,
|
||||||
|
updateOneRecordInput: {
|
||||||
|
[`${fieldOnRelatedObject}Id`]: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { updateOneRecordAndDetachRelations };
|
||||||
|
};
|
@ -19,6 +19,7 @@ export const useFieldContext = ({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
objectRecordId,
|
objectRecordId,
|
||||||
customUseUpdateOneObjectHook,
|
customUseUpdateOneObjectHook,
|
||||||
|
overridenIsFieldEmpty,
|
||||||
}: {
|
}: {
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
fieldMetadataName: string;
|
fieldMetadataName: string;
|
||||||
@ -27,6 +28,7 @@ export const useFieldContext = ({
|
|||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
objectRecordId: string;
|
objectRecordId: string;
|
||||||
customUseUpdateOneObjectHook?: RecordUpdateHook;
|
customUseUpdateOneObjectHook?: RecordUpdateHook;
|
||||||
|
overridenIsFieldEmpty?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
@ -78,6 +80,7 @@ export const useFieldContext = ({
|
|||||||
customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation,
|
customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation,
|
||||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||||
clearable,
|
clearable,
|
||||||
|
overridenIsFieldEmpty,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -47,9 +47,11 @@ export const useUpdateOneRecord = <
|
|||||||
const updateOneRecord = async ({
|
const updateOneRecord = async ({
|
||||||
idToUpdate,
|
idToUpdate,
|
||||||
updateOneRecordInput,
|
updateOneRecordInput,
|
||||||
|
optimisticRecord,
|
||||||
}: {
|
}: {
|
||||||
idToUpdate: string;
|
idToUpdate: string;
|
||||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||||
|
optimisticRecord?: Partial<ObjectRecord>;
|
||||||
}) => {
|
}) => {
|
||||||
const sanitizedInput = {
|
const sanitizedInput = {
|
||||||
...sanitizeRecordInput({
|
...sanitizeRecordInput({
|
||||||
@ -68,16 +70,16 @@ export const useUpdateOneRecord = <
|
|||||||
computeReferences: true,
|
computeReferences: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const optimisticRecord = {
|
const computedOptimisticRecord = {
|
||||||
...cachedRecord,
|
...cachedRecord,
|
||||||
...sanitizedInput,
|
...(optimisticRecord ?? sanitizedInput),
|
||||||
...{ id: idToUpdate },
|
...{ id: idToUpdate },
|
||||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||||
};
|
};
|
||||||
|
|
||||||
const optimisticRecordWithConnection =
|
const optimisticRecordWithConnection =
|
||||||
getRecordNodeFromRecord<ObjectRecord>({
|
getRecordNodeFromRecord<ObjectRecord>({
|
||||||
record: optimisticRecord,
|
record: computedOptimisticRecord,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
recordGqlFields: computedRecordGqlFields,
|
recordGqlFields: computedRecordGqlFields,
|
||||||
@ -92,7 +94,7 @@ export const useUpdateOneRecord = <
|
|||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
cache: apolloClient.cache,
|
cache: apolloClient.cache,
|
||||||
record: optimisticRecord,
|
record: computedOptimisticRecord,
|
||||||
});
|
});
|
||||||
|
|
||||||
triggerUpdateRecordOptimisticEffect({
|
triggerUpdateRecordOptimisticEffect({
|
||||||
|
@ -3,10 +3,13 @@ import { useContext } from 'react';
|
|||||||
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
||||||
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
|
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
|
||||||
|
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating';
|
||||||
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
|
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||||
import { isFieldChipDisplay } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
import { isFieldChipDisplay } from '@/object-record/utils/getRecordChipGeneratorPerObjectPerField';
|
||||||
|
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
@ -22,7 +25,7 @@ import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisp
|
|||||||
import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay';
|
import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay';
|
||||||
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
|
import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
|
||||||
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
|
import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay';
|
||||||
import { RelationFieldDisplay } from '../meta-types/display/components/RelationFieldDisplay';
|
import { RelationToOneFieldDisplay } from '../meta-types/display/components/RelationToOneFieldDisplay';
|
||||||
import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay';
|
import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay';
|
||||||
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
|
import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
|
||||||
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
|
import { UuidFieldDisplay } from '../meta-types/display/components/UuidFieldDisplay';
|
||||||
@ -37,7 +40,6 @@ import { isFieldMultiSelect } from '../types/guards/isFieldMultiSelect';
|
|||||||
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
||||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||||
import { isFieldRawJson } from '../types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '../types/guards/isFieldRawJson';
|
||||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
|
||||||
import { isFieldSelect } from '../types/guards/isFieldSelect';
|
import { isFieldSelect } from '../types/guards/isFieldSelect';
|
||||||
import { isFieldText } from '../types/guards/isFieldText';
|
import { isFieldText } from '../types/guards/isFieldText';
|
||||||
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||||
@ -49,8 +51,10 @@ export const FieldDisplay = () => {
|
|||||||
|
|
||||||
return isChipDisplay ? (
|
return isChipDisplay ? (
|
||||||
<ChipFieldDisplay />
|
<ChipFieldDisplay />
|
||||||
) : isFieldRelation(fieldDefinition) ? (
|
) : isFieldRelationToOneObject(fieldDefinition) ? (
|
||||||
<RelationFieldDisplay />
|
<RelationToOneFieldDisplay />
|
||||||
|
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||||
|
<RelationFromManyFieldDisplay />
|
||||||
) : isFieldPhone(fieldDefinition) ||
|
) : isFieldPhone(fieldDefinition) ||
|
||||||
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
||||||
<PhoneFieldDisplay />
|
<PhoneFieldDisplay />
|
||||||
|
@ -6,7 +6,7 @@ import { FullNameFieldInput } from '@/object-record/record-field/meta-types/inpu
|
|||||||
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
|
import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput';
|
||||||
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
||||||
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput';
|
||||||
import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput';
|
import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput';
|
||||||
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput';
|
||||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||||
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate';
|
||||||
@ -16,6 +16,7 @@ import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldL
|
|||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
||||||
|
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput';
|
|||||||
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
|
import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput';
|
||||||
import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput';
|
import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput';
|
||||||
import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput';
|
import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput';
|
||||||
import { RelationFieldInput } from '../meta-types/input/components/RelationFieldInput';
|
import { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput';
|
||||||
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
import { TextFieldInput } from '../meta-types/input/components/TextFieldInput';
|
||||||
import { FieldInputEvent } from '../types/FieldInputEvent';
|
import { FieldInputEvent } from '../types/FieldInputEvent';
|
||||||
import { isFieldAddress } from '../types/guards/isFieldAddress';
|
import { isFieldAddress } from '../types/guards/isFieldAddress';
|
||||||
@ -40,7 +41,6 @@ import { isFieldLink } from '../types/guards/isFieldLink';
|
|||||||
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
||||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||||
import { isFieldRating } from '../types/guards/isFieldRating';
|
import { isFieldRating } from '../types/guards/isFieldRating';
|
||||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
|
||||||
import { isFieldText } from '../types/guards/isFieldText';
|
import { isFieldText } from '../types/guards/isFieldText';
|
||||||
|
|
||||||
type FieldInputProps = {
|
type FieldInputProps = {
|
||||||
@ -72,16 +72,10 @@ export const FieldInput = ({
|
|||||||
<RecordFieldInputScope
|
<RecordFieldInputScope
|
||||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||||
>
|
>
|
||||||
{isFieldRelation(fieldDefinition) ? (
|
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||||
isFieldRelationFromManyObjects(fieldDefinition) ? (
|
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
<RelationManyFieldInput
|
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||||
relationPickerScopeId={getScopeIdFromComponentId(
|
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||||
`relation-picker-${fieldDefinition.fieldMetadataId}`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
|
||||||
)
|
|
||||||
) : isFieldPhone(fieldDefinition) ||
|
) : isFieldPhone(fieldDefinition) ||
|
||||||
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
||||||
<PhoneFieldInput
|
<PhoneFieldInput
|
||||||
|
@ -30,6 +30,7 @@ export type GenericFieldContextType = {
|
|||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
isCentered?: boolean;
|
isCentered?: boolean;
|
||||||
|
overridenIsFieldEmpty?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FieldContext = createContext<GenericFieldContextType>(
|
export const FieldContext = createContext<GenericFieldContextType>(
|
||||||
|
@ -2,17 +2,22 @@ import { useContext } from 'react';
|
|||||||
|
|
||||||
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
||||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
|
|
||||||
export const useIsFieldEmpty = () => {
|
export const useIsFieldEmpty = () => {
|
||||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
const { entityId, fieldDefinition, overridenIsFieldEmpty } =
|
||||||
|
useContext(FieldContext);
|
||||||
const fieldValue = useRecordFieldValue(
|
const fieldValue = useRecordFieldValue(
|
||||||
entityId,
|
entityId,
|
||||||
fieldDefinition.metadata.fieldName,
|
fieldDefinition?.metadata?.fieldName ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isDefined(overridenIsFieldEmpty)) {
|
||||||
|
return overridenIsFieldEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
return isFieldValueEmpty({
|
return isFieldValueEmpty({
|
||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
|
@ -15,7 +15,8 @@ import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/is
|
|||||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||||
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
|
||||||
|
import { isFieldRelationToOneValue } from '@/object-record/record-field/types/guards/isFieldRelationToOneValue';
|
||||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||||
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
|
import { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
|
||||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||||
@ -38,8 +39,6 @@ import { isFieldPhone } from '../types/guards/isFieldPhone';
|
|||||||
import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue';
|
import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue';
|
||||||
import { isFieldRating } from '../types/guards/isFieldRating';
|
import { isFieldRating } from '../types/guards/isFieldRating';
|
||||||
import { isFieldRatingValue } from '../types/guards/isFieldRatingValue';
|
import { isFieldRatingValue } from '../types/guards/isFieldRatingValue';
|
||||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
|
||||||
import { isFieldRelationValue } from '../types/guards/isFieldRelationValue';
|
|
||||||
import { isFieldText } from '../types/guards/isFieldText';
|
import { isFieldText } from '../types/guards/isFieldText';
|
||||||
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
|
import { isFieldTextValue } from '../types/guards/isFieldTextValue';
|
||||||
|
|
||||||
@ -55,14 +54,10 @@ export const usePersistField = () => {
|
|||||||
const persistField = useRecoilCallback(
|
const persistField = useRecoilCallback(
|
||||||
({ set }) =>
|
({ set }) =>
|
||||||
(valueToPersist: unknown) => {
|
(valueToPersist: unknown) => {
|
||||||
const fieldIsRelation =
|
const fieldIsRelationToOneObject =
|
||||||
isFieldRelation(fieldDefinition) &&
|
isFieldRelationToOneObject(
|
||||||
isFieldRelationValue(valueToPersist);
|
|
||||||
|
|
||||||
const fieldIsRelationFromManyObjects =
|
|
||||||
isFieldRelationFromManyObjects(
|
|
||||||
fieldDefinition as FieldDefinition<FieldRelationMetadata>,
|
fieldDefinition as FieldDefinition<FieldRelationMetadata>,
|
||||||
) && isFieldRelationValue(valueToPersist);
|
) && isFieldRelationToOneValue(valueToPersist);
|
||||||
|
|
||||||
const fieldIsText =
|
const fieldIsText =
|
||||||
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
|
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
|
||||||
@ -120,7 +115,7 @@ export const usePersistField = () => {
|
|||||||
isFieldRawJsonValue(valueToPersist);
|
isFieldRawJsonValue(valueToPersist);
|
||||||
|
|
||||||
const isValuePersistable =
|
const isValuePersistable =
|
||||||
(fieldIsRelation && !fieldIsRelationFromManyObjects) ||
|
fieldIsRelationToOneObject ||
|
||||||
fieldIsText ||
|
fieldIsText ||
|
||||||
fieldIsBoolean ||
|
fieldIsBoolean ||
|
||||||
fieldIsEmail ||
|
fieldIsEmail ||
|
||||||
@ -145,7 +140,7 @@ export const usePersistField = () => {
|
|||||||
valueToPersist,
|
valueToPersist,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fieldIsRelation && !fieldIsRelationFromManyObjects) {
|
if (fieldIsRelationToOneObject) {
|
||||||
const value = valueToPersist as EntityForSelect;
|
const value = valueToPersist as EntityForSelect;
|
||||||
updateRecord?.({
|
updateRecord?.({
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
import { isArray } from '@sniptt/guards';
|
|
||||||
import { EntityChip } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
|
||||||
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
|
|
||||||
import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
|
||||||
|
|
||||||
export const RelationFieldDisplay = () => {
|
|
||||||
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
|
||||||
useRelationFieldDisplay();
|
|
||||||
|
|
||||||
if (
|
|
||||||
!fieldValue ||
|
|
||||||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray(fieldValue) && isFieldRelationFromManyObjects(fieldDefinition)) {
|
|
||||||
return (
|
|
||||||
<RelationFromManyFieldDisplay fieldValue={fieldValue as ObjectRecord[]} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordChipData = generateRecordChipData(fieldValue);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EntityChip
|
|
||||||
entityId={fieldValue.id}
|
|
||||||
name={recordChipData.name as any}
|
|
||||||
avatarType={recordChipData.avatarType}
|
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(recordChipData.avatarUrl) || ''}
|
|
||||||
linkToEntity={recordChipData.linkToShowPage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,18 +1,21 @@
|
|||||||
import { EntityChip } from 'twenty-ui';
|
import { EntityChip } from 'twenty-ui';
|
||||||
|
|
||||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
|
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
|
||||||
export const RelationFromManyFieldDisplay = ({
|
export const RelationFromManyFieldDisplay = () => {
|
||||||
fieldValue,
|
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
||||||
}: {
|
useRelationFromManyFieldDisplay();
|
||||||
fieldValue: ObjectRecord[];
|
|
||||||
}) => {
|
|
||||||
const { isFocused } = useFieldFocus();
|
const { isFocused } = useFieldFocus();
|
||||||
const { generateRecordChipData } = useRelationFieldDisplay();
|
|
||||||
|
if (
|
||||||
|
!fieldValue ||
|
||||||
|
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const recordChipsData = fieldValue.map((fieldValueItem) =>
|
const recordChipsData = fieldValue.map((fieldValueItem) =>
|
||||||
generateRecordChipData(fieldValueItem),
|
generateRecordChipData(fieldValueItem),
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import { EntityChip } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useRelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay';
|
||||||
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
|
||||||
|
export const RelationToOneFieldDisplay = () => {
|
||||||
|
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
||||||
|
useRelationToOneFieldDisplay();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!fieldValue ||
|
||||||
|
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recordChipData = generateRecordChipData(fieldValue);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntityChip
|
||||||
|
entityId={fieldValue.id}
|
||||||
|
name={recordChipData.name as any}
|
||||||
|
avatarType={recordChipData.avatarType}
|
||||||
|
avatarUrl={getImageAbsoluteURIOrBase64(recordChipData.avatarUrl) || ''}
|
||||||
|
linkToEntity={recordChipData.linkToShowPage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -9,7 +9,7 @@ import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinit
|
|||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import {
|
import {
|
||||||
RecordFieldValueSelectorContextProvider,
|
RecordFieldValueSelectorContextProvider,
|
||||||
useSetRecordValue,
|
useSetRecordFieldValue,
|
||||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||||
@ -18,6 +18,7 @@ import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
fieldValue,
|
fieldValue,
|
||||||
|
otherPersonMock,
|
||||||
relationFromManyFieldDisplayMock,
|
relationFromManyFieldDisplayMock,
|
||||||
} from './relationFromManyFieldDisplayMock';
|
} from './relationFromManyFieldDisplayMock';
|
||||||
|
|
||||||
@ -30,21 +31,26 @@ const RelationFieldValueSetterEffect = () => {
|
|||||||
recordStoreFamilyState(relationFromManyFieldDisplayMock.relationEntityId),
|
recordStoreFamilyState(relationFromManyFieldDisplayMock.relationEntityId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const setRecordValue = useSetRecordValue();
|
const setRecordFieldValue = useSetRecordFieldValue();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEntity(relationFromManyFieldDisplayMock.entityValue);
|
setEntity(relationFromManyFieldDisplayMock.entityValue);
|
||||||
setRelationEntity(relationFromManyFieldDisplayMock.relationFieldValue);
|
setRelationEntity(relationFromManyFieldDisplayMock.relationFieldValue);
|
||||||
|
|
||||||
setRecordValue(
|
setRecordFieldValue(
|
||||||
relationFromManyFieldDisplayMock.entityValue.id,
|
relationFromManyFieldDisplayMock.entityValue.id,
|
||||||
relationFromManyFieldDisplayMock.entityValue,
|
'company',
|
||||||
|
[relationFromManyFieldDisplayMock.entityValue],
|
||||||
);
|
);
|
||||||
setRecordValue(
|
setRecordFieldValue(otherPersonMock.entityValue.id, 'company', [
|
||||||
|
relationFromManyFieldDisplayMock.entityValue,
|
||||||
|
]);
|
||||||
|
setRecordFieldValue(
|
||||||
relationFromManyFieldDisplayMock.relationFieldValue.id,
|
relationFromManyFieldDisplayMock.relationFieldValue.id,
|
||||||
|
'company',
|
||||||
relationFromManyFieldDisplayMock.relationFieldValue,
|
relationFromManyFieldDisplayMock.relationFieldValue,
|
||||||
);
|
);
|
||||||
}, [setEntity, setRelationEntity, setRecordValue]);
|
}, [setEntity, setRelationEntity, setRecordFieldValue]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
import { RelationFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFieldDisplay';
|
import { RelationToOneFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationToOneFieldDisplay';
|
||||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||||
@ -15,7 +15,7 @@ const meta: Meta = {
|
|||||||
getFieldDecorator('person', 'company'),
|
getFieldDecorator('person', 'company'),
|
||||||
ComponentDecorator,
|
ComponentDecorator,
|
||||||
],
|
],
|
||||||
component: RelationFieldDisplay,
|
component: RelationToOneFieldDisplay,
|
||||||
args: {},
|
args: {},
|
||||||
parameters: {
|
parameters: {
|
||||||
chromatic: { disableSnapshot: true },
|
chromatic: { disableSnapshot: true },
|
||||||
@ -24,7 +24,7 @@ const meta: Meta = {
|
|||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof RelationFieldDisplay>;
|
type Story = StoryObj<typeof RelationToOneFieldDisplay>;
|
||||||
|
|
||||||
export const Default: Story = {};
|
export const Default: Story = {};
|
||||||
|
|
@ -59,6 +59,60 @@ export const fieldValue = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const otherPersonMock = {
|
||||||
|
entityValue: {
|
||||||
|
__typename: 'Person',
|
||||||
|
asd: '',
|
||||||
|
city: 'Paris',
|
||||||
|
jobTitle: '',
|
||||||
|
name: 'John Doe',
|
||||||
|
createdAt: '2024-05-01T13:16:29.046Z',
|
||||||
|
company: {
|
||||||
|
__typename: 'Company',
|
||||||
|
domainName: 'google.com',
|
||||||
|
xLink: {
|
||||||
|
__typename: 'Link',
|
||||||
|
label: '',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
name: 'Google',
|
||||||
|
annualRecurringRevenue: {
|
||||||
|
__typename: 'Currency',
|
||||||
|
amountMicros: null,
|
||||||
|
currencyCode: '',
|
||||||
|
},
|
||||||
|
employees: null,
|
||||||
|
accountOwnerId: null,
|
||||||
|
address: '',
|
||||||
|
idealCustomerProfile: false,
|
||||||
|
createdAt: '2024-05-01T13:16:29.046Z',
|
||||||
|
id: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||||
|
position: 6,
|
||||||
|
updatedAt: '2024-05-01T13:16:29.046Z',
|
||||||
|
linkedinLink: {
|
||||||
|
__typename: 'Link',
|
||||||
|
label: '',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: 'd3e70589-c449-4e64-8268-065640fdaff0',
|
||||||
|
email: 'john.doe@google.com',
|
||||||
|
phone: '+33744332211',
|
||||||
|
linkedinLink: {
|
||||||
|
__typename: 'Link',
|
||||||
|
label: '',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
xLink: {
|
||||||
|
__typename: 'Link',
|
||||||
|
label: '',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
tEst: '',
|
||||||
|
position: 14,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const relationFromManyFieldDisplayMock = {
|
export const relationFromManyFieldDisplayMock = {
|
||||||
entityId: '20202020-2d40-4e49-8df4-9c6a049191df',
|
entityId: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||||
relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025',
|
relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||||
@ -67,11 +121,7 @@ export const relationFromManyFieldDisplayMock = {
|
|||||||
asd: '',
|
asd: '',
|
||||||
city: 'Seattle',
|
city: 'Seattle',
|
||||||
jobTitle: '',
|
jobTitle: '',
|
||||||
name: {
|
name: 'Lorie Vladim',
|
||||||
__typename: 'FullName',
|
|
||||||
firstName: 'Lorie',
|
|
||||||
lastName: 'Vladim',
|
|
||||||
},
|
|
||||||
createdAt: '2024-05-01T13:16:29.046Z',
|
createdAt: '2024-05-01T13:16:29.046Z',
|
||||||
company: {
|
company: {
|
||||||
__typename: 'Company',
|
__typename: 'Company',
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext';
|
||||||
|
import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData';
|
||||||
|
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { FIELD_EDIT_BUTTON_WIDTH } from '@/ui/field/display/constants/FieldEditButtonWidth';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
|
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||||
|
|
||||||
|
export const useRelationFromManyFieldDisplay = () => {
|
||||||
|
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||||
|
|
||||||
|
const { chipGeneratorPerObjectPerField } = useContext(
|
||||||
|
PreComputedChipGeneratorsContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDefined(chipGeneratorPerObjectPerField)) {
|
||||||
|
throw new Error('Chip generator per object per field is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
assertFieldMetadata(
|
||||||
|
FieldMetadataType.Relation,
|
||||||
|
isFieldRelation,
|
||||||
|
fieldDefinition,
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = fieldDefinition.editButtonIcon;
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
|
const fieldValue = useRecordFieldValue<ObjectRecord[] | undefined>(
|
||||||
|
entityId,
|
||||||
|
fieldName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxWidthForField =
|
||||||
|
isDefined(button) && isDefined(maxWidth)
|
||||||
|
? maxWidth - FIELD_EDIT_BUTTON_WIDTH
|
||||||
|
: maxWidth;
|
||||||
|
|
||||||
|
if (!isNonEmptyString(fieldDefinition.metadata.objectMetadataNameSingular)) {
|
||||||
|
throw new Error('Object metadata name singular is not a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateRecordChipData =
|
||||||
|
chipGeneratorPerObjectPerField[
|
||||||
|
fieldDefinition.metadata.objectMetadataNameSingular
|
||||||
|
]?.[fieldDefinition.metadata.fieldName] ?? generateDefaultRecordChipData;
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldDefinition,
|
||||||
|
fieldValue,
|
||||||
|
maxWidth: maxWidthForField,
|
||||||
|
entityId,
|
||||||
|
generateRecordChipData,
|
||||||
|
};
|
||||||
|
};
|
@ -13,7 +13,7 @@ import { FieldContext } from '../../contexts/FieldContext';
|
|||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
import { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||||
|
|
||||||
export const useRelationFieldDisplay = () => {
|
export const useRelationToOneFieldDisplay = () => {
|
||||||
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||||
|
|
||||||
const { chipGeneratorPerObjectPerField } = useContext(
|
const { chipGeneratorPerObjectPerField } = useContext(
|
@ -0,0 +1,37 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||||
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
|
||||||
|
import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput';
|
||||||
|
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||||
|
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||||
|
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||||
|
|
||||||
|
export type RelationFromManyFieldInputProps = {
|
||||||
|
onSubmit?: FieldInputEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RelationFromManyFieldInput = ({
|
||||||
|
onSubmit,
|
||||||
|
}: RelationFromManyFieldInputProps) => {
|
||||||
|
const { fieldDefinition } = useContext(FieldContext);
|
||||||
|
const relationPickerScopeId = `relation-picker-${fieldDefinition.fieldMetadataId}`;
|
||||||
|
const { updateRelation } = useUpdateRelationFromManyFieldInput({
|
||||||
|
scopeId: relationPickerScopeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onSubmit?.(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||||
|
<ObjectMetadataItemsRelationPickerEffect />
|
||||||
|
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||||
|
<MultiRecordSelect onSubmit={handleSubmit} onChange={updateRelation} />
|
||||||
|
</RelationPickerScope>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,124 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
|
||||||
|
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
|
||||||
|
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
|
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
|
||||||
|
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
||||||
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
|
export const RelationFromManyFieldInputMultiRecordsEffect = () => {
|
||||||
|
const { fieldValue, fieldDefinition } = useRelationField<EntityForSelect[]>();
|
||||||
|
const scopeId = useAvailableScopeIdOrThrow(
|
||||||
|
RelationPickerScopeInternalContext,
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
objectRecordsIdsMultiSelectState,
|
||||||
|
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||||
|
recordMultiSelectIsLoadingState,
|
||||||
|
} = useObjectRecordMultiSelectScopedStates(scopeId);
|
||||||
|
const [objectRecordsIdsMultiSelect, setObjectRecordsIdsMultiSelect] =
|
||||||
|
useRecoilState(objectRecordsIdsMultiSelectState);
|
||||||
|
|
||||||
|
const { entities } = useRelationPickerEntitiesOptions({
|
||||||
|
relationObjectNameSingular:
|
||||||
|
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setRecordMultiSelectIsLoading = useSetRecoilState(
|
||||||
|
recordMultiSelectIsLoadingState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular:
|
||||||
|
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allRecords = useMemo(
|
||||||
|
() => [
|
||||||
|
...entities.entitiesToSelect.map((entity) => {
|
||||||
|
const { record, ...recordIdentifier } = entity;
|
||||||
|
return {
|
||||||
|
objectMetadataItem: objectMetadataItem,
|
||||||
|
record: record,
|
||||||
|
recordIdentifier: recordIdentifier,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[entities.entitiesToSelect, objectMetadataItem],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
objectRecordMultiSelectCheckedRecordsIds,
|
||||||
|
setObjectRecordMultiSelectCheckedRecordsIds,
|
||||||
|
] = useRecoilState(objectRecordMultiSelectCheckedRecordsIdsState);
|
||||||
|
|
||||||
|
const updateRecords = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
(newRecords: ObjectRecordForSelect[]) => {
|
||||||
|
for (const newRecord of newRecords) {
|
||||||
|
const currentRecord = snapshot
|
||||||
|
.getLoadable(
|
||||||
|
objectRecordMultiSelectComponentFamilyState({
|
||||||
|
scopeId: scopeId,
|
||||||
|
familyKey: newRecord.record.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const newRecordWithSelected = {
|
||||||
|
...newRecord,
|
||||||
|
selected: objectRecordMultiSelectCheckedRecordsIds.includes(
|
||||||
|
newRecord.record.id,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDeeplyEqual(
|
||||||
|
newRecordWithSelected.selected,
|
||||||
|
currentRecord?.selected,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
set(
|
||||||
|
objectRecordMultiSelectComponentFamilyState({
|
||||||
|
scopeId: scopeId,
|
||||||
|
familyKey: newRecordWithSelected.record.id,
|
||||||
|
}),
|
||||||
|
newRecordWithSelected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[objectRecordMultiSelectCheckedRecordsIds, scopeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateRecords(allRecords);
|
||||||
|
const allRecordsIds = allRecords.map((record) => record.record.id);
|
||||||
|
if (!isDeeplyEqual(allRecordsIds, objectRecordsIdsMultiSelect)) {
|
||||||
|
setObjectRecordsIdsMultiSelect(allRecordsIds);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
allRecords,
|
||||||
|
objectRecordsIdsMultiSelect,
|
||||||
|
setObjectRecordsIdsMultiSelect,
|
||||||
|
updateRecords,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setObjectRecordMultiSelectCheckedRecordsIds(
|
||||||
|
fieldValue.map((fieldValueItem: EntityForSelect) => fieldValueItem.id),
|
||||||
|
);
|
||||||
|
}, [fieldValue, setObjectRecordMultiSelectCheckedRecordsIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRecordMultiSelectIsLoading(entities.loading);
|
||||||
|
}, [entities.loading, setRecordMultiSelectIsLoading]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
@ -1,82 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { useUpdateRelationManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput';
|
|
||||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
|
||||||
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
|
||||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
|
||||||
import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions';
|
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
|
||||||
|
|
||||||
import { useRelationField } from '../../hooks/useRelationField';
|
|
||||||
|
|
||||||
export const RelationManyFieldInput = ({
|
|
||||||
relationPickerScopeId = 'relation-picker',
|
|
||||||
}: {
|
|
||||||
relationPickerScopeId?: string;
|
|
||||||
}) => {
|
|
||||||
const { closeInlineCell: closeEditableField } = useInlineCell();
|
|
||||||
|
|
||||||
const { fieldDefinition, fieldValue } = useRelationField<EntityForSelect[]>();
|
|
||||||
const { entities, relationPickerSearchFilter } =
|
|
||||||
useRelationPickerEntitiesOptions({
|
|
||||||
relationObjectNameSingular:
|
|
||||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
|
||||||
relationPickerScopeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { setRelationPickerSearchFilter } = useRelationPicker({
|
|
||||||
relationPickerScopeId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleChange } = useUpdateRelationManyFieldInput({ entities });
|
|
||||||
|
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
|
||||||
objectNameSingular:
|
|
||||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
|
||||||
});
|
|
||||||
const allRecords = useMemo(
|
|
||||||
() => [
|
|
||||||
...entities.entitiesToSelect.map((entity) => {
|
|
||||||
const { record, ...recordIdentifier } = entity;
|
|
||||||
return {
|
|
||||||
objectMetadataItem: objectMetadataItem,
|
|
||||||
record: record,
|
|
||||||
recordIdentifier: recordIdentifier,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[entities.entitiesToSelect, objectMetadataItem],
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedRecords = useMemo(
|
|
||||||
() =>
|
|
||||||
allRecords.filter(
|
|
||||||
(entity) =>
|
|
||||||
fieldValue?.some((f) => {
|
|
||||||
return f.id === entity.recordIdentifier.id;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
[allRecords, fieldValue],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ObjectMetadataItemsRelationPickerEffect
|
|
||||||
relationPickerScopeId={relationPickerScopeId}
|
|
||||||
/>
|
|
||||||
<MultiRecordSelect
|
|
||||||
allRecords={allRecords}
|
|
||||||
selectedObjectRecords={selectedRecords}
|
|
||||||
loading={entities.loading}
|
|
||||||
searchFilter={relationPickerSearchFilter}
|
|
||||||
setSearchFilter={setRelationPickerSearchFilter}
|
|
||||||
onSubmit={() => {
|
|
||||||
closeEditableField();
|
|
||||||
}}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -14,15 +14,15 @@ const StyledRelationPickerContainer = styled.div`
|
|||||||
top: -1px;
|
top: -1px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type RelationFieldInputProps = {
|
export type RelationToOneFieldInputProps = {
|
||||||
onSubmit?: FieldInputEvent;
|
onSubmit?: FieldInputEvent;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RelationFieldInput = ({
|
export const RelationToOneFieldInput = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: RelationFieldInputProps) => {
|
}: RelationToOneFieldInputProps) => {
|
||||||
const { fieldDefinition, initialSearchValue, fieldValue } =
|
const { fieldDefinition, initialSearchValue, fieldValue } =
|
||||||
useRelationField<EntityForSelect>();
|
useRelationField<EntityForSelect>();
|
||||||
|
|
@ -5,7 +5,7 @@ import { useSetRecoilState } from 'recoil';
|
|||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput';
|
import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||||
@ -60,7 +60,7 @@ const RelationManyFieldInputWithContext = () => {
|
|||||||
entityId={'entityId'}
|
entityId={'entityId'}
|
||||||
>
|
>
|
||||||
<RelationWorkspaceSetterEffect />
|
<RelationWorkspaceSetterEffect />
|
||||||
<RelationManyFieldInput />
|
<RelationFromManyFieldInput />
|
||||||
</FieldContextProvider>
|
</FieldContextProvider>
|
||||||
<div data-testid="data-field-input-click-outside-div" />
|
<div data-testid="data-field-input-click-outside-div" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@ import { useSetRecoilState } from 'recoil';
|
|||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||||
@ -26,9 +27,9 @@ import {
|
|||||||
|
|
||||||
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||||
import {
|
import {
|
||||||
RelationFieldInput,
|
RelationToOneFieldInput,
|
||||||
RelationFieldInputProps,
|
RelationToOneFieldInputProps,
|
||||||
} from '../RelationFieldInput';
|
} from '../RelationToOneFieldInput';
|
||||||
|
|
||||||
const RelationWorkspaceSetterEffect = () => {
|
const RelationWorkspaceSetterEffect = () => {
|
||||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||||
@ -44,16 +45,16 @@ const RelationWorkspaceSetterEffect = () => {
|
|||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RelationFieldInputWithContextProps = RelationFieldInputProps & {
|
type RelationToOneFieldInputWithContextProps = RelationToOneFieldInputProps & {
|
||||||
value: number;
|
value: number;
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RelationFieldInputWithContext = ({
|
const RelationToOneFieldInputWithContext = ({
|
||||||
entityId,
|
entityId,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
}: RelationFieldInputWithContextProps) => {
|
}: RelationToOneFieldInputWithContextProps) => {
|
||||||
const setHotKeyScope = useSetHotkeyScope();
|
const setHotKeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -79,8 +80,12 @@ const RelationFieldInputWithContext = ({
|
|||||||
}}
|
}}
|
||||||
entityId={entityId}
|
entityId={entityId}
|
||||||
>
|
>
|
||||||
<RelationWorkspaceSetterEffect />
|
<RelationPickerScope
|
||||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
relationPickerScopeId={'relation-to-one-field-input'}
|
||||||
|
>
|
||||||
|
<RelationWorkspaceSetterEffect />
|
||||||
|
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||||
|
</RelationPickerScope>
|
||||||
</FieldContextProvider>
|
</FieldContextProvider>
|
||||||
<div data-testid="data-field-input-click-outside-div" />
|
<div data-testid="data-field-input-click-outside-div" />
|
||||||
</div>
|
</div>
|
||||||
@ -99,8 +104,8 @@ const clearMocksDecorator: Decorator = (Story, context) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'UI/Data/Field/Input/RelationFieldInput',
|
title: 'UI/Data/Field/Input/RelationToOneFieldInput',
|
||||||
component: RelationFieldInputWithContext,
|
component: RelationToOneFieldInputWithContext,
|
||||||
args: {
|
args: {
|
||||||
useEditButton: true,
|
useEditButton: true,
|
||||||
onSubmit: submitJestFn,
|
onSubmit: submitJestFn,
|
||||||
@ -123,7 +128,7 @@ const meta: Meta = {
|
|||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof RelationFieldInputWithContext>;
|
type Story = StoryObj<typeof RelationToOneFieldInputWithContext>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
decorators: [ComponentWithRecoilScopeDecorator],
|
decorators: [ComponentWithRecoilScopeDecorator],
|
@ -0,0 +1,93 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
|
import { useAttachRelatedRecordFromRecord } from '@/object-record/hooks/useAttachRelatedRecordFromRecord';
|
||||||
|
import { useDetachRelatedRecordFromRecord } from '@/object-record/hooks/useDetachRelatedRecordFromRecord';
|
||||||
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
||||||
|
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||||
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useUpdateRelationFromManyFieldInput = ({
|
||||||
|
scopeId,
|
||||||
|
}: {
|
||||||
|
scopeId: string;
|
||||||
|
}) => {
|
||||||
|
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
|
assertFieldMetadata(
|
||||||
|
FieldMetadataType.Relation,
|
||||||
|
isFieldRelation,
|
||||||
|
fieldDefinition,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fieldDefinition.metadata.objectMetadataNameSingular) {
|
||||||
|
throw new Error('ObjectMetadataNameSingular is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateOneRecordAndDetachRelations } =
|
||||||
|
useDetachRelatedRecordFromRecord({
|
||||||
|
recordObjectNameSingular:
|
||||||
|
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||||
|
fieldNameOnRecordObject: fieldDefinition.metadata.fieldName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { updateOneRecordAndAttachRelations } =
|
||||||
|
useAttachRelatedRecordFromRecord({
|
||||||
|
recordObjectNameSingular:
|
||||||
|
fieldDefinition.metadata.objectMetadataNameSingular,
|
||||||
|
fieldNameOnRecordObject: fieldDefinition.metadata.fieldName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateRelation = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
async (objectRecordId: string) => {
|
||||||
|
const previouslyCheckedRecordsIds = snapshot
|
||||||
|
.getLoadable(
|
||||||
|
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||||
|
scopeId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const isNewlySelected =
|
||||||
|
!previouslyCheckedRecordsIds.includes(objectRecordId);
|
||||||
|
if (isNewlySelected) {
|
||||||
|
set(
|
||||||
|
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||||
|
scopeId,
|
||||||
|
}),
|
||||||
|
(prev) => [...prev, objectRecordId],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
set(
|
||||||
|
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||||
|
scopeId,
|
||||||
|
}),
|
||||||
|
(prev) => prev.filter((id) => id !== objectRecordId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNewlySelected) {
|
||||||
|
await updateOneRecordAndAttachRelations({
|
||||||
|
recordId: entityId,
|
||||||
|
relatedRecordId: objectRecordId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await updateOneRecordAndDetachRelations({
|
||||||
|
recordId: entityId,
|
||||||
|
relatedRecordId: objectRecordId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
entityId,
|
||||||
|
scopeId,
|
||||||
|
updateOneRecordAndAttachRelations,
|
||||||
|
updateOneRecordAndDetachRelations,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { updateRelation };
|
||||||
|
};
|
@ -1,52 +0,0 @@
|
|||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
|
||||||
import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField';
|
|
||||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
|
||||||
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
|
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
|
||||||
import { isDefined } from '~/utils/isDefined';
|
|
||||||
|
|
||||||
export const useUpdateRelationManyFieldInput = ({
|
|
||||||
entities,
|
|
||||||
}: {
|
|
||||||
entities: EntitiesForMultipleEntitySelect<EntityForSelect>;
|
|
||||||
}) => {
|
|
||||||
const { fieldDefinition, fieldValue, setFieldValue, entityId } =
|
|
||||||
useRelationField<EntityForSelect[]>();
|
|
||||||
|
|
||||||
const { updateOneRecord } = useUpdateOneRecord({
|
|
||||||
objectNameSingular:
|
|
||||||
fieldDefinition.metadata.relationObjectMetadataNameSingular,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fieldName = fieldDefinition.metadata.targetFieldMetadataName;
|
|
||||||
|
|
||||||
const handleChange = (
|
|
||||||
objectRecord: ObjectRecordForSelect | null,
|
|
||||||
isSelected: boolean,
|
|
||||||
) => {
|
|
||||||
const entityToAddOrRemove = entities.entitiesToSelect.find(
|
|
||||||
(entity) => entity.id === objectRecord?.recordIdentifier.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedFieldValue = isSelected
|
|
||||||
? [...(fieldValue ?? []), entityToAddOrRemove]
|
|
||||||
: (fieldValue ?? []).filter(
|
|
||||||
(value) => value.id !== objectRecord?.recordIdentifier.id,
|
|
||||||
);
|
|
||||||
setFieldValue(
|
|
||||||
updatedFieldValue.filter((value) =>
|
|
||||||
isDefined(value),
|
|
||||||
) as EntityForSelect[],
|
|
||||||
);
|
|
||||||
if (isDefined(objectRecord)) {
|
|
||||||
updateOneRecord({
|
|
||||||
idToUpdate: objectRecord.record?.id,
|
|
||||||
updateOneRecordInput: {
|
|
||||||
[`${fieldName}Id`]: isSelected ? entityId : null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return { handleChange };
|
|
||||||
};
|
|
@ -0,0 +1,13 @@
|
|||||||
|
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||||
|
|
||||||
|
export type ActivityTargetObjectRecord = {
|
||||||
|
activityTargetId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const activityTargetObjectRecordFamilyState = createFamilyState<
|
||||||
|
ActivityTargetObjectRecord,
|
||||||
|
string
|
||||||
|
>({
|
||||||
|
key: 'activityTargetObjectRecordFamilyState',
|
||||||
|
defaultValue: { activityTargetId: null },
|
||||||
|
});
|
@ -0,0 +1,7 @@
|
|||||||
|
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||||
|
|
||||||
|
export const objectRecordMultiSelectCheckedRecordsIdsComponentState =
|
||||||
|
createComponentState<string[]>({
|
||||||
|
key: 'objectRecordMultiSelectCheckedRecordsIdsComponentState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
@ -0,0 +1,12 @@
|
|||||||
|
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
|
import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState';
|
||||||
|
|
||||||
|
export type ObjectRecordAndSelected = ObjectRecordForSelect & {
|
||||||
|
selected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const objectRecordMultiSelectComponentFamilyState =
|
||||||
|
createComponentFamilyState<ObjectRecordAndSelected | undefined, string>({
|
||||||
|
key: 'objectRecordMultiSelectComponentFamilyState',
|
||||||
|
defaultValue: undefined,
|
||||||
|
});
|
@ -0,0 +1,7 @@
|
|||||||
|
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||||
|
|
||||||
|
export const recordMultiSelectIsLoadingComponentState =
|
||||||
|
createComponentState<boolean>({
|
||||||
|
key: 'recordMultiSelectIsLoadingComponentState',
|
||||||
|
defaultValue: false,
|
||||||
|
});
|
@ -13,12 +13,12 @@ import {
|
|||||||
FieldNumberValue,
|
FieldNumberValue,
|
||||||
FieldPhoneValue,
|
FieldPhoneValue,
|
||||||
FieldRatingValue,
|
FieldRatingValue,
|
||||||
FieldRelationValue,
|
FieldRelationFromManyValue,
|
||||||
|
FieldRelationToOneValue,
|
||||||
FieldSelectValue,
|
FieldSelectValue,
|
||||||
FieldTextValue,
|
FieldTextValue,
|
||||||
FieldUUidValue,
|
FieldUUidValue,
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
|
||||||
|
|
||||||
export type FieldTextDraftValue = string;
|
export type FieldTextDraftValue = string;
|
||||||
export type FieldNumberDraftValue = string;
|
export type FieldNumberDraftValue = string;
|
||||||
@ -28,6 +28,7 @@ export type FieldEmailDraftValue = string;
|
|||||||
export type FieldSelectDraftValue = string;
|
export type FieldSelectDraftValue = string;
|
||||||
export type FieldMultiSelectDraftValue = string[];
|
export type FieldMultiSelectDraftValue = string[];
|
||||||
export type FieldRelationDraftValue = string;
|
export type FieldRelationDraftValue = string;
|
||||||
|
export type FieldRelationManyDraftValue = string[];
|
||||||
export type FieldLinkDraftValue = { url: string; label: string };
|
export type FieldLinkDraftValue = { url: string; label: string };
|
||||||
export type FieldLinksDraftValue = {
|
export type FieldLinksDraftValue = {
|
||||||
primaryLinkLabel: string;
|
primaryLinkLabel: string;
|
||||||
@ -79,12 +80,12 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
|||||||
? FieldSelectDraftValue
|
? FieldSelectDraftValue
|
||||||
: FieldValue extends FieldMultiSelectValue
|
: FieldValue extends FieldMultiSelectValue
|
||||||
? FieldMultiSelectDraftValue
|
? FieldMultiSelectDraftValue
|
||||||
: FieldValue extends
|
: FieldValue extends FieldRelationToOneValue
|
||||||
| FieldRelationValue<EntityForSelect>
|
|
||||||
| FieldRelationValue<EntityForSelect[]>
|
|
||||||
? FieldRelationDraftValue
|
? FieldRelationDraftValue
|
||||||
: FieldValue extends FieldAddressValue
|
: FieldValue extends FieldRelationFromManyValue
|
||||||
? FieldAddressDraftValue
|
? FieldRelationManyDraftValue
|
||||||
: FieldValue extends FieldJsonValue
|
: FieldValue extends FieldAddressValue
|
||||||
? FieldJsonDraftValue
|
? FieldAddressDraftValue
|
||||||
: never;
|
: FieldValue extends FieldJsonValue
|
||||||
|
? FieldJsonDraftValue
|
||||||
|
: never;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ThemeColor } from 'twenty-ui';
|
import { ThemeColor } from 'twenty-ui';
|
||||||
|
|
||||||
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
import { RATING_VALUES } from '@/object-record/record-field/meta-types/constants/RatingValues';
|
||||||
|
import { ZodHelperLiteral } from '@/object-record/record-field/types/ZodHelperLiteral';
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
import { WithNarrowedStringLiteralProperty } from '~/types/WithNarrowedStringLiteralProperty';
|
||||||
|
|
||||||
import { CurrencyCode } from './CurrencyCode';
|
import { CurrencyCode } from './CurrencyCode';
|
||||||
|
|
||||||
@ -110,6 +112,18 @@ export type FieldRelationMetadata = {
|
|||||||
useEditButton?: boolean;
|
useEditButton?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldRelationOneMetadata = WithNarrowedStringLiteralProperty<
|
||||||
|
FieldRelationMetadata,
|
||||||
|
'relationType',
|
||||||
|
'TO_ONE_OBJECT'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type FieldRelationManyMetadata = WithNarrowedStringLiteralProperty<
|
||||||
|
FieldRelationMetadata,
|
||||||
|
'relationType',
|
||||||
|
'FROM_MANY_OBJECTS'
|
||||||
|
>;
|
||||||
|
|
||||||
export type FieldSelectMetadata = {
|
export type FieldSelectMetadata = {
|
||||||
objectMetadataNameSingular?: string;
|
objectMetadataNameSingular?: string;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@ -174,10 +188,13 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number];
|
|||||||
export type FieldSelectValue = string | null;
|
export type FieldSelectValue = string | null;
|
||||||
export type FieldMultiSelectValue = string[] | null;
|
export type FieldMultiSelectValue = string[] | null;
|
||||||
|
|
||||||
export type FieldRelationValue<T extends EntityForSelect | EntityForSelect[]> =
|
export type FieldRelationToOneValue = EntityForSelect | null;
|
||||||
T | null;
|
|
||||||
|
|
||||||
// See https://zod.dev/?id=json-type
|
export type FieldRelationFromManyValue = EntityForSelect[] | [];
|
||||||
type Literal = string | number | boolean | null;
|
|
||||||
export type Json = Literal | { [key: string]: Json } | Json[];
|
export type FieldRelationValue<
|
||||||
|
T extends FieldRelationToOneValue | FieldRelationFromManyValue,
|
||||||
|
> = T;
|
||||||
|
|
||||||
|
export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[];
|
||||||
export type FieldJsonValue = Record<string, Json> | Json[] | null;
|
export type FieldJsonValue = Record<string, Json> | Json[] | null;
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
/** See https://zod.dev/?id=json-type */
|
||||||
|
export type ZodHelperLiteral = string | number | boolean | null;
|
@ -1,10 +1,9 @@
|
|||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
|
|
||||||
import { FieldDefinition } from '../FieldDefinition';
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
import { FieldRelationMetadata } from '../FieldMetadata';
|
import { FieldMetadata, FieldRelationManyMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
export const isFieldRelationFromManyObjects = (
|
export const isFieldRelationFromManyObjects = (
|
||||||
field: Pick<FieldDefinition<FieldRelationMetadata>, 'type' | 'metadata'>,
|
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||||
): field is FieldDefinition<FieldRelationMetadata> =>
|
): field is FieldDefinition<FieldRelationManyMetadata> =>
|
||||||
field.type === FieldMetadataType.Relation &&
|
isFieldRelation(field) && field.metadata.relationType === 'FROM_MANY_OBJECTS';
|
||||||
field.metadata.relationType === 'FROM_MANY_OBJECTS';
|
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { FieldRelationFromManyValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
|
||||||
|
// TODO: add zod
|
||||||
|
export const isFieldRelationFromManyValue = (
|
||||||
|
fieldValue: unknown,
|
||||||
|
): fieldValue is FieldRelationFromManyValue =>
|
||||||
|
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
|
@ -0,0 +1,9 @@
|
|||||||
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
|
|
||||||
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
|
import { FieldMetadata, FieldRelationOneMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
|
export const isFieldRelationToOneObject = (
|
||||||
|
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||||
|
): field is FieldDefinition<FieldRelationOneMetadata> =>
|
||||||
|
isFieldRelation(field) && field.metadata.relationType === 'TO_ONE_OBJECT';
|
@ -0,0 +1,9 @@
|
|||||||
|
import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { FieldRelationToOneValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
|
||||||
|
// TODO: add zod
|
||||||
|
export const isFieldRelationToOneValue = (
|
||||||
|
fieldValue: unknown,
|
||||||
|
): fieldValue is FieldRelationToOneValue =>
|
||||||
|
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
|
@ -1,13 +0,0 @@
|
|||||||
import { isNull, isObject, isUndefined } from '@sniptt/guards';
|
|
||||||
|
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
|
||||||
|
|
||||||
import { FieldRelationValue } from '../FieldMetadata';
|
|
||||||
|
|
||||||
// TODO: add zod
|
|
||||||
export const isFieldRelationValue = <
|
|
||||||
T extends EntityForSelect | EntityForSelect[],
|
|
||||||
>(
|
|
||||||
fieldValue: unknown,
|
|
||||||
): fieldValue is FieldRelationValue<T> =>
|
|
||||||
!isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue));
|
|
@ -11,7 +11,7 @@ import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/s
|
|||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||||
|
|
||||||
type UseLoadRecordIndexBoardProps = {
|
type UseLoadRecordIndexBoardProps = {
|
||||||
@ -33,7 +33,7 @@ export const useLoadRecordIndexBoard = ({
|
|||||||
setFieldDefinitions,
|
setFieldDefinitions,
|
||||||
isCompactModeActiveState,
|
isCompactModeActiveState,
|
||||||
} = useRecordBoard(recordBoardId);
|
} = useRecordBoard(recordBoardId);
|
||||||
const { setRecords: setRecordsInStore } = useSetRecordInStore();
|
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
const recordIndexFieldDefinitions = useRecoilValue(
|
const recordIndexFieldDefinitions = useRecoilValue(
|
||||||
recordIndexFieldDefinitionsState,
|
recordIndexFieldDefinitionsState,
|
||||||
@ -82,8 +82,8 @@ export const useLoadRecordIndexBoard = ({
|
|||||||
}, [records, setRecordIdsInBoard]);
|
}, [records, setRecordIdsInBoard]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecordsInStore(records);
|
upsertRecordsInStore(records);
|
||||||
}, [records, setRecordsInStore]);
|
}, [records, upsertRecordsInStore]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecordCountInCurrentView(totalCount);
|
setRecordCountInCurrentView(totalCount);
|
||||||
|
@ -9,7 +9,7 @@ import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-
|
|||||||
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||||
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
|
|
||||||
type UseLoadRecordIndexBoardProps = {
|
type UseLoadRecordIndexBoardProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
@ -30,7 +30,7 @@ export const useLoadRecordIndexBoardColumn = ({
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
const { setRecordIdsForColumn } = useRecordBoard(recordBoardId);
|
const { setRecordIdsForColumn } = useRecordBoard(recordBoardId);
|
||||||
const { setRecords: setRecordsInStore } = useSetRecordInStore();
|
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||||
@ -75,8 +75,8 @@ export const useLoadRecordIndexBoardColumn = ({
|
|||||||
}, [records, setRecordIdsForColumn, columnId]);
|
}, [records, setRecordIdsForColumn, columnId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecordsInStore(records);
|
upsertRecordsInStore(records);
|
||||||
}, [records, setRecordsInStore]);
|
}, [records, upsertRecordsInStore]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
records,
|
records,
|
||||||
|
@ -6,7 +6,6 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
|||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||||
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButtonIcon';
|
||||||
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
|
||||||
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
import { useIsFieldInputOnly } from '@/object-record/record-field/hooks/useIsFieldInputOnly';
|
||||||
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
@ -26,11 +25,8 @@ export const RecordInlineCell = ({
|
|||||||
loading,
|
loading,
|
||||||
}: RecordInlineCellProps) => {
|
}: RecordInlineCellProps) => {
|
||||||
const { fieldDefinition, entityId } = useContext(FieldContext);
|
const { fieldDefinition, entityId } = useContext(FieldContext);
|
||||||
|
|
||||||
const buttonIcon = useGetButtonIcon();
|
const buttonIcon = useGetButtonIcon();
|
||||||
|
|
||||||
const isFieldEmpty = useIsFieldEmpty();
|
|
||||||
|
|
||||||
const isFieldInputOnly = useIsFieldInputOnly();
|
const isFieldInputOnly = useIsFieldInputOnly();
|
||||||
|
|
||||||
const { closeInlineCell } = useInlineCell();
|
const { closeInlineCell } = useInlineCell();
|
||||||
@ -104,7 +100,6 @@ export const RecordInlineCell = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
displayModeContent={<FieldDisplay />}
|
displayModeContent={<FieldDisplay />}
|
||||||
isDisplayModeContentEmpty={isFieldEmpty}
|
|
||||||
isDisplayModeFixHeight
|
isDisplayModeFixHeight
|
||||||
editModeContentOnly={isFieldInputOnly}
|
editModeContentOnly={isFieldInputOnly}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
@ -69,7 +69,6 @@ export type RecordInlineCellContainerProps = {
|
|||||||
editModeContentOnly?: boolean;
|
editModeContentOnly?: boolean;
|
||||||
displayModeContent: ReactElement;
|
displayModeContent: ReactElement;
|
||||||
customEditHotkeyScope?: HotkeyScope;
|
customEditHotkeyScope?: HotkeyScope;
|
||||||
isDisplayModeContentEmpty?: boolean;
|
|
||||||
isDisplayModeFixHeight?: boolean;
|
isDisplayModeFixHeight?: boolean;
|
||||||
disableHoverEffect?: boolean;
|
disableHoverEffect?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@ -85,7 +84,6 @@ export const RecordInlineCellContainer = ({
|
|||||||
editModeContent,
|
editModeContent,
|
||||||
displayModeContent,
|
displayModeContent,
|
||||||
customEditHotkeyScope,
|
customEditHotkeyScope,
|
||||||
isDisplayModeContentEmpty,
|
|
||||||
editModeContentOnly,
|
editModeContentOnly,
|
||||||
isDisplayModeFixHeight,
|
isDisplayModeFixHeight,
|
||||||
disableHoverEffect,
|
disableHoverEffect,
|
||||||
@ -149,7 +147,6 @@ export const RecordInlineCellContainer = ({
|
|||||||
disableHoverEffect,
|
disableHoverEffect,
|
||||||
editModeContent,
|
editModeContent,
|
||||||
editModeContentOnly,
|
editModeContentOnly,
|
||||||
isDisplayModeContentEmpty,
|
|
||||||
isDisplayModeFixHeight,
|
isDisplayModeFixHeight,
|
||||||
buttonIcon,
|
buttonIcon,
|
||||||
label,
|
label,
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
|
import { useIsFieldEmpty } from '@/object-record/record-field/hooks/useIsFieldEmpty';
|
||||||
|
import { RecordInlineCellContainerProps } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||||
|
import { RecordInlineCellButton } from '@/object-record/record-inline-cell/components/RecordInlineCellEditButton';
|
||||||
|
|
||||||
const StyledRecordInlineCellNormalModeOuterContainer = styled.div<
|
const StyledRecordInlineCellNormalModeOuterContainer = styled.div<
|
||||||
Pick<
|
Pick<
|
||||||
RecordInlineCellDisplayModeProps,
|
RecordInlineCellDisplayModeProps,
|
||||||
| 'isDisplayModeContentEmpty'
|
'disableHoverEffect' | 'isDisplayModeFixHeight' | 'isHovered'
|
||||||
| 'disableHoverEffect'
|
|
||||||
| 'isDisplayModeFixHeight'
|
|
||||||
| 'isHovered'
|
|
||||||
>
|
>
|
||||||
>`
|
>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -51,33 +53,45 @@ const StyledEmptyField = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type RecordInlineCellDisplayModeProps = {
|
type RecordInlineCellDisplayModeProps = {
|
||||||
isDisplayModeContentEmpty?: boolean;
|
|
||||||
disableHoverEffect?: boolean;
|
disableHoverEffect?: boolean;
|
||||||
isDisplayModeFixHeight?: boolean;
|
isDisplayModeFixHeight?: boolean;
|
||||||
isHovered?: boolean;
|
isHovered?: boolean;
|
||||||
emptyPlaceholder?: string;
|
emptyPlaceholder?: string;
|
||||||
};
|
} & Pick<RecordInlineCellContainerProps, 'buttonIcon' | 'editModeContentOnly'>;
|
||||||
|
|
||||||
export const RecordInlineCellDisplayMode = ({
|
export const RecordInlineCellDisplayMode = ({
|
||||||
children,
|
children,
|
||||||
isDisplayModeContentEmpty,
|
|
||||||
disableHoverEffect,
|
disableHoverEffect,
|
||||||
isDisplayModeFixHeight,
|
isDisplayModeFixHeight,
|
||||||
emptyPlaceholder = 'Empty',
|
emptyPlaceholder = 'Empty',
|
||||||
isHovered,
|
isHovered,
|
||||||
}: React.PropsWithChildren<RecordInlineCellDisplayModeProps>) => (
|
buttonIcon,
|
||||||
<StyledRecordInlineCellNormalModeOuterContainer
|
editModeContentOnly,
|
||||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
}: React.PropsWithChildren<RecordInlineCellDisplayModeProps>) => {
|
||||||
disableHoverEffect={disableHoverEffect}
|
const { isFocused } = useFieldFocus();
|
||||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
const isDisplayModeContentEmpty = useIsFieldEmpty();
|
||||||
isHovered={isHovered}
|
const showEditButton =
|
||||||
>
|
buttonIcon &&
|
||||||
<StyledRecordInlineCellNormalModeInnerContainer>
|
isFocused &&
|
||||||
{isDisplayModeContentEmpty || !children ? (
|
!isDisplayModeContentEmpty &&
|
||||||
<StyledEmptyField>{emptyPlaceholder}</StyledEmptyField>
|
!editModeContentOnly;
|
||||||
) : (
|
|
||||||
children
|
return (
|
||||||
)}
|
<>
|
||||||
</StyledRecordInlineCellNormalModeInnerContainer>
|
<StyledRecordInlineCellNormalModeOuterContainer
|
||||||
</StyledRecordInlineCellNormalModeOuterContainer>
|
disableHoverEffect={disableHoverEffect}
|
||||||
);
|
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||||
|
isHovered={isHovered}
|
||||||
|
>
|
||||||
|
<StyledRecordInlineCellNormalModeInnerContainer>
|
||||||
|
{isDisplayModeContentEmpty || !children ? (
|
||||||
|
<StyledEmptyField>{emptyPlaceholder}</StyledEmptyField>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</StyledRecordInlineCellNormalModeInnerContainer>
|
||||||
|
</StyledRecordInlineCellNormalModeOuterContainer>
|
||||||
|
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -4,7 +4,6 @@ import styled from '@emotion/styled';
|
|||||||
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||||
import { RecordInlineCellContainerProps } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
import { RecordInlineCellContainerProps } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||||
import { RecordInlineCellDisplayMode } from '@/object-record/record-inline-cell/components/RecordInlineCellDisplayMode';
|
import { RecordInlineCellDisplayMode } from '@/object-record/record-inline-cell/components/RecordInlineCellDisplayMode';
|
||||||
import { RecordInlineCellButton } from '@/object-record/record-inline-cell/components/RecordInlineCellEditButton';
|
|
||||||
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
|
||||||
import { RecordInlineCellSkeletonLoader } from '@/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader';
|
import { RecordInlineCellSkeletonLoader } from '@/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader';
|
||||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||||
@ -27,7 +26,6 @@ type RecordInlineCellValueProps = Pick<
|
|||||||
| 'customEditHotkeyScope'
|
| 'customEditHotkeyScope'
|
||||||
| 'editModeContent'
|
| 'editModeContent'
|
||||||
| 'editModeContentOnly'
|
| 'editModeContentOnly'
|
||||||
| 'isDisplayModeContentEmpty'
|
|
||||||
| 'isDisplayModeFixHeight'
|
| 'isDisplayModeFixHeight'
|
||||||
| 'disableHoverEffect'
|
| 'disableHoverEffect'
|
||||||
| 'readonly'
|
| 'readonly'
|
||||||
@ -43,7 +41,6 @@ export const RecordInlineCellValue = ({
|
|||||||
disableHoverEffect,
|
disableHoverEffect,
|
||||||
editModeContent,
|
editModeContent,
|
||||||
editModeContentOnly,
|
editModeContentOnly,
|
||||||
isDisplayModeContentEmpty,
|
|
||||||
isDisplayModeFixHeight,
|
isDisplayModeFixHeight,
|
||||||
readonly,
|
readonly,
|
||||||
buttonIcon,
|
buttonIcon,
|
||||||
@ -61,13 +58,6 @@ export const RecordInlineCellValue = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditButton =
|
|
||||||
buttonIcon &&
|
|
||||||
!isInlineCellInEditMode &&
|
|
||||||
isFocused &&
|
|
||||||
!editModeContentOnly &&
|
|
||||||
!isDisplayModeContentEmpty;
|
|
||||||
|
|
||||||
if (loading === true) {
|
if (loading === true) {
|
||||||
return <RecordInlineCellSkeletonLoader />;
|
return <RecordInlineCellSkeletonLoader />;
|
||||||
}
|
}
|
||||||
@ -81,7 +71,6 @@ export const RecordInlineCellValue = ({
|
|||||||
<StyledClickableContainer readonly={readonly}>
|
<StyledClickableContainer readonly={readonly}>
|
||||||
<RecordInlineCellDisplayMode
|
<RecordInlineCellDisplayMode
|
||||||
disableHoverEffect={disableHoverEffect}
|
disableHoverEffect={disableHoverEffect}
|
||||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
|
||||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||||
isHovered={isFocused}
|
isHovered={isFocused}
|
||||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||||
@ -96,14 +85,14 @@ export const RecordInlineCellValue = ({
|
|||||||
>
|
>
|
||||||
<RecordInlineCellDisplayMode
|
<RecordInlineCellDisplayMode
|
||||||
disableHoverEffect={disableHoverEffect}
|
disableHoverEffect={disableHoverEffect}
|
||||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
|
||||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||||
isHovered={isFocused}
|
isHovered={isFocused}
|
||||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||||
|
buttonIcon={buttonIcon}
|
||||||
|
editModeContentOnly={editModeContentOnly}
|
||||||
>
|
>
|
||||||
{displayModeContent}
|
{displayModeContent}
|
||||||
</RecordInlineCellDisplayMode>
|
</RecordInlineCellDisplayMode>
|
||||||
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
|
||||||
</StyledClickableContainer>
|
</StyledClickableContainer>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList';
|
import { RecordDetailRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsList';
|
||||||
import { RecordDetailRelationRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem';
|
import { RecordDetailRelationRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem';
|
||||||
|
import { RecordDetailRelationRecordsListItemEffect } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItemEffect';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
type RecordDetailRelationRecordsListProps = {
|
type RecordDetailRelationRecordsListProps = {
|
||||||
@ -19,12 +20,18 @@ export const RecordDetailRelationRecordsList = ({
|
|||||||
return (
|
return (
|
||||||
<RecordDetailRecordsList>
|
<RecordDetailRecordsList>
|
||||||
{relationRecords.slice(0, 5).map((relationRecord) => (
|
{relationRecords.slice(0, 5).map((relationRecord) => (
|
||||||
<RecordDetailRelationRecordsListItem
|
<>
|
||||||
key={relationRecord.id}
|
<RecordDetailRelationRecordsListItemEffect
|
||||||
isExpanded={expandedItem === relationRecord.id}
|
key={`${relationRecord.id}-effect`}
|
||||||
onClick={handleItemClick}
|
relationRecordId={relationRecord.id}
|
||||||
relationRecord={relationRecord}
|
/>
|
||||||
/>
|
<RecordDetailRelationRecordsListItem
|
||||||
|
key={relationRecord.id}
|
||||||
|
isExpanded={expandedItem === relationRecord.id}
|
||||||
|
onClick={handleItemClick}
|
||||||
|
relationRecord={relationRecord}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
))}
|
))}
|
||||||
</RecordDetailRecordsList>
|
</RecordDetailRecordsList>
|
||||||
);
|
);
|
||||||
|
@ -15,7 +15,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
|||||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
import { useLazyFindOneRecord } from '@/object-record/hooks/useLazyFindOneRecord';
|
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import {
|
import {
|
||||||
FieldContext,
|
FieldContext,
|
||||||
@ -29,7 +28,6 @@ import { PropertyBox } from '@/object-record/record-inline-cell/property-box/com
|
|||||||
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
|
||||||
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
import { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
||||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
@ -99,12 +97,6 @@ export const RecordDetailRelationRecordsListItem = ({
|
|||||||
|
|
||||||
const persistField = usePersistField();
|
const persistField = usePersistField();
|
||||||
|
|
||||||
const {
|
|
||||||
called: hasFetchedRelationRecord,
|
|
||||||
findOneRecord: findOneRelationRecord,
|
|
||||||
} = useLazyFindOneRecord({
|
|
||||||
objectNameSingular: relationObjectMetadataNameSingular,
|
|
||||||
});
|
|
||||||
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
|
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
|
||||||
objectNameSingular: relationObjectMetadataNameSingular,
|
objectNameSingular: relationObjectMetadataNameSingular,
|
||||||
});
|
});
|
||||||
@ -168,8 +160,6 @@ export const RecordDetailRelationRecordsListItem = ({
|
|||||||
return [updateEntity, { loading: false }];
|
return [updateEntity, { loading: false }];
|
||||||
};
|
};
|
||||||
|
|
||||||
const { setRecords } = useSetRecordInStore();
|
|
||||||
|
|
||||||
const handleClick = () => onClick(relationRecord.id);
|
const handleClick = () => onClick(relationRecord.id);
|
||||||
|
|
||||||
const AnimatedIconChevronDown = useCallback<IconComponent>(
|
const AnimatedIconChevronDown = useCallback<IconComponent>(
|
||||||
@ -194,16 +184,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
|||||||
record={relationRecord}
|
record={relationRecord}
|
||||||
objectNameSingular={relationObjectMetadataItem.nameSingular}
|
objectNameSingular={relationObjectMetadataItem.nameSingular}
|
||||||
/>
|
/>
|
||||||
<StyledClickableZone
|
<StyledClickableZone onClick={handleClick}>
|
||||||
onClick={handleClick}
|
|
||||||
onMouseOver={() =>
|
|
||||||
!hasFetchedRelationRecord &&
|
|
||||||
findOneRelationRecord({
|
|
||||||
objectRecordId: relationRecord.id,
|
|
||||||
onCompleted: (record) => setRecords([record]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<LightIconButton
|
<LightIconButton
|
||||||
className="displayOnHover"
|
className="displayOnHover"
|
||||||
Icon={AnimatedIconChevronDown}
|
Icon={AnimatedIconChevronDown}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import { useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type RecordDetailRelationRecordsListItemEffectProps = {
|
||||||
|
relationRecordId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RecordDetailRelationRecordsListItemEffect = ({
|
||||||
|
relationRecordId,
|
||||||
|
}: RecordDetailRelationRecordsListItemEffectProps) => {
|
||||||
|
const { fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
|
const { relationObjectMetadataNameSingular } =
|
||||||
|
fieldDefinition.metadata as FieldRelationMetadata;
|
||||||
|
|
||||||
|
const { record } = useFindOneRecord({
|
||||||
|
objectNameSingular: relationObjectMetadataNameSingular,
|
||||||
|
objectRecordId: relationRecordId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { upsertRecords } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDefined(record)) {
|
||||||
|
upsertRecords([record]);
|
||||||
|
}
|
||||||
|
}, [record, upsertRecords]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -3,8 +3,8 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
export const useSetRecordInStore = () => {
|
export const useUpsertRecordsInStore = () => {
|
||||||
const setRecords = useRecoilCallback(
|
const upsertRecords = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
(records: ObjectRecord[]) => {
|
(records: ObjectRecord[]) => {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
@ -21,6 +21,6 @@ export const useSetRecordInStore = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setRecords,
|
upsertRecords,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -55,7 +55,8 @@ export const RecordTableHeader = ({
|
|||||||
}: {
|
}: {
|
||||||
createRecord: () => void;
|
createRecord: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { visibleTableColumnsSelector } = useRecordTableStates();
|
const { visibleTableColumnsSelector, hiddenTableColumnsSelector } =
|
||||||
|
useRecordTableStates();
|
||||||
|
|
||||||
const scrollWrapper = useScrollWrapperScopedRef();
|
const scrollWrapper = useScrollWrapperScopedRef();
|
||||||
const isTableWiderThanScreen =
|
const isTableWiderThanScreen =
|
||||||
@ -63,7 +64,7 @@ export const RecordTableHeader = ({
|
|||||||
(scrollWrapper.current?.scrollWidth ?? 0);
|
(scrollWrapper.current?.scrollWidth ?? 0);
|
||||||
|
|
||||||
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
||||||
const hiddenTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector());
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
@ -0,0 +1,132 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
useRecoilCallback,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
useSetRecoilState,
|
||||||
|
} from 'recoil';
|
||||||
|
|
||||||
|
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||||
|
import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
|
||||||
|
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
||||||
|
import {
|
||||||
|
ObjectRecordForSelect,
|
||||||
|
SelectedObjectRecordId,
|
||||||
|
useMultiObjectSearch,
|
||||||
|
} from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||||
|
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
||||||
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
|
export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({
|
||||||
|
selectedObjectRecordIds,
|
||||||
|
}: {
|
||||||
|
selectedObjectRecordIds: SelectedObjectRecordId[];
|
||||||
|
}) => {
|
||||||
|
const scopeId = useAvailableScopeIdOrThrow(
|
||||||
|
RelationPickerScopeInternalContext,
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
objectRecordsIdsMultiSelectState,
|
||||||
|
objectRecordMultiSelectCheckedRecordsIdsState,
|
||||||
|
recordMultiSelectIsLoadingState,
|
||||||
|
} = useObjectRecordMultiSelectScopedStates(scopeId);
|
||||||
|
const [objectRecordsIdsMultiSelect, setObjectRecordsIdsMultiSelect] =
|
||||||
|
useRecoilState(objectRecordsIdsMultiSelectState);
|
||||||
|
|
||||||
|
const setRecordMultiSelectIsLoading = useSetRecoilState(
|
||||||
|
recordMultiSelectIsLoadingState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const relationPickerScopedId = useAvailableScopeIdOrThrow(
|
||||||
|
RelationPickerScopeInternalContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
|
||||||
|
relationPickerScopedId,
|
||||||
|
});
|
||||||
|
const relationPickerSearchFilter = useRecoilValue(
|
||||||
|
relationPickerSearchFilterState,
|
||||||
|
);
|
||||||
|
const { filteredSelectedObjectRecords, loading, objectRecordsToSelect } =
|
||||||
|
useMultiObjectSearch({
|
||||||
|
searchFilterValue: relationPickerSearchFilter,
|
||||||
|
selectedObjectRecordIds,
|
||||||
|
excludedObjectRecordIds: [],
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [
|
||||||
|
objectRecordMultiSelectCheckedRecordsIds,
|
||||||
|
setObjectRecordMultiSelectCheckedRecordsIds,
|
||||||
|
] = useRecoilState(objectRecordMultiSelectCheckedRecordsIdsState);
|
||||||
|
|
||||||
|
const updateRecords = useRecoilCallback(
|
||||||
|
({ snapshot, set }) =>
|
||||||
|
(newRecords: ObjectRecordForSelect[]) => {
|
||||||
|
for (const newRecord of newRecords) {
|
||||||
|
const currentRecord = snapshot
|
||||||
|
.getLoadable(
|
||||||
|
objectRecordMultiSelectComponentFamilyState({
|
||||||
|
scopeId: scopeId,
|
||||||
|
familyKey: newRecord.record.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
const newRecordWithSelected = {
|
||||||
|
...newRecord,
|
||||||
|
selected: objectRecordMultiSelectCheckedRecordsIds.some(
|
||||||
|
(checkedRecordId) => checkedRecordId === newRecord.record.id,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDeeplyEqual(
|
||||||
|
newRecordWithSelected.selected,
|
||||||
|
currentRecord?.selected,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
set(
|
||||||
|
objectRecordMultiSelectComponentFamilyState({
|
||||||
|
scopeId: scopeId,
|
||||||
|
familyKey: newRecordWithSelected.record.id,
|
||||||
|
}),
|
||||||
|
newRecordWithSelected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[objectRecordMultiSelectCheckedRecordsIds, scopeId],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const allRecords = [
|
||||||
|
...(filteredSelectedObjectRecords ?? []),
|
||||||
|
...(objectRecordsToSelect ?? []),
|
||||||
|
];
|
||||||
|
updateRecords(allRecords);
|
||||||
|
const allRecordsIds = allRecords.map((record) => record.record.id);
|
||||||
|
if (!isDeeplyEqual(allRecordsIds, objectRecordsIdsMultiSelect)) {
|
||||||
|
setObjectRecordsIdsMultiSelect(allRecordsIds);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
filteredSelectedObjectRecords,
|
||||||
|
objectRecordsIdsMultiSelect,
|
||||||
|
objectRecordsToSelect,
|
||||||
|
setObjectRecordsIdsMultiSelect,
|
||||||
|
updateRecords,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setObjectRecordMultiSelectCheckedRecordsIds(
|
||||||
|
selectedObjectRecordIds.map((rec) => rec.id),
|
||||||
|
);
|
||||||
|
}, [selectedObjectRecordIds, setObjectRecordMultiSelectCheckedRecordsIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRecordMultiSelectIsLoading(loading);
|
||||||
|
}, [loading, setRecordMultiSelectIsLoading]);
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
@ -1,12 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||||
import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
|
import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
|
||||||
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
|
import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem';
|
||||||
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
||||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
||||||
|
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
||||||
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
@ -15,7 +17,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
|
|||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
|
|
||||||
export const StyledSelectableItem = styled(SelectableItem)`
|
export const StyledSelectableItem = styled(SelectableItem)`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -24,134 +26,80 @@ export const StyledSelectableItem = styled(SelectableItem)`
|
|||||||
export const MultiRecordSelect = ({
|
export const MultiRecordSelect = ({
|
||||||
onChange,
|
onChange,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
selectedObjectRecords,
|
|
||||||
allRecords,
|
|
||||||
loading,
|
|
||||||
searchFilter,
|
|
||||||
setSearchFilter,
|
|
||||||
}: {
|
}: {
|
||||||
onChange?: (
|
onChange?: (changedRecordForSelectId: string) => void;
|
||||||
changedRecordForSelect: ObjectRecordForSelect,
|
onSubmit?: () => void;
|
||||||
newSelectedValue: boolean,
|
|
||||||
) => void;
|
|
||||||
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
|
|
||||||
selectedObjectRecords: ObjectRecordForSelect[];
|
|
||||||
allRecords: ObjectRecordForSelect[];
|
|
||||||
loading: boolean;
|
|
||||||
searchFilter: string;
|
|
||||||
setSearchFilter: (searchFilter: string) => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [internalSelectedRecords, setInternalSelectedRecords] = useState<
|
const relationPickerScopedId = useAvailableScopeIdOrThrow(
|
||||||
ObjectRecordForSelect[]
|
RelationPickerScopeInternalContext,
|
||||||
>([]);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const { objectRecordsIdsMultiSelectState, recordMultiSelectIsLoadingState } =
|
||||||
if (!loading) {
|
useObjectRecordMultiSelectScopedStates(relationPickerScopedId);
|
||||||
setInternalSelectedRecords(selectedObjectRecords);
|
|
||||||
}
|
|
||||||
}, [selectedObjectRecords, loading]);
|
|
||||||
|
|
||||||
|
const recordMultiSelectIsLoading = useRecoilValue(
|
||||||
|
recordMultiSelectIsLoadingState,
|
||||||
|
);
|
||||||
|
const objectRecordsIdsMultiSelect = useRecoilValue(
|
||||||
|
objectRecordsIdsMultiSelectState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
|
||||||
|
relationPickerScopedId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setSearchFilter = useSetRecoilState(relationPickerSearchFilterState);
|
||||||
|
const relationPickerSearchFilter = useRecoilValue(
|
||||||
|
relationPickerSearchFilterState,
|
||||||
|
);
|
||||||
const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
|
const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, {
|
||||||
leading: true,
|
leading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFilterChange = useCallback(
|
||||||
debouncedSetSearchFilter(event.currentTarget.value);
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
};
|
debouncedSetSearchFilter(event.currentTarget.value);
|
||||||
|
},
|
||||||
const handleSelectChange = (
|
[debouncedSetSearchFilter],
|
||||||
changedRecordForSelect: ObjectRecordForSelect,
|
|
||||||
newSelectedValue: boolean,
|
|
||||||
) => {
|
|
||||||
const newSelectedRecords = newSelectedValue
|
|
||||||
? [...internalSelectedRecords, changedRecordForSelect]
|
|
||||||
: internalSelectedRecords.filter(
|
|
||||||
(selectedRecord) =>
|
|
||||||
selectedRecord.record.id !== changedRecordForSelect.record.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
setInternalSelectedRecords(newSelectedRecords);
|
|
||||||
|
|
||||||
onChange?.(changedRecordForSelect, newSelectedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const entitiesInDropdown = useMemo(
|
|
||||||
() =>
|
|
||||||
[...(allRecords ?? [])].filter((entity) =>
|
|
||||||
isNonEmptyString(entity.recordIdentifier.id),
|
|
||||||
),
|
|
||||||
[allRecords],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectableItemIds = entitiesInDropdown.map(
|
|
||||||
(entity) => entity.record.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MultipleObjectRecordOnClickOutsideEffect
|
<MultipleObjectRecordOnClickOutsideEffect
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
onClickOutside={() => {
|
onClickOutside={() => {
|
||||||
onSubmit?.(internalSelectedRecords);
|
onSubmit?.();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DropdownMenu ref={containerRef} data-select-disable>
|
<DropdownMenu ref={containerRef} data-select-disable>
|
||||||
<DropdownMenuSearchInput
|
<DropdownMenuSearchInput
|
||||||
value={searchFilter}
|
value={relationPickerSearchFilter}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItemsContainer hasMaxHeight>
|
<DropdownMenuItemsContainer hasMaxHeight>
|
||||||
{loading ? (
|
{recordMultiSelectIsLoading ? (
|
||||||
<MenuItem text="Loading..." />
|
<MenuItem text="Loading..." />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SelectableList
|
<SelectableList
|
||||||
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
|
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
|
||||||
selectableItemIdArray={selectableItemIds}
|
selectableItemIdArray={objectRecordsIdsMultiSelect}
|
||||||
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
hotkeyScope={RelationPickerHotkeyScope.RelationPicker}
|
||||||
onEnter={(recordId) => {
|
|
||||||
const recordIsSelected = internalSelectedRecords?.some(
|
|
||||||
(selectedRecord) => selectedRecord.record.id === recordId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const correspondingRecordForSelect = entitiesInDropdown?.find(
|
|
||||||
(entity) => entity.record.id === recordId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDefined(correspondingRecordForSelect)) {
|
|
||||||
handleSelectChange(
|
|
||||||
correspondingRecordForSelect,
|
|
||||||
!recordIsSelected,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{entitiesInDropdown?.map((objectRecordForSelect) => (
|
{objectRecordsIdsMultiSelect?.map((recordId) => {
|
||||||
<MultipleObjectRecordSelectItem
|
return (
|
||||||
key={objectRecordForSelect.record.id}
|
<MultipleObjectRecordSelectItem
|
||||||
objectRecordForSelect={objectRecordForSelect}
|
key={recordId}
|
||||||
onSelectedChange={(newSelectedValue) =>
|
objectRecordId={recordId}
|
||||||
handleSelectChange(
|
onChange={onChange}
|
||||||
objectRecordForSelect,
|
/>
|
||||||
newSelectedValue,
|
);
|
||||||
)
|
})}
|
||||||
}
|
|
||||||
selected={internalSelectedRecords?.some(
|
|
||||||
(selectedRecord) => {
|
|
||||||
return (
|
|
||||||
selectedRecord.record.id ===
|
|
||||||
objectRecordForSelect.record.id
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SelectableList>
|
</SelectableList>
|
||||||
{entitiesInDropdown?.length === 0 && (
|
{objectRecordsIdsMultiSelect?.length === 0 && (
|
||||||
<MenuItem text="No result" />
|
<MenuItem text="No result" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
|
||||||
import {
|
|
||||||
ObjectRecordForSelect,
|
|
||||||
SelectedObjectRecordId,
|
|
||||||
useMultiObjectSearch,
|
|
||||||
} from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
|
||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
|
||||||
|
|
||||||
export const StyledSelectableItem = styled(SelectableItem)`
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
export const MultipleObjectRecordSelect = ({
|
|
||||||
onChange,
|
|
||||||
onSubmit,
|
|
||||||
selectedObjectRecordIds,
|
|
||||||
}: {
|
|
||||||
onChange?: (
|
|
||||||
changedRecordForSelect: ObjectRecordForSelect,
|
|
||||||
newSelectedValue: boolean,
|
|
||||||
) => void;
|
|
||||||
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
|
|
||||||
selectedObjectRecordIds: SelectedObjectRecordId[];
|
|
||||||
}) => {
|
|
||||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
|
||||||
|
|
||||||
const {
|
|
||||||
filteredSelectedObjectRecords,
|
|
||||||
loading,
|
|
||||||
objectRecordsToSelect,
|
|
||||||
selectedObjectRecords,
|
|
||||||
} = useMultiObjectSearch({
|
|
||||||
searchFilterValue: searchFilter,
|
|
||||||
selectedObjectRecordIds,
|
|
||||||
excludedObjectRecordIds: [],
|
|
||||||
limit: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedObjectRecordsForSelect = useMemo(
|
|
||||||
() =>
|
|
||||||
selectedObjectRecords.filter((selectedObjectRecord) =>
|
|
||||||
selectedObjectRecordIds.some(
|
|
||||||
(selectedObjectRecordId) =>
|
|
||||||
selectedObjectRecordId.id ===
|
|
||||||
selectedObjectRecord.recordIdentifier.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
[selectedObjectRecords, selectedObjectRecordIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MultiRecordSelect
|
|
||||||
onChange={onChange}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
selectedObjectRecords={selectedObjectRecordsForSelect}
|
|
||||||
allRecords={[
|
|
||||||
...(filteredSelectedObjectRecords ?? []),
|
|
||||||
...(objectRecordsToSelect ?? []),
|
|
||||||
]}
|
|
||||||
loading={loading}
|
|
||||||
searchFilter={searchFilter}
|
|
||||||
setSearchFilter={setSearchFilter}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -3,12 +3,15 @@ import { useRecoilValue } from 'recoil';
|
|||||||
import { Avatar } from 'twenty-ui';
|
import { Avatar } from 'twenty-ui';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||||
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId';
|
||||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
||||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||||
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
|
import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar';
|
||||||
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const StyledSelectableItem = styled(SelectableItem)`
|
export const StyledSelectableItem = styled(SelectableItem)`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -16,45 +19,60 @@ export const StyledSelectableItem = styled(SelectableItem)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const MultipleObjectRecordSelectItem = ({
|
export const MultipleObjectRecordSelectItem = ({
|
||||||
objectRecordForSelect,
|
objectRecordId,
|
||||||
onSelectedChange,
|
onChange,
|
||||||
selected,
|
|
||||||
}: {
|
}: {
|
||||||
objectRecordForSelect: ObjectRecordForSelect;
|
objectRecordId: string;
|
||||||
onSelectedChange?: (selected: boolean) => void;
|
onChange?: (changedRecordForSelectId: string) => void;
|
||||||
selected: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const { isSelectedItemIdSelector } = useSelectableList(
|
const { isSelectedItemIdSelector } = useSelectableList(
|
||||||
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
|
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSelectedByKeyboard = useRecoilValue(
|
const isSelectedByKeyboard = useRecoilValue(
|
||||||
isSelectedItemIdSelector(objectRecordForSelect.record.id),
|
isSelectedItemIdSelector(objectRecordId),
|
||||||
|
);
|
||||||
|
const scopeId = useAvailableScopeIdOrThrow(
|
||||||
|
RelationPickerScopeInternalContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { objectRecordMultiSelectFamilyState } =
|
||||||
|
useObjectRecordMultiSelectScopedStates(scopeId);
|
||||||
|
|
||||||
|
const record = useRecoilValue(
|
||||||
|
objectRecordMultiSelectFamilyState(objectRecordId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectChange = () => {
|
||||||
|
onChange?.(objectRecordId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { selected, recordIdentifier } = record;
|
||||||
|
|
||||||
|
if (!isDefined(recordIdentifier)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSelectableItem
|
<StyledSelectableItem itemId={objectRecordId} key={objectRecordId + v4()}>
|
||||||
itemId={objectRecordForSelect.record.id}
|
|
||||||
key={objectRecordForSelect.record.id + v4()}
|
|
||||||
>
|
|
||||||
<MenuItemMultiSelectAvatar
|
<MenuItemMultiSelectAvatar
|
||||||
selected={selected}
|
onSelectChange={(_isNewlySelectedValue) => handleSelectChange()}
|
||||||
onSelectChange={onSelectedChange}
|
|
||||||
isKeySelected={isSelectedByKeyboard}
|
isKeySelected={isSelectedByKeyboard}
|
||||||
|
selected={selected}
|
||||||
avatar={
|
avatar={
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={getImageAbsoluteURIOrBase64(
|
avatarUrl={getImageAbsoluteURIOrBase64(recordIdentifier.avatarUrl)}
|
||||||
objectRecordForSelect.recordIdentifier.avatarUrl,
|
entityId={objectRecordId}
|
||||||
)}
|
placeholder={recordIdentifier.name}
|
||||||
entityId={objectRecordForSelect.record.id}
|
|
||||||
placeholder={objectRecordForSelect.recordIdentifier.name}
|
|
||||||
size="md"
|
size="md"
|
||||||
type={
|
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||||
objectRecordForSelect.recordIdentifier.avatarType ?? 'rounded'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
text={objectRecordForSelect.recordIdentifier.name}
|
text={recordIdentifier.name}
|
||||||
/>
|
/>
|
||||||
</StyledSelectableItem>
|
</StyledSelectableItem>
|
||||||
);
|
);
|
||||||
|
@ -44,7 +44,6 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
|||||||
const { entities, relationPickerSearchFilter } =
|
const { entities, relationPickerSearchFilter } =
|
||||||
useRelationPickerEntitiesOptions({
|
useRelationPickerEntitiesOptions({
|
||||||
relationObjectNameSingular,
|
relationObjectNameSingular,
|
||||||
relationPickerScopeId,
|
|
||||||
selectedRelationRecordIds,
|
selectedRelationRecordIds,
|
||||||
excludedRelationRecordIds,
|
excludedRelationRecordIds,
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
export const TABLE_COLUMNS_DENY_LIST = [
|
||||||
|
'attachments',
|
||||||
|
'activities',
|
||||||
|
'timelineActivities',
|
||||||
|
];
|
@ -1,22 +1,26 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates';
|
||||||
|
import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext';
|
||||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||||
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
|
|
||||||
export const useRelationPickerEntitiesOptions = ({
|
export const useRelationPickerEntitiesOptions = ({
|
||||||
relationObjectNameSingular,
|
relationObjectNameSingular,
|
||||||
relationPickerScopeId = 'relation-picker',
|
|
||||||
selectedRelationRecordIds = [],
|
selectedRelationRecordIds = [],
|
||||||
excludedRelationRecordIds = [],
|
excludedRelationRecordIds = [],
|
||||||
}: {
|
}: {
|
||||||
relationObjectNameSingular: string;
|
relationObjectNameSingular: string;
|
||||||
relationPickerScopeId?: string;
|
|
||||||
selectedRelationRecordIds?: string[];
|
selectedRelationRecordIds?: string[];
|
||||||
excludedRelationRecordIds?: string[];
|
excludedRelationRecordIds?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
|
const scopeId = useAvailableScopeIdOrThrow(
|
||||||
|
RelationPickerScopeInternalContext,
|
||||||
|
);
|
||||||
|
|
||||||
const { searchQueryState, relationPickerSearchFilterState } =
|
const { searchQueryState, relationPickerSearchFilterState } =
|
||||||
useRelationPickerScopedStates({
|
useRelationPickerScopedStates({
|
||||||
relationPickerScopedId: relationPickerScopeId,
|
relationPickerScopedId: scopeId,
|
||||||
});
|
});
|
||||||
const relationPickerSearchFilter = useRecoilValue(
|
const relationPickerSearchFilter = useRecoilValue(
|
||||||
relationPickerSearchFilterState,
|
relationPickerSearchFilterState,
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||||
|
import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/relation-picker/constants/TableColumnsDenyList';
|
||||||
|
|
||||||
export const filterAvailableTableColumns = (
|
export const filterAvailableTableColumns = (
|
||||||
columnDefinition: ColumnDefinition<FieldMetadata>,
|
columnDefinition: ColumnDefinition<FieldMetadata>,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (
|
if (
|
||||||
isFieldRelation(columnDefinition) &&
|
isFieldRelation(columnDefinition) &&
|
||||||
columnDefinition.metadata?.relationType !== 'TO_ONE_OBJECT'
|
columnDefinition.metadata?.relationType !== 'TO_ONE_OBJECT' &&
|
||||||
|
columnDefinition.metadata?.relationType !== 'FROM_MANY_OBJECTS'
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TABLE_COLUMNS_DENY_LIST.includes(columnDefinition.metadata.fieldName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (columnDefinition.type === 'UUID') {
|
if (columnDefinition.type === 'UUID') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,7 @@ import { isString } from '@sniptt/guards';
|
|||||||
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
import { isFieldRelationToOneValue } from '@/object-record/record-field/types/guards/isFieldRelationToOneValue';
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
@ -31,7 +30,7 @@ export const sanitizeRecordInput = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||||
isFieldRelationValue<EntityForSelect>(fieldValue)
|
isFieldRelationToOneValue(fieldValue)
|
||||||
) {
|
) {
|
||||||
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||||
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
type SettingsDataModelSetRecordEffectProps = {
|
type SettingsDataModelSetRecordEffectProps = {
|
||||||
@ -10,11 +10,11 @@ type SettingsDataModelSetRecordEffectProps = {
|
|||||||
export const SettingsDataModelSetRecordEffect = ({
|
export const SettingsDataModelSetRecordEffect = ({
|
||||||
record,
|
record,
|
||||||
}: SettingsDataModelSetRecordEffectProps) => {
|
}: SettingsDataModelSetRecordEffectProps) => {
|
||||||
const { setRecords: setRecordsInStore } = useSetRecordInStore();
|
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecordsInStore([record]);
|
upsertRecordsInStore([record]);
|
||||||
}, [record, setRecordsInStore]);
|
}, [record, upsertRecordsInStore]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -64,7 +64,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
|||||||
type: FieldMetadataType.Relation,
|
type: FieldMetadataType.Relation,
|
||||||
metadata: {
|
metadata: {
|
||||||
fieldName: 'favorites',
|
fieldName: 'favorites',
|
||||||
placeHolder: 'Favorites',
|
|
||||||
relationType: 'FROM_MANY_OBJECTS',
|
relationType: 'FROM_MANY_OBJECTS',
|
||||||
relationObjectMetadataNameSingular: '',
|
relationObjectMetadataNameSingular: '',
|
||||||
relationObjectMetadataNamePlural: '',
|
relationObjectMetadataNamePlural: '',
|
||||||
@ -99,7 +98,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
|||||||
type: FieldMetadataType.Relation,
|
type: FieldMetadataType.Relation,
|
||||||
metadata: {
|
metadata: {
|
||||||
fieldName: 'accountOwner',
|
fieldName: 'accountOwner',
|
||||||
placeHolder: 'Account Owner',
|
|
||||||
relationType: 'TO_ONE_OBJECT',
|
relationType: 'TO_ONE_OBJECT',
|
||||||
relationObjectMetadataNameSingular: 'workspaceMember',
|
relationObjectMetadataNameSingular: 'workspaceMember',
|
||||||
relationObjectMetadataNamePlural: 'workspaceMembers',
|
relationObjectMetadataNamePlural: 'workspaceMembers',
|
||||||
@ -117,7 +115,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
|||||||
type: FieldMetadataType.Relation,
|
type: FieldMetadataType.Relation,
|
||||||
metadata: {
|
metadata: {
|
||||||
fieldName: 'people',
|
fieldName: 'people',
|
||||||
placeHolder: 'People',
|
|
||||||
relationType: 'FROM_MANY_OBJECTS',
|
relationType: 'FROM_MANY_OBJECTS',
|
||||||
relationObjectMetadataNameSingular: '',
|
relationObjectMetadataNameSingular: '',
|
||||||
relationObjectMetadataNamePlural: '',
|
relationObjectMetadataNamePlural: '',
|
||||||
@ -135,7 +132,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
|||||||
type: FieldMetadataType.Relation,
|
type: FieldMetadataType.Relation,
|
||||||
metadata: {
|
metadata: {
|
||||||
fieldName: 'attachments',
|
fieldName: 'attachments',
|
||||||
placeHolder: 'Attachments',
|
|
||||||
relationType: 'FROM_MANY_OBJECTS',
|
relationType: 'FROM_MANY_OBJECTS',
|
||||||
relationObjectMetadataNameSingular: '',
|
relationObjectMetadataNameSingular: '',
|
||||||
relationObjectMetadataNamePlural: '',
|
relationObjectMetadataNamePlural: '',
|
||||||
@ -204,7 +200,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
|||||||
type: FieldMetadataType.Relation,
|
type: FieldMetadataType.Relation,
|
||||||
metadata: {
|
metadata: {
|
||||||
fieldName: 'opportunities',
|
fieldName: 'opportunities',
|
||||||
placeHolder: 'Opportunities',
|
|
||||||
relationType: 'FROM_MANY_OBJECTS',
|
relationType: 'FROM_MANY_OBJECTS',
|
||||||
relationObjectMetadataNameSingular: '',
|
relationObjectMetadataNameSingular: '',
|
||||||
relationObjectMetadataNamePlural: '',
|
relationObjectMetadataNamePlural: '',
|
||||||
@ -239,7 +234,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
|||||||
type: FieldMetadataType.Relation,
|
type: FieldMetadataType.Relation,
|
||||||
metadata: {
|
metadata: {
|
||||||
fieldName: 'activityTargets',
|
fieldName: 'activityTargets',
|
||||||
placeHolder: 'Activities',
|
|
||||||
relationType: 'FROM_MANY_OBJECTS',
|
relationType: 'FROM_MANY_OBJECTS',
|
||||||
relationObjectMetadataNameSingular: '',
|
relationObjectMetadataNameSingular: '',
|
||||||
relationObjectMetadataNamePlural: '',
|
relationObjectMetadataNamePlural: '',
|
||||||
|
@ -20,8 +20,8 @@ describe('useContextScopeId', () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const scopedId = result.current;
|
const scopeId = result.current;
|
||||||
expect(scopedId).toBe(mockedContextValue);
|
expect(scopeId).toBe(mockedContextValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw an error when used outside of the specified context', () => {
|
it('Should throw an error when used outside of the specified context', () => {
|
||||||
|
@ -20,8 +20,8 @@ describe('useRecoilScopeId', () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const scopedId = result.current;
|
const scopeId = result.current;
|
||||||
expect(scopedId).toBe(mockedContextValue);
|
expect(scopeId).toBe(mockedContextValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw an error when used outside of the specified context', () => {
|
it('Should throw an error when used outside of the specified context', () => {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Decorator } from '@storybook/react';
|
import { Decorator } from '@storybook/react';
|
||||||
|
|
||||||
import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
|
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||||
|
|
||||||
export const RecordStoreDecorator: Decorator = (Story, context) => {
|
export const RecordStoreDecorator: Decorator = (Story, context) => {
|
||||||
const { records } = context.parameters;
|
const { records } = context.parameters;
|
||||||
|
|
||||||
const { setRecords } = useSetRecordInStore();
|
const { upsertRecords } = useUpsertRecordsInStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRecords(records);
|
upsertRecords(records);
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Story />;
|
return <Story />;
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export type WithNarrowedStringLiteralProperty<
|
||||||
|
T,
|
||||||
|
K extends keyof T,
|
||||||
|
Sub extends T[K],
|
||||||
|
> = Omit<T, K> & {
|
||||||
|
[P in K]: Extract<T[K], Sub>;
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
|
|
||||||
export const isDeeplyEqual = <T>(a: T, b: T) => deepEqual(a, b);
|
export const isDeeplyEqual = <T>(a: T, b: T, options?: { strict: boolean }) =>
|
||||||
|
deepEqual(a, b, options);
|
||||||
|
Loading…
Reference in New Issue
Block a user