@strapi/content-manager 5.55.1 → 5.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/dist/admin/pages/EditView/components/DocumentActions.js +133 -129
  2. package/dist/admin/pages/EditView/components/DocumentActions.js.map +1 -1
  3. package/dist/admin/pages/EditView/components/DocumentActions.mjs +137 -133
  4. package/dist/admin/pages/EditView/components/DocumentActions.mjs.map +1 -1
  5. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.js +113 -28
  6. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.js.map +1 -1
  7. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.mjs +113 -29
  8. package/dist/admin/pages/EditView/components/FormInputs/Relations/RelationModal.mjs.map +1 -1
  9. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js +8 -3
  10. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.js.map +1 -1
  11. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs +8 -3
  12. package/dist/admin/pages/EditView/components/FormInputs/Relations/Relations.mjs.map +1 -1
  13. package/dist/admin/src/pages/EditView/components/FormInputs/Relations/RelationModal.d.ts +27 -5
  14. package/dist/admin/src/pages/EditView/components/FormInputs/Wysiwyg/WysiwygStyles.d.ts +2 -2
  15. package/dist/admin/src/pages/EditView/components/FormLayout.d.ts +4 -4
  16. package/dist/server/controllers/relations.js +4 -0
  17. package/dist/server/controllers/relations.js.map +1 -1
  18. package/dist/server/controllers/relations.mjs +4 -0
  19. package/dist/server/controllers/relations.mjs.map +1 -1
  20. package/dist/server/src/controllers/relations.d.ts.map +1 -1
  21. package/package.json +9 -9
@@ -8,41 +8,56 @@ import { useNavigate, useMatch, useParams } from 'react-router-dom';
8
8
  import { PUBLISHED_AT_ATTRIBUTE_NAME } from '../../../constants/attributes.mjs';
9
9
  import { SINGLE_TYPES } from '../../../constants/collections.mjs';
10
10
  import { useDocumentRBAC } from '../../../features/DocumentRBAC.mjs';
11
- import { useDocument, useDoc } from '../../../hooks/useDocument.mjs';
11
+ import { useDoc } from '../../../hooks/useDocument.mjs';
12
12
  import { useDocumentActions } from '../../../hooks/useDocumentActions.mjs';
13
13
  import { useDocumentContext } from '../../../hooks/useDocumentContext.mjs';
14
14
  import { usePreviewContext } from '../../../preview/pages/Preview.mjs';
15
15
  import { LIST_PATH, CLONE_PATH } from '../../../router.mjs';
16
- import { useGetDraftRelationCountQuery as useLazyGetDraftRelationCountQuery, useUpdateDocumentMutation } from '../../../services/documents.mjs';
16
+ import { useLazyGetDocumentQuery, useGetDraftRelationCountQuery as useLazyGetDraftRelationCountQuery } from '../../../services/documents.mjs';
17
17
  import { buildValidParams, isBaseQueryError } from '../../../utils/api.mjs';
18
- import { getIn, setIn, isObject } from '../../../utils/objects.mjs';
18
+ import { getIn, isObject } from '../../../utils/objects.mjs';
19
19
  import { getTranslation } from '../../../utils/translations.mjs';
20
20
  import { handleInvisibleAttributes } from '../utils/data.mjs';
21
21
  import { EMPTY_DRAFT_RELATION_COUNTS, countLocalDraftRelations, normalizeDraftRelationCounts, resolveDraftRelationCounts, getDraftRelationsPublishState } from '../utils/draftRelationCounts.mjs';
22
22
  import { getEditViewShortcut } from '../utils/keyboardShortcuts.mjs';
23
- import { useRelationModal } from './FormInputs/Relations/RelationModal.mjs';
23
+ import { useRelationModal, isAnyRelationModalOpen } from './FormInputs/Relations/RelationModal.mjs';
24
24
 
25
25
  const publishConfirmDialogOpeners = new Map();
26
26
  const openPublishConfirmDialog = (scope)=>{
27
27
  publishConfirmDialogOpeners.get(scope)?.();
28
28
  };
29
- const connectRelationToParent = (parentDataToUpdate, fieldToConnect, data, fieldToConnectUID)=>{
29
+ /**
30
+ * Builds the local-form patch needed to connect a newly created/published relation into the
31
+ * parent's own relation field, given that field's CURRENT (possibly unsaved) live value.
32
+ *
33
+ * This only ever touches the parent's in-memory form state (via `setParentFormValue`, sourced
34
+ * from the parent's own `useForm(...).onChange`) — it deliberately never persists the parent
35
+ * document to the server, so connecting a relation on the fly can't silently save unrelated
36
+ * unsaved edits made elsewhere in the parent's form.
37
+ */ const buildRelationConnectPatch = (currentFieldValue, fieldToConnect, data, status, fieldToConnectUID)=>{
38
+ // Spread the freshly created/published document first so its own mainField (e.g. `name`) and
39
+ // other attributes — including its real numeric `id`, not just `documentId` — are available.
40
+ // Without the real `id`, the relation field's "already connected" search filter (which keys off
41
+ // `rel.id`) can't recognize this item, so it would stay selectable in the relation combobox
42
+ // even after being connected. Keeping the label/status render correct needs the mainField too,
43
+ // which is why we still spread the whole document rather than only picking id/documentId/locale.
30
44
  const relationToConnect = {
31
- id: data.documentId,
45
+ ...data,
32
46
  documentId: data.documentId,
33
- locale: data.locale
47
+ locale: data.locale,
48
+ ...status ? {
49
+ status
50
+ } : {}
34
51
  };
35
52
  /*
36
- * Check if the fieldToConnect is already present in the parentDataToUpdate.
37
- * This happens in particular when in the parentDocument you have created
38
- * a new component without saving.
39
- */ const fieldValue = getIn(parentDataToUpdate, fieldToConnect);
40
- const relationFieldValue = isObject(fieldValue) ? fieldValue : undefined;
53
+ * Check if the fieldToConnect already has a value. This happens in particular when in the
54
+ * parent document you have created a new component without saving.
55
+ */ const relationFieldValue = isObject(currentFieldValue) ? currentFieldValue : undefined;
41
56
  const isFieldPresent = relationFieldValue !== undefined;
42
57
  const fieldPath = fieldToConnect.split('.');
43
58
  const fieldName = fieldPath.at(-1);
44
59
  if (!fieldName) {
45
- return parentDataToUpdate;
60
+ return null;
46
61
  }
47
62
  const parentPath = fieldPath.slice(0, -1).join('.');
48
63
  const existingConnect = Array.isArray(relationFieldValue?.connect) ? relationFieldValue.connect : [];
@@ -58,11 +73,10 @@ const connectRelationToParent = (parentDataToUpdate, fieldToConnect, data, field
58
73
  connect,
59
74
  disconnect: []
60
75
  };
61
- const dataWithRelation = setIn(parentDataToUpdate, fieldToConnect, relationValue);
62
- if (!isFieldPresent && fieldToConnectUID && parentPath) {
63
- return setIn(dataWithRelation, `${parentPath}.__component`, fieldToConnectUID);
64
- }
65
- return dataWithRelation;
76
+ return {
77
+ relationValue,
78
+ componentUIDPath: !isFieldPresent && fieldToConnectUID && parentPath ? `${parentPath}.__component` : undefined
79
+ };
66
80
  };
67
81
  const DocumentActions = ({ actions })=>{
68
82
  const { formatMessage } = useIntl();
@@ -447,18 +461,6 @@ const transformData = (data)=>{
447
461
  return data;
448
462
  };
449
463
  const transformDocumentData = (data)=>transformData(data);
450
- const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentFormValues, options)=>{
451
- const parentDataToUpdate = parentFormValues ?? fallbackParentFormValues;
452
- if (!parentDataToUpdate) {
453
- return parentDataToUpdate;
454
- }
455
- const { data } = handleInvisibleAttributes(transformDocumentData(parentDataToUpdate), {
456
- ...options,
457
- // The live snapshot can omit saved invisible fields, so retain the parent's saved initial values.
458
- initialValues: fallbackParentFormValues
459
- });
460
- return data;
461
- };
462
464
  /* -------------------------------------------------------------------------------------------------
463
465
  * DocumentActionComponents
464
466
  * -----------------------------------------------------------------------------------------------*/ const PublishAction = ({ activeTab, documentId, model, collectionType, meta, document })=>{
@@ -476,6 +478,7 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
476
478
  }));
477
479
  const { publish, isLoading } = useDocumentActions();
478
480
  const onPreview = usePreviewContext('PublishAction', (state)=>state.onPreview, false);
481
+ const [fetchDraftDocument] = useLazyGetDocumentQuery();
479
482
  const [countDraftRelations, { isError: isErrorDraftRelations }] = useLazyGetDraftRelationCountQuery();
480
483
  const [localDraftRelationCounts, setLocalDraftRelationCounts] = React.useState(EMPTY_DRAFT_RELATION_COUNTS);
481
484
  const [serverDraftRelationCounts, setServerDraftRelationCounts] = React.useState(EMPTY_DRAFT_RELATION_COUNTS);
@@ -493,18 +496,15 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
493
496
  // need to discriminate if the publish is coming from a relation modal or in the edit view
494
497
  const relationContext = useRelationModal('PublishAction', ()=>true, false);
495
498
  const fromRelationModal = relationContext != undefined;
496
- const isRelationModalOpen = useRelationModal('PublishAction', (state)=>state.state.isModalOpen, false);
497
499
  const publishConfirmScope = fromRelationModal ? 'relation-modal' : onPreview ? 'preview' : 'panel';
498
500
  const dispatch = useRelationModal('PublishAction', (state)=>state.dispatch);
499
501
  const fieldToConnect = useRelationModal('PublishAction', (state)=>state.state.fieldToConnect, false);
500
502
  const fieldToConnectUID = useRelationModal('PublishAction', (state)=>state.state.fieldToConnectUID, false);
501
503
  const getParentFormValues = useRelationModal('PublishAction', (state)=>state.state.getParentFormValues, false);
504
+ const setParentFormValue = useRelationModal('PublishAction', (state)=>state.state.setParentFormValue, false);
502
505
  const documentHistory = useRelationModal('PublishAction', (state)=>state.state.documentHistory, false);
503
- const rootDocumentMeta = useRelationModal('PublishAction', (state)=>state.rootDocumentMeta);
504
506
  const dispatchGuidedTour = useGuidedTour('PublishAction', (s)=>s.dispatch);
505
507
  const { currentDocumentMeta } = useDocumentContext('PublishAction');
506
- const [updateDocumentMutation] = useUpdateDocumentMutation();
507
- const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();
508
508
  const idToPublish = currentDocumentMeta.documentId || id;
509
509
  React.useEffect(()=>{
510
510
  if (isErrorDraftRelations) {
@@ -577,16 +577,6 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
577
577
  }, [
578
578
  fetchDraftRelationsCount
579
579
  ]);
580
- const parentDocumentMetaToUpdate = documentHistory?.at(-2) ?? rootDocumentMeta;
581
- const parentDocumentData = useDocument({
582
- documentId: parentDocumentMetaToUpdate?.documentId,
583
- model: parentDocumentMetaToUpdate?.model,
584
- collectionType: parentDocumentMetaToUpdate?.collectionType,
585
- params: parentDocumentMetaToUpdate?.params
586
- }, {
587
- skip: !parentDocumentMetaToUpdate
588
- });
589
- const { getInitialFormValues } = useDoc();
590
580
  const isDocumentPublished = (document?.[PUBLISHED_AT_ATTRIBUTE_NAME] || meta?.availableStatus.some((doc)=>doc[PUBLISHED_AT_ATTRIBUTE_NAME] !== null)) && document?.status !== 'modified';
591
581
  // eslint-disable-next-line react-hooks/exhaustive-deps
592
582
  const performPublish = async ()=>{
@@ -643,6 +633,17 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
643
633
  // Reset form with current values as new initial values (clears errors/submitting and sets modified to false)
644
634
  if ('data' in res) {
645
635
  resetForm(getValues());
636
+ // This document is now persisted with whatever local connect patches were merged into
637
+ // it, if any (see the pendingConnects explanation below) — they'd be stale from here on.
638
+ dispatch?.({
639
+ type: 'CLEAR_PENDING_CONNECTS',
640
+ payload: {
641
+ documentMeta: {
642
+ model,
643
+ documentId: res.data.documentId
644
+ }
645
+ }
646
+ });
646
647
  dispatchGuidedTour({
647
648
  type: 'set_completed_actions',
648
649
  payload: [
@@ -668,42 +669,45 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
668
669
  params: currentDocumentMeta.params
669
670
  };
670
671
  /*
671
- * Update, if needed, the parent relation with the newly published document.
672
- * Check if in history we have the parent relation otherwise use the
673
- * rootDocument
674
- */ if (fieldToConnect && documentHistory && (parentDocumentMetaToUpdate.documentId || parentDocumentMetaToUpdate.collectionType === SINGLE_TYPES)) {
675
- const fallbackParentFormValues = parentDocumentMetaToUpdate.collectionType === SINGLE_TYPES ? getInitialFormValues() : parentDocumentData.getInitialFormValues();
676
- const parentDataToUpdate = prepareParentDataForRelationUpdate(getParentFormValues?.(), fallbackParentFormValues, {
677
- schema: parentDocumentData.schema,
678
- components: parentDocumentData.components
672
+ * Connect the newly published document to the parent's relation field, purely in local
673
+ * form state — never on the server. This must not persist the parent document,
674
+ * otherwise publishing here would silently save any of the parent's other unrelated
675
+ * unsaved edits too.
676
+ *
677
+ * The parent is either the root document that originally opened the modal (its own Form
678
+ * stays mounted for the whole session, so it's safe to write into directly) or a nested
679
+ * document still further up the modal's documentHistory (whose Form is the SAME shared
680
+ * instance the modal reuses at every level, wholesale-replaced on every navigation — a
681
+ * direct write there would land on the wrong document and be gone the moment we
682
+ * navigate). For the latter the patch is queued on GO_TO_CREATED_RELATION below instead,
683
+ * to be re-applied once that document becomes current again.
684
+ */ const isNestedParent = Array.isArray(documentHistory) && documentHistory.length >= 2;
685
+ let connectPatch;
686
+ if (fieldToConnect && (setParentFormValue || isNestedParent)) {
687
+ const currentFieldValue = getIn(getParentFormValues?.(), fieldToConnect);
688
+ // Publishing a brand-new document creates two rows sharing one documentId — a draft
689
+ // and a published one, each with its own numeric id. The parent's relation search
690
+ // (and its "already connected" filter) identifies items by the draft row's id, so
691
+ // connecting with the publish response's id would leave this relation still
692
+ // selectable there. Fetch the draft row to get the id the search actually expects.
693
+ const draftDocument = await fetchDraftDocument({
694
+ collectionType,
695
+ model,
696
+ documentId: res.data.documentId,
697
+ params: currentDocumentMeta.params
679
698
  });
680
- const metaDocumentToUpdate = documentHistory.at(-2) ?? rootDocumentMeta;
681
- if (parentDataToUpdate) {
682
- const dataToUpdate = connectRelationToParent(parentDataToUpdate, fieldToConnect, res.data, fieldToConnectUID);
683
- try {
684
- const updateRes = await updateDocumentMutation({
685
- collectionType: metaDocumentToUpdate.collectionType,
686
- model: metaDocumentToUpdate.model,
687
- documentId: metaDocumentToUpdate.collectionType !== SINGLE_TYPES ? metaDocumentToUpdate.documentId : undefined,
688
- params: metaDocumentToUpdate.params,
689
- data: dataToUpdate
690
- });
691
- if ('error' in updateRes) {
692
- toggleNotification({
693
- type: 'danger',
694
- message: formatAPIError(updateRes.error)
695
- });
696
- return;
697
- }
698
- } catch (err) {
699
- toggleNotification({
700
- type: 'danger',
701
- message: formatMessage({
702
- id: 'notification.error',
703
- defaultMessage: 'An error occurred'
704
- })
705
- });
706
- throw err;
699
+ const patch = buildRelationConnectPatch(currentFieldValue, fieldToConnect, draftDocument.data?.data ?? res.data, 'published', fieldToConnectUID);
700
+ if (patch && isNestedParent) {
701
+ connectPatch = {
702
+ fieldToConnect,
703
+ relationValue: patch.relationValue,
704
+ componentUIDPath: patch.componentUIDPath,
705
+ componentUID: patch.componentUIDPath ? fieldToConnectUID : undefined
706
+ };
707
+ } else if (patch && setParentFormValue) {
708
+ setParentFormValue(fieldToConnect, patch.relationValue);
709
+ if (patch.componentUIDPath) {
710
+ setParentFormValue(patch.componentUIDPath, fieldToConnectUID);
707
711
  }
708
712
  }
709
713
  }
@@ -711,7 +715,8 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
711
715
  type: 'GO_TO_CREATED_RELATION',
712
716
  payload: {
713
717
  document: newRelation,
714
- shouldBypassConfirmation: true
718
+ shouldBypassConfirmation: true,
719
+ connectPatch
715
720
  }
716
721
  });
717
722
  }
@@ -757,7 +762,9 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
757
762
  return;
758
763
  }
759
764
  e.preventDefault();
760
- if (!fromRelationModal && isRelationModalOpen) {
765
+ // When a relation modal is open, only its own PublishAction instance (fromRelationModal)
766
+ // should react to the shortcut — otherwise the background entry publishes at the same time.
767
+ if (!fromRelationModal && isAnyRelationModalOpen()) {
761
768
  return;
762
769
  }
763
770
  if (isDisabled) {
@@ -777,7 +784,6 @@ const prepareParentDataForRelationUpdate = (parentFormValues, fallbackParentForm
777
784
  fromRelationModal,
778
785
  getFreshDraftRelationCounts,
779
786
  isDisabled,
780
- isRelationModalOpen,
781
787
  performPublish,
782
788
  publishConfirmScope,
783
789
  schema?.options?.draftAndPublish
@@ -873,7 +879,6 @@ const UpdateAction = ({ activeTab, documentId, model, collectionType })=>{
873
879
  const { currentDocument: { components } } = useDocumentContext('UpdateAction');
874
880
  const [{ rawQuery }] = useQueryParams();
875
881
  const onPreview = usePreviewContext('UpdateAction', (state)=>state.onPreview, false);
876
- const { getInitialFormValues } = useDoc();
877
882
  const isSubmitting = useForm('UpdateAction', ({ isSubmitting })=>isSubmitting);
878
883
  const modified = useForm('UpdateAction', ({ modified })=>modified);
879
884
  const setSubmitting = useForm('UpdateAction', ({ setSubmitting })=>setSubmitting);
@@ -889,21 +894,10 @@ const UpdateAction = ({ activeTab, documentId, model, collectionType })=>{
889
894
  const fieldToConnect = useRelationModal('UpdateAction', (state)=>state.state.fieldToConnect, false);
890
895
  const fieldToConnectUID = useRelationModal('PublishAction', (state)=>state.state.fieldToConnectUID, false);
891
896
  const getParentFormValues = useRelationModal('UpdateAction', (state)=>state.state.getParentFormValues, false);
897
+ const setParentFormValue = useRelationModal('UpdateAction', (state)=>state.state.setParentFormValue, false);
892
898
  const documentHistory = useRelationModal('UpdateAction', (state)=>state.state.documentHistory, false);
893
- const rootDocumentMeta = useRelationModal('UpdateAction', (state)=>state.rootDocumentMeta);
894
899
  const fromRelationModal = relationContext != undefined;
895
900
  const { currentDocumentMeta } = useDocumentContext('UpdateAction');
896
- const [updateDocumentMutation] = useUpdateDocumentMutation();
897
- const { _unstableFormatAPIError: formatAPIError } = useAPIErrorHandler();
898
- const parentDocumentMetaToUpdate = documentHistory?.at(-2) ?? rootDocumentMeta;
899
- const parentDocumentData = useDocument({
900
- documentId: parentDocumentMetaToUpdate?.documentId,
901
- model: parentDocumentMetaToUpdate?.model,
902
- collectionType: parentDocumentMetaToUpdate?.collectionType,
903
- params: parentDocumentMetaToUpdate?.params
904
- }, {
905
- skip: !parentDocumentMetaToUpdate
906
- });
907
901
  const { schema } = useDoc();
908
902
  const suitableSchema = fromRelationModal ? relationalModalSchema : schema;
909
903
  const hasDraftAndPublished = suitableSchema?.options?.draftAndPublish ?? false;
@@ -975,6 +969,17 @@ const UpdateAction = ({ activeTab, documentId, model, collectionType })=>{
975
969
  setErrors(formatValidationErrors(res.error));
976
970
  } else {
977
971
  resetForm(latestValues);
972
+ // This document is now persisted with whatever local connect patches were merged into
973
+ // it, if any (see the pendingConnects explanation below) — they'd be stale from here on.
974
+ dispatch?.({
975
+ type: 'CLEAR_PENDING_CONNECTS',
976
+ payload: {
977
+ documentMeta: {
978
+ model,
979
+ documentId
980
+ }
981
+ }
982
+ });
978
983
  }
979
984
  } else {
980
985
  const { data } = handleInvisibleAttributes(transformDocumentData(latestValues), {
@@ -995,43 +1000,34 @@ const UpdateAction = ({ activeTab, documentId, model, collectionType })=>{
995
1000
  params: currentDocumentMeta.params
996
1001
  };
997
1002
  /*
998
- * Update, if needed, the parent relation with the newly published document.
999
- * Check if in history we have the parent relation otherwise use the
1000
- * rootDocument
1001
- */ if (fieldToConnect && documentHistory && (parentDocumentMetaToUpdate.documentId || parentDocumentMetaToUpdate.collectionType === SINGLE_TYPES)) {
1002
- const fallbackParentFormValues = parentDocumentMetaToUpdate.collectionType === SINGLE_TYPES ? getInitialFormValues() : parentDocumentData.getInitialFormValues();
1003
- const parentDataToUpdate = prepareParentDataForRelationUpdate(getParentFormValues?.(), fallbackParentFormValues, {
1004
- schema: parentDocumentData.schema,
1005
- components: parentDocumentData.components
1006
- });
1007
- if (parentDataToUpdate) {
1008
- const dataToUpdate = connectRelationToParent(parentDataToUpdate, fieldToConnect, res.data, fieldToConnectUID);
1009
- try {
1010
- const updateRes = await updateDocumentMutation({
1011
- collectionType: parentDocumentMetaToUpdate.collectionType,
1012
- model: parentDocumentMetaToUpdate.model,
1013
- documentId: parentDocumentMetaToUpdate.collectionType !== SINGLE_TYPES ? parentDocumentMetaToUpdate.documentId : undefined,
1014
- params: parentDocumentMetaToUpdate.params,
1015
- data: {
1016
- ...dataToUpdate
1017
- }
1018
- });
1019
- if ('error' in updateRes) {
1020
- toggleNotification({
1021
- type: 'danger',
1022
- message: formatAPIError(updateRes.error)
1023
- });
1024
- return;
1025
- }
1026
- } catch (err) {
1027
- toggleNotification({
1028
- type: 'danger',
1029
- message: formatMessage({
1030
- id: 'notification.error',
1031
- defaultMessage: 'An error occurred'
1032
- })
1033
- });
1034
- throw err;
1003
+ * Connect the newly created document to the parent's relation field, purely in local
1004
+ * form state — never on the server. This must not persist the parent document,
1005
+ * otherwise saving here would silently save any of the parent's other unrelated
1006
+ * unsaved edits too.
1007
+ *
1008
+ * The parent is either the root document that originally opened the modal (its own
1009
+ * Form stays mounted for the whole session, so it's safe to write into directly) or a
1010
+ * nested document still further up the modal's documentHistory (whose Form is the
1011
+ * SAME shared instance the modal reuses at every level, wholesale-replaced on every
1012
+ * navigation — a direct write there would land on the wrong document and be gone the
1013
+ * moment we navigate). For the latter the patch is queued on GO_TO_CREATED_RELATION
1014
+ * below instead, to be re-applied once that document becomes current again.
1015
+ */ const isNestedParent = Array.isArray(documentHistory) && documentHistory.length >= 2;
1016
+ let connectPatch;
1017
+ if (fieldToConnect && (setParentFormValue || isNestedParent)) {
1018
+ const currentFieldValue = getIn(getParentFormValues?.(), fieldToConnect);
1019
+ const patch = buildRelationConnectPatch(currentFieldValue, fieldToConnect, res.data, hasDraftAndPublished ? 'draft' : undefined, fieldToConnectUID);
1020
+ if (patch && isNestedParent) {
1021
+ connectPatch = {
1022
+ fieldToConnect,
1023
+ relationValue: patch.relationValue,
1024
+ componentUIDPath: patch.componentUIDPath,
1025
+ componentUID: patch.componentUIDPath ? fieldToConnectUID : undefined
1026
+ };
1027
+ } else if (patch && setParentFormValue) {
1028
+ setParentFormValue(fieldToConnect, patch.relationValue);
1029
+ if (patch.componentUIDPath) {
1030
+ setParentFormValue(patch.componentUIDPath, fieldToConnectUID);
1035
1031
  }
1036
1032
  }
1037
1033
  }
@@ -1039,7 +1035,8 @@ const UpdateAction = ({ activeTab, documentId, model, collectionType })=>{
1039
1035
  type: 'GO_TO_CREATED_RELATION',
1040
1036
  payload: {
1041
1037
  document: createdRelation,
1042
- shouldBypassConfirmation: true
1038
+ shouldBypassConfirmation: true,
1039
+ connectPatch
1043
1040
  }
1044
1041
  });
1045
1042
  } else {
@@ -1076,12 +1073,19 @@ const UpdateAction = ({ activeTab, documentId, model, collectionType })=>{
1076
1073
  handleUpdateRef.current = handleUpdate;
1077
1074
  const isDisabledRef = React.useRef(isDisabled);
1078
1075
  isDisabledRef.current = isDisabled;
1076
+ const fromRelationModalRef = React.useRef(fromRelationModal);
1077
+ fromRelationModalRef.current = fromRelationModal;
1079
1078
  React.useEffect(()=>{
1080
1079
  const handleKeyDown = (e)=>{
1081
1080
  if (getEditViewShortcut(e) !== 'save') {
1082
1081
  return;
1083
1082
  }
1084
1083
  e.preventDefault();
1084
+ // When a relation modal is open, only its own UpdateAction instance (fromRelationModal)
1085
+ // should react to the shortcut — otherwise the background entry saves at the same time.
1086
+ if (!fromRelationModalRef.current && isAnyRelationModalOpen()) {
1087
+ return;
1088
+ }
1085
1089
  if (!isDisabledRef.current) {
1086
1090
  handleUpdateRef.current();
1087
1091
  }