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 { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
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 = () => {
|
||||
const { setRecords } = useSetRecordInStore();
|
||||
const { upsertRecords } = useUpsertRecordsInStore();
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
const { record: calendarEvent } = useFindOneRecord<CalendarEvent>({
|
||||
objectNameSingular:
|
||||
FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular,
|
||||
objectRecordId: viewableRecordId ?? '',
|
||||
recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields,
|
||||
onCompleted: (record) => setRecords([record]),
|
||||
onCompleted: (record) => upsertRecords([record]),
|
||||
});
|
||||
|
||||
if (!calendarEvent) {
|
||||
|
@ -86,14 +86,6 @@ export const ActivityEditorFields = ({
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'activityTargets',
|
||||
fieldPosition: 3,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledPropertyBox>
|
||||
{activity.type === 'Task' &&
|
||||
@ -112,15 +104,11 @@ export const ActivityEditorFields = ({
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{ActivityTargetsContextProvider &&
|
||||
isDefined(activityFromCache) &&
|
||||
isRightDrawerAnimationCompleted && (
|
||||
<ActivityTargetsContextProvider>
|
||||
{isDefined(activityFromCache) && isRightDrawerAnimationCompleted && (
|
||||
<ActivityTargetsInlineCell
|
||||
activity={activityFromCache}
|
||||
maxWidth={340}
|
||||
/>
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
|
@ -8,11 +8,11 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
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 = () => {
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
const { setRecords } = useSetRecordInStore();
|
||||
const { upsertRecords } = useUpsertRecordsInStore();
|
||||
|
||||
const { record: thread } = useFindOneRecord<EmailThread>({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageThread,
|
||||
@ -20,7 +20,7 @@ export const useRightDrawerEmailThread = () => {
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
},
|
||||
onCompleted: (record) => setRecords([record]),
|
||||
onCompleted: (record) => upsertRecords([record]),
|
||||
});
|
||||
|
||||
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 { isNonEmptyArray, isNull } from '@sniptt/guards';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
@ -15,10 +16,17 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
|
||||
import { objectRecordMultiSelectCheckedRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectCheckedRecordsIdsComponentState';
|
||||
import {
|
||||
ObjectRecordAndSelected,
|
||||
objectRecordMultiSelectComponentFamilyState,
|
||||
} from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { MultipleObjectRecordSelect } from '@/object-record/relation-picker/components/MultipleObjectRecordSelect';
|
||||
import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
|
||||
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
|
||||
const StyledSelectContainer = styled.div`
|
||||
@ -37,6 +45,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
activityTargetWithTargetRecords,
|
||||
}: ActivityTargetInlineCellEditModeProps) => {
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
const relationPickerScopeId = `relation-picker-${activity.id}`;
|
||||
|
||||
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
|
||||
(activityTarget) => ({
|
||||
@ -74,91 +83,22 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const handleSubmit = async (selectedRecords: ObjectRecordForSelect[]) => {
|
||||
closeEditableField();
|
||||
const handleSubmit = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
const activityTargetsAfterUpdate =
|
||||
activityTargetWithTargetRecords.filter((activityTarget) => {
|
||||
const record = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: relationPickerScopeId,
|
||||
familyKey: activityTarget.targetObject.id,
|
||||
}),
|
||||
)
|
||||
.getValue() as ObjectRecordAndSelected;
|
||||
|
||||
const activityTargetsToDelete = activityTargetWithTargetRecords.filter(
|
||||
(activityTargetObjectRecord) =>
|
||||
!selectedRecords.some(
|
||||
(selectedRecord) =>
|
||||
selectedRecord.recordIdentifier.id ===
|
||||
activityTargetObjectRecord.targetObject.id,
|
||||
),
|
||||
);
|
||||
|
||||
const selectedTargetObjectsToCreate = selectedRecords.filter(
|
||||
(selectedRecord) =>
|
||||
!activityTargetWithTargetRecords.some(
|
||||
(activityTargetWithTargetRecord) =>
|
||||
activityTargetWithTargetRecord.targetObject.id ===
|
||||
selectedRecord.recordIdentifier.id,
|
||||
),
|
||||
);
|
||||
|
||||
const existingActivityTargets = activityTargetWithTargetRecords.map(
|
||||
(activityTargetObjectRecord) => activityTargetObjectRecord.activityTarget,
|
||||
);
|
||||
|
||||
let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
|
||||
|
||||
const activityTargetsToCreate = selectedTargetObjectsToCreate.map(
|
||||
(selectedRecord) => {
|
||||
const emptyActivityTarget = prefillRecord<ActivityTarget>({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
input: {
|
||||
id: v4(),
|
||||
activityId: activity.id,
|
||||
activity,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
[getActivityTargetObjectFieldName({
|
||||
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
|
||||
})]: selectedRecord.record,
|
||||
[getActivityTargetObjectFieldIdName({
|
||||
nameSingular: selectedRecord.objectMetadataItem.nameSingular,
|
||||
})]: selectedRecord.recordIdentifier.id,
|
||||
},
|
||||
return record.selected;
|
||||
});
|
||||
|
||||
return emptyActivityTarget;
|
||||
},
|
||||
);
|
||||
|
||||
activityTargetsAfterUpdate.push(...activityTargetsToCreate);
|
||||
|
||||
if (isNonEmptyArray(activityTargetsToDelete)) {
|
||||
activityTargetsAfterUpdate = activityTargetsAfterUpdate.filter(
|
||||
(activityTarget) =>
|
||||
!activityTargetsToDelete.some(
|
||||
(activityTargetToDelete) =>
|
||||
activityTargetToDelete.activityTarget.id === activityTarget.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
createManyActivityTargetsInCache(activityTargetsToCreate);
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (activityTargetsToCreate.length > 0) {
|
||||
await createManyActivityTargets(activityTargetsToCreate);
|
||||
}
|
||||
|
||||
if (activityTargetsToDelete.length > 0) {
|
||||
await deleteManyActivityTargets(
|
||||
activityTargetsToDelete.map(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.activityTarget.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setActivityFromStore((currentActivity) => {
|
||||
if (isNull(currentActivity)) {
|
||||
return null;
|
||||
@ -169,14 +109,155 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
};
|
||||
});
|
||||
};
|
||||
closeEditableField();
|
||||
},
|
||||
[
|
||||
activityTargetWithTargetRecords,
|
||||
closeEditableField,
|
||||
relationPickerScopeId,
|
||||
setActivityFromStore,
|
||||
],
|
||||
);
|
||||
|
||||
const handleChange = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async (recordId: string) => {
|
||||
const existingActivityTargets = activityTargetWithTargetRecords.map(
|
||||
(activityTargetObjectRecord) =>
|
||||
activityTargetObjectRecord.activityTarget,
|
||||
);
|
||||
|
||||
let activityTargetsAfterUpdate = Array.from(existingActivityTargets);
|
||||
|
||||
const previouslyCheckedRecordsIds = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const isNewlySelected = !previouslyCheckedRecordsIds.includes(recordId);
|
||||
|
||||
if (isNewlySelected) {
|
||||
const record = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: relationPickerScopeId,
|
||||
familyKey: recordId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
`Could not find selected record with id ${recordId}`,
|
||||
);
|
||||
}
|
||||
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
}),
|
||||
(prev) => [...prev, recordId],
|
||||
);
|
||||
|
||||
const newActivityTargetId = v4();
|
||||
const fieldName = getActivityTargetObjectFieldName({
|
||||
nameSingular: record.objectMetadataItem.nameSingular,
|
||||
});
|
||||
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: record.objectMetadataItem.nameSingular,
|
||||
});
|
||||
const newActivityTarget = prefillRecord<ActivityTarget>({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
input: {
|
||||
id: newActivityTargetId,
|
||||
activityId: activity.id,
|
||||
activity,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
[fieldName]: record.record,
|
||||
[fieldNameWithIdSuffix]: recordId,
|
||||
},
|
||||
});
|
||||
|
||||
activityTargetsAfterUpdate.push(newActivityTarget);
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
createManyActivityTargetsInCache([newActivityTarget]);
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await createManyActivityTargets([newActivityTarget]);
|
||||
}
|
||||
|
||||
set(activityTargetObjectRecordFamilyState(recordId), {
|
||||
activityTargetId: newActivityTargetId,
|
||||
});
|
||||
} else {
|
||||
const activityTargetToDeleteId = snapshot
|
||||
.getLoadable(activityTargetObjectRecordFamilyState(recordId))
|
||||
.getValue().activityTargetId;
|
||||
|
||||
if (!activityTargetToDeleteId) {
|
||||
throw new Error('Could not delete this activity target.');
|
||||
}
|
||||
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
}),
|
||||
previouslyCheckedRecordsIds.filter((id) => id !== recordId),
|
||||
);
|
||||
activityTargetsAfterUpdate = activityTargetsAfterUpdate.filter(
|
||||
(activityTarget) => activityTarget.id !== activityTargetToDeleteId,
|
||||
);
|
||||
|
||||
if (isActivityInCreateMode) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await deleteManyActivityTargets([activityTargetToDeleteId]);
|
||||
}
|
||||
|
||||
set(activityTargetObjectRecordFamilyState(recordId), {
|
||||
activityTargetId: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
activity,
|
||||
activityTargetWithTargetRecords,
|
||||
createManyActivityTargets,
|
||||
createManyActivityTargetsInCache,
|
||||
deleteManyActivityTargets,
|
||||
isActivityInCreateMode,
|
||||
objectMetadataItemActivityTarget,
|
||||
relationPickerScopeId,
|
||||
upsertActivity,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledSelectContainer>
|
||||
<MultipleObjectRecordSelect
|
||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||
onSubmit={handleSubmit}
|
||||
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||
<ActivityTargetObjectRecordEffect
|
||||
activityTargetWithTargetRecords={activityTargetWithTargetRecords}
|
||||
/>
|
||||
<ActivityTargetInlineCellEditModeMultiRecordsEffect
|
||||
selectedObjectRecordIds={selectedTargetObjectIds}
|
||||
/>
|
||||
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
||||
</RelationPickerScope>
|
||||
</StyledSelectContainer>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { activityTargetObjectRecordFamilyState } from '@/object-record/record-field/states/activityTargetObjectRecordFamilyState';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const ActivityTargetObjectRecordEffect = ({
|
||||
activityTargetWithTargetRecords,
|
||||
}: {
|
||||
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
||||
}) => {
|
||||
const updateActivityTargets = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(newActivityTargets: ActivityTargetWithTargetRecord[]) => {
|
||||
for (const newActivityTarget of newActivityTargets) {
|
||||
const objectRecordId = newActivityTarget.targetObject.id;
|
||||
const record = snapshot
|
||||
.getLoadable(activityTargetObjectRecordFamilyState(objectRecordId))
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
record.activityTargetId,
|
||||
newActivityTarget.activityTarget.id,
|
||||
)
|
||||
) {
|
||||
set(activityTargetObjectRecordFamilyState(objectRecordId), {
|
||||
activityTargetId: newActivityTarget.activityTarget.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateActivityTargets(activityTargetWithTargetRecords);
|
||||
}, [activityTargetWithTargetRecords, updateActivityTargets]);
|
||||
|
||||
return <></>;
|
||||
};
|
@ -7,6 +7,8 @@ import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTa
|
||||
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||
@ -41,9 +43,20 @@ export const ActivityTargetsInlineCell = ({
|
||||
ActivityEditorHotkeyScope.ActivityTargets,
|
||||
);
|
||||
|
||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activity.id,
|
||||
fieldMetadataName: 'activityTargets',
|
||||
fieldPosition: 3,
|
||||
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<RecordFieldInputScope recordFieldInputScopeId={activity?.id ?? ''}>
|
||||
<FieldFocusContextProvider>
|
||||
{ActivityTargetsContextProvider && (
|
||||
<ActivityTargetsContextProvider>
|
||||
<RecordInlineCellContainer
|
||||
buttonIcon={IconPencil}
|
||||
customEditHotkeyScope={{
|
||||
@ -66,8 +79,9 @@ export const ActivityTargetsInlineCell = ({
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
}
|
||||
isDisplayModeContentEmpty={activityTargetObjectRecords.length === 0}
|
||||
/>
|
||||
</ActivityTargetsContextProvider>
|
||||
)}
|
||||
</FieldFocusContextProvider>
|
||||
</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 { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||
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 { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityWithoutTargetsFamilyState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
@ -68,11 +68,11 @@ export const TimelineQueryEffect = ({
|
||||
(newActivities: Activity[]) => {
|
||||
for (const newActivity of newActivities) {
|
||||
const currentActivity = snapshot
|
||||
.getLoadable(timelineActivitiesFammilyState(newActivity.id))
|
||||
.getLoadable(timelineActivitiesFamilyState(newActivity.id))
|
||||
.getValue();
|
||||
|
||||
if (!isDeeplyEqual(newActivity, currentActivity)) {
|
||||
set(timelineActivitiesFammilyState(newActivity.id), newActivity);
|
||||
set(timelineActivitiesFamilyState(newActivity.id), newActivity);
|
||||
}
|
||||
|
||||
const currentActivityWithoutTarget = snapshot
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export const timelineActivitiesFammilyState = createFamilyState<
|
||||
export const timelineActivitiesFamilyState = createFamilyState<
|
||||
Activity | null,
|
||||
string
|
||||
>({
|
||||
key: 'timelineActivitiesFammilyState',
|
||||
key: 'timelineActivitiesFamilyState',
|
||||
defaultValue: null,
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-dra
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
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 {
|
||||
formatToHumanReadableDay,
|
||||
formatToHumanReadableMonth,
|
||||
@ -85,7 +85,7 @@ export const EventCardCalendarEvent = ({
|
||||
}: {
|
||||
calendarEventId: string;
|
||||
}) => {
|
||||
const { setRecords } = useSetRecordInStore();
|
||||
const { upsertRecords } = useUpsertRecordsInStore();
|
||||
|
||||
const {
|
||||
record: calendarEvent,
|
||||
@ -101,7 +101,7 @@ export const EventCardCalendarEvent = ({
|
||||
endsAt: true,
|
||||
},
|
||||
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
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';
|
||||
|
||||
const StyledEventCardMessageContainer = styled.div`
|
||||
@ -56,7 +56,7 @@ export const EventCardMessage = ({
|
||||
messageId: string;
|
||||
authorFullName: string;
|
||||
}) => {
|
||||
const { setRecords } = useSetRecordInStore();
|
||||
const { upsertRecords } = useUpsertRecordsInStore();
|
||||
|
||||
const {
|
||||
record: message,
|
||||
@ -75,7 +75,7 @@ export const EventCardMessage = ({
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
setRecords([data]);
|
||||
upsertRecords([data]);
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -70,6 +70,7 @@ export const triggerUpdateRelationsOptimisticEffect = ({
|
||||
isDeeplyEqual(
|
||||
currentFieldValueOnSourceRecord,
|
||||
updatedFieldValueOnSourceRecord,
|
||||
{ strict: true },
|
||||
)
|
||||
) {
|
||||
return;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
RelationDefinitionType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { ObjectMetadataItem } from '../types/ObjectMetadataItem';
|
||||
|
||||
@ -10,6 +12,15 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
||||
fields: Array<ObjectMetadataItem['fields'][0]>;
|
||||
}): FilterDefinition[] =>
|
||||
fields.reduce((acc, field) => {
|
||||
if (
|
||||
field.type === FieldMetadataType.Relation &&
|
||||
field.relationDefinition?.direction !==
|
||||
RelationDefinitionType.ManyToOne &&
|
||||
field.relationDefinition?.direction !== RelationDefinitionType.OneToOne
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (
|
||||
![
|
||||
FieldMetadataType.DateTime,
|
||||
@ -33,12 +44,6 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (field.type === FieldMetadataType.Relation) {
|
||||
if (isDefined(field.fromRelationMetadata)) {
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
return [...acc, formatFieldMetadataItemAsFilterDefinition({ field })];
|
||||
}, [] as FilterDefinition[]);
|
||||
|
||||
@ -51,9 +56,9 @@ export const formatFieldMetadataItemAsFilterDefinition = ({
|
||||
label: field.label,
|
||||
iconName: field.icon ?? 'Icon123',
|
||||
relationObjectMetadataNamePlural:
|
||||
field.toRelationMetadata?.fromObjectMetadata.namePlural,
|
||||
field.relationDefinition?.targetObjectMetadata.namePlural,
|
||||
relationObjectMetadataNameSingular:
|
||||
field.toRelationMetadata?.fromObjectMetadata.nameSingular,
|
||||
field.relationDefinition?.targetObjectMetadata.nameSingular,
|
||||
type: getFilterTypeFromFieldType(field.type),
|
||||
});
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename';
|
||||
import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
|
||||
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import {
|
||||
@ -39,7 +40,7 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
||||
|
||||
if (!isRootLevel && computeReferences) {
|
||||
return {
|
||||
__ref: `${nodeTypeName}:${record.id}`,
|
||||
__ref: getRefName(objectMetadataItem.nameSingular, record.id),
|
||||
} 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,
|
||||
objectMetadataItem,
|
||||
cache,
|
||||
recordGqlFields = undefined,
|
||||
record,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
cache: ApolloCache<object>;
|
||||
recordGqlFields?: Record<string, any>;
|
||||
record: T;
|
||||
}) => {
|
||||
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||
@ -32,6 +34,7 @@ export const updateRecordFromCache = <T extends ObjectRecord>({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
computeReferences: true,
|
||||
recordGqlFields,
|
||||
},
|
||||
)}
|
||||
`;
|
||||
|
@ -1,14 +1,31 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const generateDepthOneRecordGqlFields = ({
|
||||
objectMetadataItem,
|
||||
record,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
record?: Record<string, any>;
|
||||
}) => {
|
||||
return objectMetadataItem.fields.reduce((acc, field) => {
|
||||
const gqlFieldsFromObjectMetadataItem = objectMetadataItem.fields.reduce(
|
||||
(acc, field) => {
|
||||
return {
|
||||
...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,
|
||||
objectRecordId,
|
||||
customUseUpdateOneObjectHook,
|
||||
overridenIsFieldEmpty,
|
||||
}: {
|
||||
clearable?: boolean;
|
||||
fieldMetadataName: string;
|
||||
@ -27,6 +28,7 @@ export const useFieldContext = ({
|
||||
objectNameSingular: string;
|
||||
objectRecordId: string;
|
||||
customUseUpdateOneObjectHook?: RecordUpdateHook;
|
||||
overridenIsFieldEmpty?: boolean;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -78,6 +80,7 @@ export const useFieldContext = ({
|
||||
customUseUpdateOneObjectHook ?? useUpdateOneObjectMutation,
|
||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||
clearable,
|
||||
overridenIsFieldEmpty,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@ -47,9 +47,11 @@ export const useUpdateOneRecord = <
|
||||
const updateOneRecord = async ({
|
||||
idToUpdate,
|
||||
updateOneRecordInput,
|
||||
optimisticRecord,
|
||||
}: {
|
||||
idToUpdate: string;
|
||||
updateOneRecordInput: Partial<Omit<UpdatedObjectRecord, 'id'>>;
|
||||
optimisticRecord?: Partial<ObjectRecord>;
|
||||
}) => {
|
||||
const sanitizedInput = {
|
||||
...sanitizeRecordInput({
|
||||
@ -68,16 +70,16 @@ export const useUpdateOneRecord = <
|
||||
computeReferences: true,
|
||||
});
|
||||
|
||||
const optimisticRecord = {
|
||||
const computedOptimisticRecord = {
|
||||
...cachedRecord,
|
||||
...sanitizedInput,
|
||||
...(optimisticRecord ?? sanitizedInput),
|
||||
...{ id: idToUpdate },
|
||||
...{ __typename: capitalize(objectMetadataItem.nameSingular) },
|
||||
};
|
||||
|
||||
const optimisticRecordWithConnection =
|
||||
getRecordNodeFromRecord<ObjectRecord>({
|
||||
record: optimisticRecord,
|
||||
record: computedOptimisticRecord,
|
||||
objectMetadataItem,
|
||||
objectMetadataItems,
|
||||
recordGqlFields: computedRecordGqlFields,
|
||||
@ -92,7 +94,7 @@ export const useUpdateOneRecord = <
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: optimisticRecord,
|
||||
record: computedOptimisticRecord,
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
|
@ -3,10 +3,13 @@ import { useContext } from 'react';
|
||||
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 { 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 { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||
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 { 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 { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay';
|
||||
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 { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay';
|
||||
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 { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
import { isFieldRawJson } from '../types/guards/isFieldRawJson';
|
||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
||||
import { isFieldSelect } from '../types/guards/isFieldSelect';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
import { isFieldUuid } from '../types/guards/isFieldUuid';
|
||||
@ -49,8 +51,10 @@ export const FieldDisplay = () => {
|
||||
|
||||
return isChipDisplay ? (
|
||||
<ChipFieldDisplay />
|
||||
) : isFieldRelation(fieldDefinition) ? (
|
||||
<RelationFieldDisplay />
|
||||
) : isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldDisplay />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldDisplay />
|
||||
) : isFieldPhone(fieldDefinition) ||
|
||||
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
||||
<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 { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput';
|
||||
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 { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope';
|
||||
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 { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||
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 { 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 { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput';
|
||||
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 { FieldInputEvent } from '../types/FieldInputEvent';
|
||||
import { isFieldAddress } from '../types/guards/isFieldAddress';
|
||||
@ -40,7 +41,6 @@ import { isFieldLink } from '../types/guards/isFieldLink';
|
||||
import { isFieldNumber } from '../types/guards/isFieldNumber';
|
||||
import { isFieldPhone } from '../types/guards/isFieldPhone';
|
||||
import { isFieldRating } from '../types/guards/isFieldRating';
|
||||
import { isFieldRelation } from '../types/guards/isFieldRelation';
|
||||
import { isFieldText } from '../types/guards/isFieldText';
|
||||
|
||||
type FieldInputProps = {
|
||||
@ -72,16 +72,10 @@ export const FieldInput = ({
|
||||
<RecordFieldInputScope
|
||||
recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)}
|
||||
>
|
||||
{isFieldRelation(fieldDefinition) ? (
|
||||
isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationManyFieldInput
|
||||
relationPickerScopeId={getScopeIdFromComponentId(
|
||||
`relation-picker-${fieldDefinition.fieldMetadataId}`,
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
)
|
||||
{isFieldRelationToOneObject(fieldDefinition) ? (
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
) : isFieldRelationFromManyObjects(fieldDefinition) ? (
|
||||
<RelationFromManyFieldInput onSubmit={onSubmit} />
|
||||
) : isFieldPhone(fieldDefinition) ||
|
||||
isFieldDisplayedAsPhone(fieldDefinition) ? (
|
||||
<PhoneFieldInput
|
||||
|
@ -30,6 +30,7 @@ export type GenericFieldContextType = {
|
||||
clearable?: boolean;
|
||||
maxWidth?: number;
|
||||
isCentered?: boolean;
|
||||
overridenIsFieldEmpty?: boolean;
|
||||
};
|
||||
|
||||
export const FieldContext = createContext<GenericFieldContextType>(
|
||||
|
@ -2,17 +2,22 @@ import { useContext } from 'react';
|
||||
|
||||
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
|
||||
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { FieldContext } from '../contexts/FieldContext';
|
||||
|
||||
export const useIsFieldEmpty = () => {
|
||||
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||
|
||||
const { entityId, fieldDefinition, overridenIsFieldEmpty } =
|
||||
useContext(FieldContext);
|
||||
const fieldValue = useRecordFieldValue(
|
||||
entityId,
|
||||
fieldDefinition.metadata.fieldName,
|
||||
fieldDefinition?.metadata?.fieldName ?? '',
|
||||
);
|
||||
|
||||
if (isDefined(overridenIsFieldEmpty)) {
|
||||
return overridenIsFieldEmpty;
|
||||
}
|
||||
|
||||
return isFieldValueEmpty({
|
||||
fieldDefinition,
|
||||
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 { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||
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 { isFieldSelectValue } from '@/object-record/record-field/types/guards/isFieldSelectValue';
|
||||
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 { isFieldRating } from '../types/guards/isFieldRating';
|
||||
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 { isFieldTextValue } from '../types/guards/isFieldTextValue';
|
||||
|
||||
@ -55,14 +54,10 @@ export const usePersistField = () => {
|
||||
const persistField = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(valueToPersist: unknown) => {
|
||||
const fieldIsRelation =
|
||||
isFieldRelation(fieldDefinition) &&
|
||||
isFieldRelationValue(valueToPersist);
|
||||
|
||||
const fieldIsRelationFromManyObjects =
|
||||
isFieldRelationFromManyObjects(
|
||||
const fieldIsRelationToOneObject =
|
||||
isFieldRelationToOneObject(
|
||||
fieldDefinition as FieldDefinition<FieldRelationMetadata>,
|
||||
) && isFieldRelationValue(valueToPersist);
|
||||
) && isFieldRelationToOneValue(valueToPersist);
|
||||
|
||||
const fieldIsText =
|
||||
isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist);
|
||||
@ -120,7 +115,7 @@ export const usePersistField = () => {
|
||||
isFieldRawJsonValue(valueToPersist);
|
||||
|
||||
const isValuePersistable =
|
||||
(fieldIsRelation && !fieldIsRelationFromManyObjects) ||
|
||||
fieldIsRelationToOneObject ||
|
||||
fieldIsText ||
|
||||
fieldIsBoolean ||
|
||||
fieldIsEmail ||
|
||||
@ -145,7 +140,7 @@ export const usePersistField = () => {
|
||||
valueToPersist,
|
||||
);
|
||||
|
||||
if (fieldIsRelation && !fieldIsRelationFromManyObjects) {
|
||||
if (fieldIsRelationToOneObject) {
|
||||
const value = valueToPersist as EntityForSelect;
|
||||
updateRecord?.({
|
||||
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 { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
|
||||
import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||
|
||||
export const RelationFromManyFieldDisplay = ({
|
||||
fieldValue,
|
||||
}: {
|
||||
fieldValue: ObjectRecord[];
|
||||
}) => {
|
||||
export const RelationFromManyFieldDisplay = () => {
|
||||
const { fieldValue, fieldDefinition, generateRecordChipData } =
|
||||
useRelationFromManyFieldDisplay();
|
||||
const { isFocused } = useFieldFocus();
|
||||
const { generateRecordChipData } = useRelationFieldDisplay();
|
||||
|
||||
if (
|
||||
!fieldValue ||
|
||||
!fieldDefinition?.metadata.relationObjectMetadataNameSingular
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const recordChipsData = fieldValue.map((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 {
|
||||
RecordFieldValueSelectorContextProvider,
|
||||
useSetRecordValue,
|
||||
useSetRecordFieldValue,
|
||||
} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||
@ -18,6 +18,7 @@ import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
|
||||
import {
|
||||
fieldValue,
|
||||
otherPersonMock,
|
||||
relationFromManyFieldDisplayMock,
|
||||
} from './relationFromManyFieldDisplayMock';
|
||||
|
||||
@ -30,21 +31,26 @@ const RelationFieldValueSetterEffect = () => {
|
||||
recordStoreFamilyState(relationFromManyFieldDisplayMock.relationEntityId),
|
||||
);
|
||||
|
||||
const setRecordValue = useSetRecordValue();
|
||||
const setRecordFieldValue = useSetRecordFieldValue();
|
||||
|
||||
useEffect(() => {
|
||||
setEntity(relationFromManyFieldDisplayMock.entityValue);
|
||||
setRelationEntity(relationFromManyFieldDisplayMock.relationFieldValue);
|
||||
|
||||
setRecordValue(
|
||||
setRecordFieldValue(
|
||||
relationFromManyFieldDisplayMock.entityValue.id,
|
||||
relationFromManyFieldDisplayMock.entityValue,
|
||||
'company',
|
||||
[relationFromManyFieldDisplayMock.entityValue],
|
||||
);
|
||||
setRecordValue(
|
||||
setRecordFieldValue(otherPersonMock.entityValue.id, 'company', [
|
||||
relationFromManyFieldDisplayMock.entityValue,
|
||||
]);
|
||||
setRecordFieldValue(
|
||||
relationFromManyFieldDisplayMock.relationFieldValue.id,
|
||||
'company',
|
||||
relationFromManyFieldDisplayMock.relationFieldValue,
|
||||
);
|
||||
}, [setEntity, setRelationEntity, setRecordValue]);
|
||||
}, [setEntity, setRelationEntity, setRecordFieldValue]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
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 { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
@ -15,7 +15,7 @@ const meta: Meta = {
|
||||
getFieldDecorator('person', 'company'),
|
||||
ComponentDecorator,
|
||||
],
|
||||
component: RelationFieldDisplay,
|
||||
component: RelationToOneFieldDisplay,
|
||||
args: {},
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
@ -24,7 +24,7 @@ const meta: Meta = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RelationFieldDisplay>;
|
||||
type Story = StoryObj<typeof RelationToOneFieldDisplay>;
|
||||
|
||||
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 = {
|
||||
entityId: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025',
|
||||
@ -67,11 +121,7 @@ export const relationFromManyFieldDisplayMock = {
|
||||
asd: '',
|
||||
city: 'Seattle',
|
||||
jobTitle: '',
|
||||
name: {
|
||||
__typename: 'FullName',
|
||||
firstName: 'Lorie',
|
||||
lastName: 'Vladim',
|
||||
},
|
||||
name: 'Lorie Vladim',
|
||||
createdAt: '2024-05-01T13:16:29.046Z',
|
||||
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 { isFieldRelation } from '../../types/guards/isFieldRelation';
|
||||
|
||||
export const useRelationFieldDisplay = () => {
|
||||
export const useRelationToOneFieldDisplay = () => {
|
||||
const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext);
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
export type RelationFieldInputProps = {
|
||||
export type RelationToOneFieldInputProps = {
|
||||
onSubmit?: FieldInputEvent;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export const RelationFieldInput = ({
|
||||
export const RelationToOneFieldInput = ({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: RelationFieldInputProps) => {
|
||||
}: RelationToOneFieldInputProps) => {
|
||||
const { fieldDefinition, initialSearchValue, fieldValue } =
|
||||
useRelationField<EntityForSelect>();
|
||||
|
@ -5,7 +5,7 @@ import { useSetRecoilState } from 'recoil';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
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 { FieldMetadataType } from '~/generated/graphql';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
@ -60,7 +60,7 @@ const RelationManyFieldInputWithContext = () => {
|
||||
entityId={'entityId'}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationManyFieldInput />
|
||||
<RelationFromManyFieldInput />
|
||||
</FieldContextProvider>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@ import { useSetRecoilState } from 'recoil';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
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 { FieldMetadataType } from '~/generated/graphql';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
@ -26,9 +27,9 @@ import {
|
||||
|
||||
import { FieldContextProvider } from '../../../__stories__/FieldContextProvider';
|
||||
import {
|
||||
RelationFieldInput,
|
||||
RelationFieldInputProps,
|
||||
} from '../RelationFieldInput';
|
||||
RelationToOneFieldInput,
|
||||
RelationToOneFieldInputProps,
|
||||
} from '../RelationToOneFieldInput';
|
||||
|
||||
const RelationWorkspaceSetterEffect = () => {
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
@ -44,16 +45,16 @@ const RelationWorkspaceSetterEffect = () => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
type RelationFieldInputWithContextProps = RelationFieldInputProps & {
|
||||
type RelationToOneFieldInputWithContextProps = RelationToOneFieldInputProps & {
|
||||
value: number;
|
||||
entityId?: string;
|
||||
};
|
||||
|
||||
const RelationFieldInputWithContext = ({
|
||||
const RelationToOneFieldInputWithContext = ({
|
||||
entityId,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: RelationFieldInputWithContextProps) => {
|
||||
}: RelationToOneFieldInputWithContextProps) => {
|
||||
const setHotKeyScope = useSetHotkeyScope();
|
||||
|
||||
useEffect(() => {
|
||||
@ -78,9 +79,13 @@ const RelationFieldInputWithContext = ({
|
||||
},
|
||||
}}
|
||||
entityId={entityId}
|
||||
>
|
||||
<RelationPickerScope
|
||||
relationPickerScopeId={'relation-to-one-field-input'}
|
||||
>
|
||||
<RelationWorkspaceSetterEffect />
|
||||
<RelationFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
<RelationToOneFieldInput onSubmit={onSubmit} onCancel={onCancel} />
|
||||
</RelationPickerScope>
|
||||
</FieldContextProvider>
|
||||
<div data-testid="data-field-input-click-outside-div" />
|
||||
</div>
|
||||
@ -99,8 +104,8 @@ const clearMocksDecorator: Decorator = (Story, context) => {
|
||||
};
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Data/Field/Input/RelationFieldInput',
|
||||
component: RelationFieldInputWithContext,
|
||||
title: 'UI/Data/Field/Input/RelationToOneFieldInput',
|
||||
component: RelationToOneFieldInputWithContext,
|
||||
args: {
|
||||
useEditButton: true,
|
||||
onSubmit: submitJestFn,
|
||||
@ -123,7 +128,7 @@ const meta: Meta = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RelationFieldInputWithContext>;
|
||||
type Story = StoryObj<typeof RelationToOneFieldInputWithContext>;
|
||||
|
||||
export const Default: Story = {
|
||||
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,
|
||||
FieldPhoneValue,
|
||||
FieldRatingValue,
|
||||
FieldRelationValue,
|
||||
FieldRelationFromManyValue,
|
||||
FieldRelationToOneValue,
|
||||
FieldSelectValue,
|
||||
FieldTextValue,
|
||||
FieldUUidValue,
|
||||
} from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
|
||||
export type FieldTextDraftValue = string;
|
||||
export type FieldNumberDraftValue = string;
|
||||
@ -28,6 +28,7 @@ export type FieldEmailDraftValue = string;
|
||||
export type FieldSelectDraftValue = string;
|
||||
export type FieldMultiSelectDraftValue = string[];
|
||||
export type FieldRelationDraftValue = string;
|
||||
export type FieldRelationManyDraftValue = string[];
|
||||
export type FieldLinkDraftValue = { url: string; label: string };
|
||||
export type FieldLinksDraftValue = {
|
||||
primaryLinkLabel: string;
|
||||
@ -79,10 +80,10 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
||||
? FieldSelectDraftValue
|
||||
: FieldValue extends FieldMultiSelectValue
|
||||
? FieldMultiSelectDraftValue
|
||||
: FieldValue extends
|
||||
| FieldRelationValue<EntityForSelect>
|
||||
| FieldRelationValue<EntityForSelect[]>
|
||||
: FieldValue extends FieldRelationToOneValue
|
||||
? FieldRelationDraftValue
|
||||
: FieldValue extends FieldRelationFromManyValue
|
||||
? FieldRelationManyDraftValue
|
||||
: FieldValue extends FieldAddressValue
|
||||
? FieldAddressDraftValue
|
||||
: FieldValue extends FieldJsonValue
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { ThemeColor } from 'twenty-ui';
|
||||
|
||||
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 { WithNarrowedStringLiteralProperty } from '~/types/WithNarrowedStringLiteralProperty';
|
||||
|
||||
import { CurrencyCode } from './CurrencyCode';
|
||||
|
||||
@ -110,6 +112,18 @@ export type FieldRelationMetadata = {
|
||||
useEditButton?: boolean;
|
||||
};
|
||||
|
||||
export type FieldRelationOneMetadata = WithNarrowedStringLiteralProperty<
|
||||
FieldRelationMetadata,
|
||||
'relationType',
|
||||
'TO_ONE_OBJECT'
|
||||
>;
|
||||
|
||||
export type FieldRelationManyMetadata = WithNarrowedStringLiteralProperty<
|
||||
FieldRelationMetadata,
|
||||
'relationType',
|
||||
'FROM_MANY_OBJECTS'
|
||||
>;
|
||||
|
||||
export type FieldSelectMetadata = {
|
||||
objectMetadataNameSingular?: string;
|
||||
fieldName: string;
|
||||
@ -174,10 +188,13 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number];
|
||||
export type FieldSelectValue = string | null;
|
||||
export type FieldMultiSelectValue = string[] | null;
|
||||
|
||||
export type FieldRelationValue<T extends EntityForSelect | EntityForSelect[]> =
|
||||
T | null;
|
||||
export type FieldRelationToOneValue = EntityForSelect | null;
|
||||
|
||||
// See https://zod.dev/?id=json-type
|
||||
type Literal = string | number | boolean | null;
|
||||
export type Json = Literal | { [key: string]: Json } | Json[];
|
||||
export type FieldRelationFromManyValue = EntityForSelect[] | [];
|
||||
|
||||
export type FieldRelationValue<
|
||||
T extends FieldRelationToOneValue | FieldRelationFromManyValue,
|
||||
> = T;
|
||||
|
||||
export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[];
|
||||
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 { FieldRelationMetadata } from '../FieldMetadata';
|
||||
import { FieldMetadata, FieldRelationManyMetadata } from '../FieldMetadata';
|
||||
|
||||
export const isFieldRelationFromManyObjects = (
|
||||
field: Pick<FieldDefinition<FieldRelationMetadata>, 'type' | 'metadata'>,
|
||||
): field is FieldDefinition<FieldRelationMetadata> =>
|
||||
field.type === FieldMetadataType.Relation &&
|
||||
field.metadata.relationType === 'FROM_MANY_OBJECTS';
|
||||
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
|
||||
): field is FieldDefinition<FieldRelationManyMetadata> =>
|
||||
isFieldRelation(field) && 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 { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||
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';
|
||||
|
||||
type UseLoadRecordIndexBoardProps = {
|
||||
@ -33,7 +33,7 @@ export const useLoadRecordIndexBoard = ({
|
||||
setFieldDefinitions,
|
||||
isCompactModeActiveState,
|
||||
} = useRecordBoard(recordBoardId);
|
||||
const { setRecords: setRecordsInStore } = useSetRecordInStore();
|
||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const recordIndexFieldDefinitions = useRecoilValue(
|
||||
recordIndexFieldDefinitionsState,
|
||||
@ -82,8 +82,8 @@ export const useLoadRecordIndexBoard = ({
|
||||
}, [records, setRecordIdsInBoard]);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordsInStore(records);
|
||||
}, [records, setRecordsInStore]);
|
||||
upsertRecordsInStore(records);
|
||||
}, [records, upsertRecordsInStore]);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordCountInCurrentView(totalCount);
|
||||
|
@ -9,7 +9,7 @@ import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-
|
||||
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
|
||||
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
|
||||
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 = {
|
||||
objectNameSingular: string;
|
||||
@ -30,7 +30,7 @@ export const useLoadRecordIndexBoardColumn = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
const { setRecordIdsForColumn } = useRecordBoard(recordBoardId);
|
||||
const { setRecords: setRecordsInStore } = useSetRecordInStore();
|
||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
|
||||
const recordIndexSorts = useRecoilValue(recordIndexSortsState);
|
||||
@ -75,8 +75,8 @@ export const useLoadRecordIndexBoardColumn = ({
|
||||
}, [records, setRecordIdsForColumn, columnId]);
|
||||
|
||||
useEffect(() => {
|
||||
setRecordsInStore(records);
|
||||
}, [records, setRecordsInStore]);
|
||||
upsertRecordsInStore(records);
|
||||
}, [records, upsertRecordsInStore]);
|
||||
|
||||
return {
|
||||
records,
|
||||
|
@ -6,7 +6,6 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { FieldFocusContextProvider } from '@/object-record/record-field/contexts/FieldFocusContextProvider';
|
||||
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 { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
@ -26,11 +25,8 @@ export const RecordInlineCell = ({
|
||||
loading,
|
||||
}: RecordInlineCellProps) => {
|
||||
const { fieldDefinition, entityId } = useContext(FieldContext);
|
||||
|
||||
const buttonIcon = useGetButtonIcon();
|
||||
|
||||
const isFieldEmpty = useIsFieldEmpty();
|
||||
|
||||
const isFieldInputOnly = useIsFieldInputOnly();
|
||||
|
||||
const { closeInlineCell } = useInlineCell();
|
||||
@ -104,7 +100,6 @@ export const RecordInlineCell = ({
|
||||
/>
|
||||
}
|
||||
displayModeContent={<FieldDisplay />}
|
||||
isDisplayModeContentEmpty={isFieldEmpty}
|
||||
isDisplayModeFixHeight
|
||||
editModeContentOnly={isFieldInputOnly}
|
||||
loading={loading}
|
||||
|
@ -69,7 +69,6 @@ export type RecordInlineCellContainerProps = {
|
||||
editModeContentOnly?: boolean;
|
||||
displayModeContent: ReactElement;
|
||||
customEditHotkeyScope?: HotkeyScope;
|
||||
isDisplayModeContentEmpty?: boolean;
|
||||
isDisplayModeFixHeight?: boolean;
|
||||
disableHoverEffect?: boolean;
|
||||
loading?: boolean;
|
||||
@ -85,7 +84,6 @@ export const RecordInlineCellContainer = ({
|
||||
editModeContent,
|
||||
displayModeContent,
|
||||
customEditHotkeyScope,
|
||||
isDisplayModeContentEmpty,
|
||||
editModeContentOnly,
|
||||
isDisplayModeFixHeight,
|
||||
disableHoverEffect,
|
||||
@ -149,7 +147,6 @@ export const RecordInlineCellContainer = ({
|
||||
disableHoverEffect,
|
||||
editModeContent,
|
||||
editModeContentOnly,
|
||||
isDisplayModeContentEmpty,
|
||||
isDisplayModeFixHeight,
|
||||
buttonIcon,
|
||||
label,
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { css } from '@emotion/react';
|
||||
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<
|
||||
Pick<
|
||||
RecordInlineCellDisplayModeProps,
|
||||
| 'isDisplayModeContentEmpty'
|
||||
| 'disableHoverEffect'
|
||||
| 'isDisplayModeFixHeight'
|
||||
| 'isHovered'
|
||||
'disableHoverEffect' | 'isDisplayModeFixHeight' | 'isHovered'
|
||||
>
|
||||
>`
|
||||
align-items: center;
|
||||
@ -51,23 +53,32 @@ const StyledEmptyField = styled.div`
|
||||
`;
|
||||
|
||||
type RecordInlineCellDisplayModeProps = {
|
||||
isDisplayModeContentEmpty?: boolean;
|
||||
disableHoverEffect?: boolean;
|
||||
isDisplayModeFixHeight?: boolean;
|
||||
isHovered?: boolean;
|
||||
emptyPlaceholder?: string;
|
||||
};
|
||||
} & Pick<RecordInlineCellContainerProps, 'buttonIcon' | 'editModeContentOnly'>;
|
||||
|
||||
export const RecordInlineCellDisplayMode = ({
|
||||
children,
|
||||
isDisplayModeContentEmpty,
|
||||
disableHoverEffect,
|
||||
isDisplayModeFixHeight,
|
||||
emptyPlaceholder = 'Empty',
|
||||
isHovered,
|
||||
}: React.PropsWithChildren<RecordInlineCellDisplayModeProps>) => (
|
||||
buttonIcon,
|
||||
editModeContentOnly,
|
||||
}: React.PropsWithChildren<RecordInlineCellDisplayModeProps>) => {
|
||||
const { isFocused } = useFieldFocus();
|
||||
const isDisplayModeContentEmpty = useIsFieldEmpty();
|
||||
const showEditButton =
|
||||
buttonIcon &&
|
||||
isFocused &&
|
||||
!isDisplayModeContentEmpty &&
|
||||
!editModeContentOnly;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledRecordInlineCellNormalModeOuterContainer
|
||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||
disableHoverEffect={disableHoverEffect}
|
||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||
isHovered={isHovered}
|
||||
@ -80,4 +91,7 @@ export const RecordInlineCellDisplayMode = ({
|
||||
)}
|
||||
</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 { RecordInlineCellContainerProps } from '@/object-record/record-inline-cell/components/RecordInlineCellContainer';
|
||||
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 { RecordInlineCellSkeletonLoader } from '@/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader';
|
||||
import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell';
|
||||
@ -27,7 +26,6 @@ type RecordInlineCellValueProps = Pick<
|
||||
| 'customEditHotkeyScope'
|
||||
| 'editModeContent'
|
||||
| 'editModeContentOnly'
|
||||
| 'isDisplayModeContentEmpty'
|
||||
| 'isDisplayModeFixHeight'
|
||||
| 'disableHoverEffect'
|
||||
| 'readonly'
|
||||
@ -43,7 +41,6 @@ export const RecordInlineCellValue = ({
|
||||
disableHoverEffect,
|
||||
editModeContent,
|
||||
editModeContentOnly,
|
||||
isDisplayModeContentEmpty,
|
||||
isDisplayModeFixHeight,
|
||||
readonly,
|
||||
buttonIcon,
|
||||
@ -61,13 +58,6 @@ export const RecordInlineCellValue = ({
|
||||
}
|
||||
};
|
||||
|
||||
const showEditButton =
|
||||
buttonIcon &&
|
||||
!isInlineCellInEditMode &&
|
||||
isFocused &&
|
||||
!editModeContentOnly &&
|
||||
!isDisplayModeContentEmpty;
|
||||
|
||||
if (loading === true) {
|
||||
return <RecordInlineCellSkeletonLoader />;
|
||||
}
|
||||
@ -81,7 +71,6 @@ export const RecordInlineCellValue = ({
|
||||
<StyledClickableContainer readonly={readonly}>
|
||||
<RecordInlineCellDisplayMode
|
||||
disableHoverEffect={disableHoverEffect}
|
||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||
isHovered={isFocused}
|
||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||
@ -96,14 +85,14 @@ export const RecordInlineCellValue = ({
|
||||
>
|
||||
<RecordInlineCellDisplayMode
|
||||
disableHoverEffect={disableHoverEffect}
|
||||
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
|
||||
isDisplayModeFixHeight={isDisplayModeFixHeight}
|
||||
isHovered={isFocused}
|
||||
emptyPlaceholder={showLabel ? 'Empty' : label}
|
||||
buttonIcon={buttonIcon}
|
||||
editModeContentOnly={editModeContentOnly}
|
||||
>
|
||||
{displayModeContent}
|
||||
</RecordInlineCellDisplayMode>
|
||||
{showEditButton && <RecordInlineCellButton Icon={buttonIcon} />}
|
||||
</StyledClickableContainer>
|
||||
)}
|
||||
</>
|
||||
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
|
||||
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 { RecordDetailRelationRecordsListItemEffect } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItemEffect';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
type RecordDetailRelationRecordsListProps = {
|
||||
@ -19,12 +20,18 @@ export const RecordDetailRelationRecordsList = ({
|
||||
return (
|
||||
<RecordDetailRecordsList>
|
||||
{relationRecords.slice(0, 5).map((relationRecord) => (
|
||||
<>
|
||||
<RecordDetailRelationRecordsListItemEffect
|
||||
key={`${relationRecord.id}-effect`}
|
||||
relationRecordId={relationRecord.id}
|
||||
/>
|
||||
<RecordDetailRelationRecordsListItem
|
||||
key={relationRecord.id}
|
||||
isExpanded={expandedItem === relationRecord.id}
|
||||
onClick={handleItemClick}
|
||||
relationRecord={relationRecord}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
</RecordDetailRecordsList>
|
||||
);
|
||||
|
@ -15,7 +15,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useLazyFindOneRecord } from '@/object-record/hooks/useLazyFindOneRecord';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import {
|
||||
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 { RecordDetailRecordsListItem } from '@/object-record/record-show/record-detail-section/components/RecordDetailRecordsListItem';
|
||||
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 { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
@ -99,12 +97,6 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
|
||||
const persistField = usePersistField();
|
||||
|
||||
const {
|
||||
called: hasFetchedRelationRecord,
|
||||
findOneRecord: findOneRelationRecord,
|
||||
} = useLazyFindOneRecord({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
const { updateOneRecord: updateOneRelationRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
@ -168,8 +160,6 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
return [updateEntity, { loading: false }];
|
||||
};
|
||||
|
||||
const { setRecords } = useSetRecordInStore();
|
||||
|
||||
const handleClick = () => onClick(relationRecord.id);
|
||||
|
||||
const AnimatedIconChevronDown = useCallback<IconComponent>(
|
||||
@ -194,16 +184,7 @@ export const RecordDetailRelationRecordsListItem = ({
|
||||
record={relationRecord}
|
||||
objectNameSingular={relationObjectMetadataItem.nameSingular}
|
||||
/>
|
||||
<StyledClickableZone
|
||||
onClick={handleClick}
|
||||
onMouseOver={() =>
|
||||
!hasFetchedRelationRecord &&
|
||||
findOneRelationRecord({
|
||||
objectRecordId: relationRecord.id,
|
||||
onCompleted: (record) => setRecords([record]),
|
||||
})
|
||||
}
|
||||
>
|
||||
<StyledClickableZone onClick={handleClick}>
|
||||
<LightIconButton
|
||||
className="displayOnHover"
|
||||
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 { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const useSetRecordInStore = () => {
|
||||
const setRecords = useRecoilCallback(
|
||||
export const useUpsertRecordsInStore = () => {
|
||||
const upsertRecords = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(records: ObjectRecord[]) => {
|
||||
for (const record of records) {
|
||||
@ -21,6 +21,6 @@ export const useSetRecordInStore = () => {
|
||||
);
|
||||
|
||||
return {
|
||||
setRecords,
|
||||
upsertRecords,
|
||||
};
|
||||
};
|
@ -55,7 +55,8 @@ export const RecordTableHeader = ({
|
||||
}: {
|
||||
createRecord: () => void;
|
||||
}) => {
|
||||
const { visibleTableColumnsSelector } = useRecordTableStates();
|
||||
const { visibleTableColumnsSelector, hiddenTableColumnsSelector } =
|
||||
useRecordTableStates();
|
||||
|
||||
const scrollWrapper = useScrollWrapperScopedRef();
|
||||
const isTableWiderThanScreen =
|
||||
@ -63,7 +64,7 @@ export const RecordTableHeader = ({
|
||||
(scrollWrapper.current?.scrollWidth ?? 0);
|
||||
|
||||
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
||||
const hiddenTableColumns = useRecoilValue(visibleTableColumnsSelector());
|
||||
const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector());
|
||||
|
||||
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 { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates';
|
||||
import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect';
|
||||
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 { 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 { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
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 { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
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)`
|
||||
height: 100%;
|
||||
@ -24,134 +26,80 @@ export const StyledSelectableItem = styled(SelectableItem)`
|
||||
export const MultiRecordSelect = ({
|
||||
onChange,
|
||||
onSubmit,
|
||||
selectedObjectRecords,
|
||||
allRecords,
|
||||
loading,
|
||||
searchFilter,
|
||||
setSearchFilter,
|
||||
}: {
|
||||
onChange?: (
|
||||
changedRecordForSelect: ObjectRecordForSelect,
|
||||
newSelectedValue: boolean,
|
||||
) => void;
|
||||
onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void;
|
||||
selectedObjectRecords: ObjectRecordForSelect[];
|
||||
allRecords: ObjectRecordForSelect[];
|
||||
loading: boolean;
|
||||
searchFilter: string;
|
||||
setSearchFilter: (searchFilter: string) => void;
|
||||
onChange?: (changedRecordForSelectId: string) => void;
|
||||
onSubmit?: () => void;
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [internalSelectedRecords, setInternalSelectedRecords] = useState<
|
||||
ObjectRecordForSelect[]
|
||||
>([]);
|
||||
const relationPickerScopedId = useAvailableScopeIdOrThrow(
|
||||
RelationPickerScopeInternalContext,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
setInternalSelectedRecords(selectedObjectRecords);
|
||||
}
|
||||
}, [selectedObjectRecords, loading]);
|
||||
const { objectRecordsIdsMultiSelectState, recordMultiSelectIsLoadingState } =
|
||||
useObjectRecordMultiSelectScopedStates(relationPickerScopedId);
|
||||
|
||||
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, {
|
||||
leading: true,
|
||||
});
|
||||
|
||||
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleFilterChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
debouncedSetSearchFilter(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleSelectChange = (
|
||||
changedRecordForSelect: ObjectRecordForSelect,
|
||||
newSelectedValue: boolean,
|
||||
) => {
|
||||
const newSelectedRecords = newSelectedValue
|
||||
? [...internalSelectedRecords, changedRecordForSelect]
|
||||
: internalSelectedRecords.filter(
|
||||
(selectedRecord) =>
|
||||
selectedRecord.record.id !== changedRecordForSelect.record.id,
|
||||
},
|
||||
[debouncedSetSearchFilter],
|
||||
);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<MultipleObjectRecordOnClickOutsideEffect
|
||||
containerRef={containerRef}
|
||||
onClickOutside={() => {
|
||||
onSubmit?.(internalSelectedRecords);
|
||||
onSubmit?.();
|
||||
}}
|
||||
/>
|
||||
<DropdownMenu ref={containerRef} data-select-disable>
|
||||
<DropdownMenuSearchInput
|
||||
value={searchFilter}
|
||||
value={relationPickerSearchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
{loading ? (
|
||||
{recordMultiSelectIsLoading ? (
|
||||
<MenuItem text="Loading..." />
|
||||
) : (
|
||||
<>
|
||||
<SelectableList
|
||||
selectableListId={MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID}
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
selectableItemIdArray={objectRecordsIdsMultiSelect}
|
||||
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) => (
|
||||
<MultipleObjectRecordSelectItem
|
||||
key={objectRecordForSelect.record.id}
|
||||
objectRecordForSelect={objectRecordForSelect}
|
||||
onSelectedChange={(newSelectedValue) =>
|
||||
handleSelectChange(
|
||||
objectRecordForSelect,
|
||||
newSelectedValue,
|
||||
)
|
||||
}
|
||||
selected={internalSelectedRecords?.some(
|
||||
(selectedRecord) => {
|
||||
{objectRecordsIdsMultiSelect?.map((recordId) => {
|
||||
return (
|
||||
selectedRecord.record.id ===
|
||||
objectRecordForSelect.record.id
|
||||
);
|
||||
},
|
||||
)}
|
||||
<MultipleObjectRecordSelectItem
|
||||
key={recordId}
|
||||
objectRecordId={recordId}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</SelectableList>
|
||||
{entitiesInDropdown?.length === 0 && (
|
||||
{objectRecordsIdsMultiSelect?.length === 0 && (
|
||||
<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 { 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 { 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 { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const StyledSelectableItem = styled(SelectableItem)`
|
||||
height: 100%;
|
||||
@ -16,45 +19,60 @@ export const StyledSelectableItem = styled(SelectableItem)`
|
||||
`;
|
||||
|
||||
export const MultipleObjectRecordSelectItem = ({
|
||||
objectRecordForSelect,
|
||||
onSelectedChange,
|
||||
selected,
|
||||
objectRecordId,
|
||||
onChange,
|
||||
}: {
|
||||
objectRecordForSelect: ObjectRecordForSelect;
|
||||
onSelectedChange?: (selected: boolean) => void;
|
||||
selected: boolean;
|
||||
objectRecordId: string;
|
||||
onChange?: (changedRecordForSelectId: string) => void;
|
||||
}) => {
|
||||
const { isSelectedItemIdSelector } = useSelectableList(
|
||||
MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID,
|
||||
);
|
||||
|
||||
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 (
|
||||
<StyledSelectableItem
|
||||
itemId={objectRecordForSelect.record.id}
|
||||
key={objectRecordForSelect.record.id + v4()}
|
||||
>
|
||||
<StyledSelectableItem itemId={objectRecordId} key={objectRecordId + v4()}>
|
||||
<MenuItemMultiSelectAvatar
|
||||
selected={selected}
|
||||
onSelectChange={onSelectedChange}
|
||||
onSelectChange={(_isNewlySelectedValue) => handleSelectChange()}
|
||||
isKeySelected={isSelectedByKeyboard}
|
||||
selected={selected}
|
||||
avatar={
|
||||
<Avatar
|
||||
avatarUrl={getImageAbsoluteURIOrBase64(
|
||||
objectRecordForSelect.recordIdentifier.avatarUrl,
|
||||
)}
|
||||
entityId={objectRecordForSelect.record.id}
|
||||
placeholder={objectRecordForSelect.recordIdentifier.name}
|
||||
avatarUrl={getImageAbsoluteURIOrBase64(recordIdentifier.avatarUrl)}
|
||||
entityId={objectRecordId}
|
||||
placeholder={recordIdentifier.name}
|
||||
size="md"
|
||||
type={
|
||||
objectRecordForSelect.recordIdentifier.avatarType ?? 'rounded'
|
||||
}
|
||||
type={recordIdentifier.avatarType ?? 'rounded'}
|
||||
/>
|
||||
}
|
||||
text={objectRecordForSelect.recordIdentifier.name}
|
||||
text={recordIdentifier.name}
|
||||
/>
|
||||
</StyledSelectableItem>
|
||||
);
|
||||
|
@ -44,7 +44,6 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
const { entities, relationPickerSearchFilter } =
|
||||
useRelationPickerEntitiesOptions({
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId,
|
||||
selectedRelationRecordIds,
|
||||
excludedRelationRecordIds,
|
||||
});
|
||||
|
@ -0,0 +1,5 @@
|
||||
export const TABLE_COLUMNS_DENY_LIST = [
|
||||
'attachments',
|
||||
'activities',
|
||||
'timelineActivities',
|
||||
];
|
@ -1,22 +1,26 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
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 { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
|
||||
export const useRelationPickerEntitiesOptions = ({
|
||||
relationObjectNameSingular,
|
||||
relationPickerScopeId = 'relation-picker',
|
||||
selectedRelationRecordIds = [],
|
||||
excludedRelationRecordIds = [],
|
||||
}: {
|
||||
relationObjectNameSingular: string;
|
||||
relationPickerScopeId?: string;
|
||||
selectedRelationRecordIds?: string[];
|
||||
excludedRelationRecordIds?: string[];
|
||||
}) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
RelationPickerScopeInternalContext,
|
||||
);
|
||||
|
||||
const { searchQueryState, relationPickerSearchFilterState } =
|
||||
useRelationPickerScopedStates({
|
||||
relationPickerScopedId: relationPickerScopeId,
|
||||
relationPickerScopedId: scopeId,
|
||||
});
|
||||
const relationPickerSearchFilter = useRecoilValue(
|
||||
relationPickerSearchFilterState,
|
||||
|
@ -1,17 +1,23 @@
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { TABLE_COLUMNS_DENY_LIST } from '@/object-record/relation-picker/constants/TableColumnsDenyList';
|
||||
|
||||
export const filterAvailableTableColumns = (
|
||||
columnDefinition: ColumnDefinition<FieldMetadata>,
|
||||
): boolean => {
|
||||
if (
|
||||
isFieldRelation(columnDefinition) &&
|
||||
columnDefinition.metadata?.relationType !== 'TO_ONE_OBJECT'
|
||||
columnDefinition.metadata?.relationType !== 'TO_ONE_OBJECT' &&
|
||||
columnDefinition.metadata?.relationType !== 'FROM_MANY_OBJECTS'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TABLE_COLUMNS_DENY_LIST.includes(columnDefinition.metadata.fieldName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (columnDefinition.type === 'UUID') {
|
||||
return false;
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ import { isString } from '@sniptt/guards';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { isFieldRelationToOneValue } from '@/object-record/record-field/types/guards/isFieldRelationToOneValue';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -31,7 +30,7 @@ export const sanitizeRecordInput = ({
|
||||
|
||||
if (
|
||||
fieldMetadataItem.type === FieldMetadataType.Relation &&
|
||||
isFieldRelationValue<EntityForSelect>(fieldValue)
|
||||
isFieldRelationToOneValue(fieldValue)
|
||||
) {
|
||||
const relationIdFieldName = `${fieldMetadataItem.name}Id`;
|
||||
const relationIdFieldMetadataItem = objectMetadataItem.fields.find(
|
||||
|
@ -1,6 +1,6 @@
|
||||
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';
|
||||
|
||||
type SettingsDataModelSetRecordEffectProps = {
|
||||
@ -10,11 +10,11 @@ type SettingsDataModelSetRecordEffectProps = {
|
||||
export const SettingsDataModelSetRecordEffect = ({
|
||||
record,
|
||||
}: SettingsDataModelSetRecordEffectProps) => {
|
||||
const { setRecords: setRecordsInStore } = useSetRecordInStore();
|
||||
const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore();
|
||||
|
||||
useEffect(() => {
|
||||
setRecordsInStore([record]);
|
||||
}, [record, setRecordsInStore]);
|
||||
upsertRecordsInStore([record]);
|
||||
}, [record, upsertRecordsInStore]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -64,7 +64,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
||||
type: FieldMetadataType.Relation,
|
||||
metadata: {
|
||||
fieldName: 'favorites',
|
||||
placeHolder: 'Favorites',
|
||||
relationType: 'FROM_MANY_OBJECTS',
|
||||
relationObjectMetadataNameSingular: '',
|
||||
relationObjectMetadataNamePlural: '',
|
||||
@ -99,7 +98,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
||||
type: FieldMetadataType.Relation,
|
||||
metadata: {
|
||||
fieldName: 'accountOwner',
|
||||
placeHolder: 'Account Owner',
|
||||
relationType: 'TO_ONE_OBJECT',
|
||||
relationObjectMetadataNameSingular: 'workspaceMember',
|
||||
relationObjectMetadataNamePlural: 'workspaceMembers',
|
||||
@ -117,7 +115,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
||||
type: FieldMetadataType.Relation,
|
||||
metadata: {
|
||||
fieldName: 'people',
|
||||
placeHolder: 'People',
|
||||
relationType: 'FROM_MANY_OBJECTS',
|
||||
relationObjectMetadataNameSingular: '',
|
||||
relationObjectMetadataNamePlural: '',
|
||||
@ -135,7 +132,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
||||
type: FieldMetadataType.Relation,
|
||||
metadata: {
|
||||
fieldName: 'attachments',
|
||||
placeHolder: 'Attachments',
|
||||
relationType: 'FROM_MANY_OBJECTS',
|
||||
relationObjectMetadataNameSingular: '',
|
||||
relationObjectMetadataNamePlural: '',
|
||||
@ -204,7 +200,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
||||
type: FieldMetadataType.Relation,
|
||||
metadata: {
|
||||
fieldName: 'opportunities',
|
||||
placeHolder: 'Opportunities',
|
||||
relationType: 'FROM_MANY_OBJECTS',
|
||||
relationObjectMetadataNameSingular: '',
|
||||
relationObjectMetadataNamePlural: '',
|
||||
@ -239,7 +234,6 @@ export const SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS = (
|
||||
type: FieldMetadataType.Relation,
|
||||
metadata: {
|
||||
fieldName: 'activityTargets',
|
||||
placeHolder: 'Activities',
|
||||
relationType: 'FROM_MANY_OBJECTS',
|
||||
relationObjectMetadataNameSingular: '',
|
||||
relationObjectMetadataNamePlural: '',
|
||||
|
@ -20,8 +20,8 @@ describe('useContextScopeId', () => {
|
||||
),
|
||||
});
|
||||
|
||||
const scopedId = result.current;
|
||||
expect(scopedId).toBe(mockedContextValue);
|
||||
const scopeId = result.current;
|
||||
expect(scopeId).toBe(mockedContextValue);
|
||||
});
|
||||
|
||||
it('Should throw an error when used outside of the specified context', () => {
|
||||
|
@ -20,8 +20,8 @@ describe('useRecoilScopeId', () => {
|
||||
),
|
||||
});
|
||||
|
||||
const scopedId = result.current;
|
||||
expect(scopedId).toBe(mockedContextValue);
|
||||
const scopeId = result.current;
|
||||
expect(scopeId).toBe(mockedContextValue);
|
||||
});
|
||||
|
||||
it('Should throw an error when used outside of the specified context', () => {
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { useEffect } from '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) => {
|
||||
const { records } = context.parameters;
|
||||
|
||||
const { setRecords } = useSetRecordInStore();
|
||||
const { upsertRecords } = useUpsertRecordsInStore();
|
||||
|
||||
useEffect(() => {
|
||||
setRecords(records);
|
||||
upsertRecords(records);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
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