Skip to content

Commit a8fa15e

Browse files
JuliRossiclaude
andauthored
fix(drive-integration): link new Add Entry as child of selected parent [INTEG-4524] (#11111)
* updating logic * refactor(AddEntryForm): improve child reference handling and UI logic * test(drive-integration): add coverage for new-entry fieldMapping push path + rename fmIdx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(drive-integration): guard against parentId undefined to keep fields, fieldMappings, and edges consistent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(drive-integration): remove dead ?? 'Link' fallback and avoid double Object.keys call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(drive-integration): tighten ParentRefField types, use LocalizedField, drop unnecessary casts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ab2d7d8 commit a8fa15e

7 files changed

Lines changed: 560 additions & 139 deletions

File tree

apps/drive-integration/src/locations/Page/components/review/mapping/MappingView.tsx

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
hasFieldType,
5050
isEntryReferenceField,
5151
} from './fieldFormatting';
52+
import { linkChildToParentEntry, withUpdatedReferenceGraph } from './linkChildToParent';
5253
import { EditModal } from './edit-modals/EditModal';
5354
import { RichTextSelectionPreview } from './edit-modals/RichTextSelectionPreview';
5455

@@ -433,6 +434,7 @@ export const MappingView = ({
433434
return {
434435
tempId: entry.tempId ?? `${entry.contentTypeId}-${idx}`,
435436
label: `${contentTypeName} (${entryTitle})`,
437+
contentTypeId: entry.contentTypeId,
436438
};
437439
}),
438440
[entryBlockGraph.entries, payload.contentTypes]
@@ -445,29 +447,30 @@ export const MappingView = ({
445447
const newEntryIndex = entryBlockGraph.entries.length;
446448
const tempId = crypto.randomUUID();
447449

450+
const parentEntryIndex = isLinkedReference
451+
? entryBlockGraph.entries.findIndex(
452+
(entry, idx) =>
453+
(entry.tempId ?? `${entry.contentTypeId}-${idx}`) === params.referenceEntryId
454+
)
455+
: -1;
456+
const parentEntry =
457+
parentEntryIndex >= 0 ? entryBlockGraph.entries[parentEntryIndex] : undefined;
458+
const parentContentType = parentEntry
459+
? payload.contentTypes.find((ct) => ct.sys.id === parentEntry.contentTypeId)
460+
: undefined;
461+
448462
const refField = isLinkedReference
449-
? contentType?.fields?.find(
463+
? parentContentType?.fields?.find(
450464
(f) =>
451465
f.id === params.referenceFieldId ||
452466
(!params.referenceFieldId && isEntryReferenceField(f))
453467
)
454468
: undefined;
455469

456-
const newEntryFields: Record<string, Record<string, unknown>> = refField?.id
457-
? {
458-
[refField.id]: {
459-
[defaultLocale]:
460-
refField.type === 'Array'
461-
? [{ __ref: params.referenceEntryId }]
462-
: { __ref: params.referenceEntryId },
463-
},
464-
}
465-
: {};
466-
467470
const newEntry = {
468471
contentTypeId,
469472
tempId,
470-
fields: newEntryFields,
473+
fields: {} as Record<string, Record<string, unknown>>,
471474
fieldMappings: [],
472475
};
473476

@@ -476,6 +479,27 @@ export const MappingView = ({
476479
entries: [...entryBlockGraph.entries, newEntry],
477480
};
478481

482+
if (isLinkedReference && parentEntry && refField?.id && refField.type) {
483+
const { parentEntry: updatedParent, edges: nextEdges } = linkChildToParentEntry({
484+
parentEntry,
485+
childTempId: tempId,
486+
refField: { id: refField.id, type: refField.type },
487+
defaultLocale,
488+
previousEdges: referenceGraph.edges ?? [],
489+
});
490+
491+
next = {
492+
...next,
493+
entries: next.entries.map((entry, idx) =>
494+
idx === parentEntryIndex ? updatedParent : entry
495+
),
496+
};
497+
498+
if (onReferenceGraphChange) {
499+
onReferenceGraphChange(withUpdatedReferenceGraph(referenceGraph, nextEdges));
500+
}
501+
}
502+
479503
if (fieldIds.length > 0) {
480504
const resolvedTargets = fieldIds.flatMap((fieldId) => {
481505
const field = contentType?.fields?.find((f) => hasFieldId(f) && f.id === fieldId);
@@ -527,16 +551,6 @@ export const MappingView = ({
527551

528552
onEntryBlockGraphChange(next);
529553

530-
if (isLinkedReference && onReferenceGraphChange) {
531-
onReferenceGraphChange({
532-
...referenceGraph,
533-
edges: [
534-
...(referenceGraph.edges ?? []),
535-
{ from: tempId, to: params.referenceEntryId!, fieldId: refField?.id ?? '' },
536-
],
537-
});
538-
}
539-
540554
closeEditModal();
541555
};
542556

apps/drive-integration/src/locations/Page/components/review/mapping/edit-modals/AddEntryForm.tsx

Lines changed: 93 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { FieldSelectionDropdown } from './FieldSelectionDropdown';
77
export interface AddEntryFormState {
88
contentTypeId: string;
99
isReference: boolean | null;
10+
/** Parent entry tempId when linking as a child reference. */
1011
referenceEntryId: string;
12+
/** Reference field on the parent content type. */
1113
referenceFieldId: string;
1214
selectedFieldIds: string[];
1315
}
@@ -23,6 +25,7 @@ export const INITIAL_ADD_ENTRY_FORM_STATE: AddEntryFormState = {
2325
export interface ExistingEntryOption {
2426
tempId: string;
2527
label: string;
28+
contentTypeId: string;
2629
}
2730

2831
const getReferenceFieldOptions = (
@@ -38,17 +41,43 @@ const getReferenceFieldOptions = (
3841
});
3942
};
4043

44+
const getParentContentTypeId = (
45+
state: AddEntryFormState,
46+
existingEntries: ExistingEntryOption[]
47+
): string =>
48+
existingEntries.find((entry) => entry.tempId === state.referenceEntryId)?.contentTypeId ?? '';
49+
50+
/** Existing entries whose content type has at least one Entry reference field. */
51+
export const getParentEntriesThatAcceptChildren = (
52+
contentTypes: WorkflowContentType[],
53+
existingEntries: ExistingEntryOption[]
54+
): ExistingEntryOption[] =>
55+
existingEntries.filter(
56+
(entry) => getReferenceFieldOptions(contentTypes, entry.contentTypeId).length > 0
57+
);
58+
59+
/** True when at least one existing entry's content type can accept a child reference. */
60+
export const canLinkAsChildReference = (
61+
contentTypes: WorkflowContentType[],
62+
existingEntries: ExistingEntryOption[]
63+
): boolean => getParentEntriesThatAcceptChildren(contentTypes, existingEntries).length > 0;
64+
4165
/** Returns true when the form lacks enough input to save. */
4266
export const isAddEntrySaveDisabled = (
4367
state: AddEntryFormState,
44-
contentTypes: WorkflowContentType[]
68+
contentTypes: WorkflowContentType[],
69+
existingEntries: ExistingEntryOption[] = []
4570
): boolean => {
4671
if (!state.contentTypeId) return true;
47-
if (state.isReference === null) return true;
72+
const canBeReference = canLinkAsChildReference(contentTypes, existingEntries);
73+
// Reference Yes/No is hidden when no parent can accept a child — treat as No.
74+
if (canBeReference && state.isReference === null) return true;
4875
if (state.isReference) {
49-
const referenceFieldOptions = getReferenceFieldOptions(contentTypes, state.contentTypeId);
50-
if (referenceFieldOptions.length === 0) return true;
76+
// Child link lives on the parent — require a parent, then its reference field when ambiguous.
5177
if (!state.referenceEntryId) return true;
78+
const parentContentTypeId = getParentContentTypeId(state, existingEntries);
79+
const referenceFieldOptions = getReferenceFieldOptions(contentTypes, parentContentTypeId);
80+
if (referenceFieldOptions.length === 0) return true;
5281
if (referenceFieldOptions.length > 1 && !state.referenceFieldId) return true;
5382
}
5483
return state.selectedFieldIds.length === 0;
@@ -57,9 +86,11 @@ export const isAddEntrySaveDisabled = (
5786
/** Maps form state to the payload expected by onAddEntry. */
5887
export const toAddEntryFormParams = (
5988
state: AddEntryFormState,
60-
contentTypes: WorkflowContentType[]
89+
contentTypes: WorkflowContentType[],
90+
existingEntries: ExistingEntryOption[] = []
6191
): AddEntryFormParams => {
62-
const referenceFieldOptions = getReferenceFieldOptions(contentTypes, state.contentTypeId);
92+
const parentContentTypeId = getParentContentTypeId(state, existingEntries);
93+
const referenceFieldOptions = getReferenceFieldOptions(contentTypes, parentContentTypeId);
6394
return {
6495
contentTypeId: state.contentTypeId,
6596
isReference: state.isReference ?? false,
@@ -96,12 +127,22 @@ export const AddEntryForm = ({
96127
() => buildFieldOptionsForContentType(selectedContentType),
97128
[selectedContentType]
98129
);
130+
const parentContentTypeId = useMemo(
131+
() =>
132+
existingEntries.find((entry) => entry.tempId === state.referenceEntryId)?.contentTypeId ?? '',
133+
[existingEntries, state.referenceEntryId]
134+
);
99135
const referenceFieldOptions = useMemo(
100-
() => getReferenceFieldOptions(contentTypes, state.contentTypeId),
101-
[contentTypes, state.contentTypeId]
136+
() => getReferenceFieldOptions(contentTypes, parentContentTypeId),
137+
[contentTypes, parentContentTypeId]
138+
);
139+
const showReferenceFieldSelect =
140+
Boolean(state.referenceEntryId) && referenceFieldOptions.length > 1;
141+
const parentEntryOptions = useMemo(
142+
() => getParentEntriesThatAcceptChildren(contentTypes, existingEntries),
143+
[contentTypes, existingEntries]
102144
);
103-
const showReferenceFieldSelect = referenceFieldOptions.length > 1;
104-
const canBeReference = referenceFieldOptions.length > 0;
145+
const canBeReference = parentEntryOptions.length > 0;
105146
const hasContentType = Boolean(state.contentTypeId);
106147

107148
const handleContentTypeChange = (contentTypeId: string) => {
@@ -126,6 +167,13 @@ export const AddEntryForm = ({
126167
});
127168
};
128169

170+
const handleParentEntryChange = (referenceEntryId: string) => {
171+
onChange({
172+
referenceEntryId,
173+
referenceFieldId: '',
174+
});
175+
};
176+
129177
return (
130178
<Flex flexDirection="column" gap="spacingS">
131179
<Text as="p" fontWeight="fontWeightDemiBold">
@@ -150,39 +198,44 @@ export const AddEntryForm = ({
150198

151199
{hasContentType && (
152200
<>
153-
<FormControl marginBottom="none">
154-
<FormControl.Label>Should this entry be a reference entry?</FormControl.Label>
155-
<Flex flexDirection="column" gap="spacingXs">
156-
<Radio
157-
id="ref-yes"
158-
name="is-reference"
159-
value="yes"
160-
isChecked={state.isReference === true}
161-
isDisabled={!canBeReference}
162-
onChange={() => handleReferenceChange(true)}>
163-
Yes
164-
</Radio>
165-
<Radio
166-
id="ref-no"
167-
name="is-reference"
168-
value="no"
169-
isChecked={state.isReference === false}
170-
onChange={() => handleReferenceChange(false)}>
171-
No
172-
</Radio>
173-
</Flex>
174-
</FormControl>
201+
{canBeReference && (
202+
<FormControl marginBottom="none">
203+
<FormControl.Label>
204+
Should this new entry be a reference of an existing entry?
205+
</FormControl.Label>
206+
<Flex flexDirection="column" gap="spacingXs">
207+
<Radio
208+
id="ref-yes"
209+
name="is-reference"
210+
value="yes"
211+
isChecked={state.isReference === true}
212+
onChange={() => handleReferenceChange(true)}>
213+
Yes
214+
</Radio>
215+
<Radio
216+
id="ref-no"
217+
name="is-reference"
218+
value="no"
219+
isChecked={state.isReference === false}
220+
onChange={() => handleReferenceChange(false)}>
221+
No
222+
</Radio>
223+
</Flex>
224+
</FormControl>
225+
)}
175226

176-
{state.isReference === true && (
227+
{canBeReference && state.isReference === true && (
177228
<FormControl marginBottom="none">
178-
<FormControl.Label>Select the entry this should reference</FormControl.Label>
229+
<FormControl.Label>
230+
Which existing entry should this new entry be a reference to?
231+
</FormControl.Label>
179232
<Select
180233
value={state.referenceEntryId}
181-
onChange={(e) => onChange({ referenceEntryId: e.target.value })}>
234+
onChange={(e) => handleParentEntryChange(e.target.value)}>
182235
<Select.Option value="" isDisabled>
183236
Select an entry
184237
</Select.Option>
185-
{existingEntries.map((entry) => (
238+
{parentEntryOptions.map((entry) => (
186239
<Select.Option key={entry.tempId} value={entry.tempId}>
187240
{entry.label}
188241
</Select.Option>
@@ -191,9 +244,11 @@ export const AddEntryForm = ({
191244
</FormControl>
192245
)}
193246

194-
{state.isReference === true && showReferenceFieldSelect && (
247+
{canBeReference && state.isReference === true && showReferenceFieldSelect && (
195248
<FormControl marginBottom="none">
196-
<FormControl.Label>Which field should connect to this reference?</FormControl.Label>
249+
<FormControl.Label>
250+
Which field on the parent should link to this entry?
251+
</FormControl.Label>
197252
<Select
198253
value={state.referenceFieldId}
199254
onChange={(e) => onChange({ referenceFieldId: e.target.value })}>

apps/drive-integration/src/locations/Page/components/review/mapping/edit-modals/EditModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,12 @@ export const EditModal = ({
118118

119119
const handleAddEntrySave = () => {
120120
if (!addEntryFormState) return;
121-
onAddEntry?.(toAddEntryFormParams(addEntryFormState, contentTypes));
121+
onAddEntry?.(toAddEntryFormParams(addEntryFormState, contentTypes, existingEntries));
122122
setAddEntryFormState(null);
123123
};
124124

125125
const isAddEntryFormSaveDisabled =
126-
!addEntryFormState || isAddEntrySaveDisabled(addEntryFormState, contentTypes);
126+
!addEntryFormState || isAddEntrySaveDisabled(addEntryFormState, contentTypes, existingEntries);
127127

128128
const previewSectionTitle = viewModel.previewSectionTitle ?? 'Selected content';
129129
const previewText = (viewModel.contentPreview ?? viewModel.selectedText).trim();

0 commit comments

Comments
 (0)