@manuscripts/body-editor 3.12.66 → 3.13.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 (289) hide show
  1. package/dist/cjs/components/toolbar/type-selector/styles.js +2 -1
  2. package/dist/cjs/versions.js +1 -1
  3. package/dist/es/components/toolbar/type-selector/styles.js +2 -1
  4. package/dist/es/versions.js +1 -1
  5. package/dist/types/components/toolbar/type-selector/styles.d.ts +2 -2
  6. package/dist/types/lib/footnotes.d.ts +4 -2
  7. package/dist/types/lib/utils.d.ts +5 -4
  8. package/dist/types/versions.d.ts +1 -1
  9. package/package.json +37 -36
  10. package/{dist/es/lib/statistics.js → src/__mocks__/styleMock.ts} +2 -14
  11. package/src/assets.ts +20 -0
  12. package/src/classes/collabProvider.ts +38 -0
  13. package/src/clipboard.ts +54 -0
  14. package/src/commands.ts +2109 -0
  15. package/src/components/ChangeHandlingForm.tsx +54 -0
  16. package/src/components/LanguageDropdown/index.tsx +343 -0
  17. package/src/components/affiliations/AffiliationForm.tsx +238 -0
  18. package/src/components/affiliations/AffiliationItem.tsx +151 -0
  19. package/src/components/affiliations/AffiliationList.tsx +71 -0
  20. package/src/components/affiliations/AffiliationsModal.tsx +676 -0
  21. package/src/components/affiliations/AffiliationsPanel.tsx +76 -0
  22. package/src/components/affiliations/CreateAffiliationModal.tsx +135 -0
  23. package/src/components/authors/AuthorDetailsForm.tsx +438 -0
  24. package/src/components/authors/AuthorList.tsx +81 -0
  25. package/src/components/authors/AuthorsModal.tsx +642 -0
  26. package/src/components/authors/AuthorsPanel.tsx +69 -0
  27. package/src/components/authors/CreateAuthorModal.tsx +172 -0
  28. package/src/components/authors/CreditDrawer.tsx +71 -0
  29. package/src/components/authors/DraggableAuthor.tsx +228 -0
  30. package/src/components/authors/useManageAffiliations.tsx +77 -0
  31. package/src/components/authors/useManageCredit.tsx +63 -0
  32. package/src/components/authors-affiliations/AuthorsAndAffiliationsModals.tsx +170 -0
  33. package/src/components/authors-affiliations/GenericPanel.tsx +164 -0
  34. package/src/components/authors-affiliations/ModalTabs.tsx +100 -0
  35. package/src/components/awards/AwardForm.tsx +338 -0
  36. package/src/components/awards/AwardModal.tsx +120 -0
  37. package/src/components/awards/DeleteAwardDiaolog.tsx +52 -0
  38. package/src/components/dialog/ConfirmationDialog.tsx +131 -0
  39. package/src/components/form/CreateModalStyles.tsx +37 -0
  40. package/src/components/form/FormFooter.tsx +54 -0
  41. package/src/components/form/FormPlaceholder.tsx +71 -0
  42. package/src/components/form/ModalFormActions.tsx +88 -0
  43. package/src/components/form/UnsavedLabel.tsx +49 -0
  44. package/src/components/hooks/use-debounce.ts +56 -0
  45. package/src/components/hooks/useAffiliationShowsErrorIndicator.ts +51 -0
  46. package/src/components/hooks/useAuthorShowsErrorIndicator.ts +73 -0
  47. package/src/components/keyboard-shortcuts-modal/FormattedShortcut.tsx +56 -0
  48. package/src/components/keyboard-shortcuts-modal/KeyboardShortcutsModal.tsx +283 -0
  49. package/src/components/keyboard-shortcuts-modal/keyboard-shortcuts.ts +304 -0
  50. package/src/components/keywords/AddKeywordInline.tsx +284 -0
  51. package/src/components/keywords/DeleteKeywordDialog.tsx +52 -0
  52. package/src/components/modal-drawer/GenericDrawer.tsx +65 -0
  53. package/src/components/modal-drawer/GenericDrawerGroup.tsx +125 -0
  54. package/src/components/outline/DraggableTree.tsx +438 -0
  55. package/src/components/outline/ManuscriptOutline.tsx +127 -0
  56. package/src/components/outline/Outline.tsx +141 -0
  57. package/src/components/references/BibliographyItemSource.ts +80 -0
  58. package/src/components/references/CitationEditor.tsx +301 -0
  59. package/src/components/references/CitationViewer.tsx +62 -0
  60. package/src/components/references/ImportBibliographyForm.tsx +232 -0
  61. package/src/components/references/ImportBibliographyModal.tsx +159 -0
  62. package/src/components/references/ReferenceForm/PersonDropDown.tsx +93 -0
  63. package/src/components/references/ReferenceForm/ReferenceForm.tsx +701 -0
  64. package/src/components/references/ReferenceForm/config.ts +133 -0
  65. package/src/components/references/ReferenceForm/styled-components.ts +156 -0
  66. package/src/components/references/ReferenceLine.tsx +43 -0
  67. package/src/components/references/ReferenceSearch.tsx +163 -0
  68. package/src/components/references/ReferenceSearchResults.tsx +113 -0
  69. package/src/components/references/ReferenceSearchResultsPlaceholder.tsx +71 -0
  70. package/src/components/references/ReferenceSearchSection.tsx +124 -0
  71. package/src/components/references/ReferencesEditor.tsx +61 -0
  72. package/src/components/references/ReferencesModal.tsx +352 -0
  73. package/src/components/references/SearchInput.tsx +98 -0
  74. package/src/components/toolbar/InsertEmbedDialog.tsx +222 -0
  75. package/src/components/toolbar/InsertTableDialog.tsx +208 -0
  76. package/src/components/toolbar/ListMenuItem.tsx +203 -0
  77. package/src/components/toolbar/helpers.ts +547 -0
  78. package/src/components/toolbar/type-selector/OptionComponent.tsx +89 -0
  79. package/src/components/toolbar/type-selector/TypeSelector.tsx +157 -0
  80. package/src/components/toolbar/type-selector/styles.ts +79 -0
  81. package/src/components/views/CrossReferenceItems.tsx +199 -0
  82. package/src/components/views/DeleteEmbedDialog.tsx +83 -0
  83. package/src/components/views/DeleteFootnoteDialog.tsx +54 -0
  84. package/src/components/views/FigureDropdown.tsx +423 -0
  85. package/src/components/views/FootnotesSelector.tsx +255 -0
  86. package/src/components/views/InsertSpecialCharacter.tsx +245 -0
  87. package/src/components/views/LinkForm.tsx +220 -0
  88. package/src/components/views/TableCellContextMenu.tsx +241 -0
  89. package/src/configs/ManuscriptsEditor.ts +140 -0
  90. package/src/configs/editor-plugins.ts +119 -0
  91. package/src/configs/editor-views.ts +139 -0
  92. package/src/icons.ts +62 -0
  93. package/src/index.ts +65 -0
  94. package/src/keys/highlight.ts +65 -0
  95. package/src/keys/index.ts +33 -0
  96. package/src/keys/keyword.ts +69 -0
  97. package/src/keys/list.ts +156 -0
  98. package/src/keys/misc.ts +112 -0
  99. package/src/keys/title.ts +221 -0
  100. package/src/lib/__tests__/__fixtures__/doc.json +388 -0
  101. package/src/lib/__tests__/__snapshots__/paste.test.ts.snap +24 -0
  102. package/src/lib/__tests__/authors.test.ts +202 -0
  103. package/src/lib/__tests__/helpers.test.ts +46 -0
  104. package/src/lib/__tests__/paste.test.ts +57 -0
  105. package/src/lib/__tests__/plugins.test.ts +176 -0
  106. package/src/lib/__tests__/url.test.ts +31 -0
  107. package/src/lib/array-reducer.ts +68 -0
  108. package/src/lib/authors-and-affiliations.ts +81 -0
  109. package/src/lib/authors.ts +67 -0
  110. package/src/lib/capabilities.ts +55 -0
  111. package/src/lib/codemirror.ts +37 -0
  112. package/src/lib/comments.ts +123 -0
  113. package/src/lib/context-menu.ts +745 -0
  114. package/src/lib/copy.ts +30 -0
  115. package/src/lib/crossref.ts +90 -0
  116. package/src/lib/dnd.ts +29 -0
  117. package/src/lib/doc.ts +167 -0
  118. package/src/lib/dompurify.ts +28 -0
  119. package/src/lib/files.ts +180 -0
  120. package/src/lib/footnotes.ts +83 -0
  121. package/src/lib/get-media-type.ts +105 -0
  122. package/src/lib/helpers.ts +199 -0
  123. package/src/lib/hierarchy.ts +117 -0
  124. package/src/lib/languages.ts +37 -0
  125. package/src/lib/lists.ts +56 -0
  126. package/src/lib/math.ts +111 -0
  127. package/src/lib/media.ts +334 -0
  128. package/src/lib/navigation-utils.ts +235 -0
  129. package/src/lib/normalize.ts +70 -0
  130. package/src/lib/paste.ts +304 -0
  131. package/src/lib/placeholder.ts +32 -0
  132. package/src/lib/platform.ts +18 -0
  133. package/src/lib/plugins.ts +22 -0
  134. package/src/lib/popper.ts +180 -0
  135. package/src/lib/position-menu.ts +184 -0
  136. package/src/lib/references.ts +94 -0
  137. package/src/lib/template.ts +29 -0
  138. package/src/lib/track-changes-utils.ts +34 -0
  139. package/{dist/types/lib/statistics.d.ts → src/lib/url.ts} +11 -8
  140. package/src/lib/use-do-with-debounce.ts +39 -0
  141. package/src/lib/utils.ts +323 -0
  142. package/src/lib/view.ts +100 -0
  143. package/src/menus.tsx +684 -0
  144. package/src/node-type-icons.tsx +83 -0
  145. package/src/plugins/__tests__/__fixtures__/deleted-footnote.json +140 -0
  146. package/src/plugins/__tests__/__fixtures__/inserted-footnote.json +153 -0
  147. package/src/plugins/__tests__/footnotes.test.ts +67 -0
  148. package/src/plugins/accessibility_element.ts +219 -0
  149. package/src/plugins/add-subtitle.ts +102 -0
  150. package/src/plugins/affiliations.ts +136 -0
  151. package/src/plugins/alt-titles.ts +219 -0
  152. package/src/plugins/bibliography.ts +235 -0
  153. package/src/plugins/comments.ts +328 -0
  154. package/src/plugins/cross-references.ts +70 -0
  155. package/src/plugins/detect-inconsistency/detect-inconsistency-utils.ts +102 -0
  156. package/src/plugins/detect-inconsistency/index.ts +55 -0
  157. package/src/plugins/detect-inconsistency/validators.ts +283 -0
  158. package/src/plugins/doi.ts +66 -0
  159. package/src/plugins/editor-props.ts +39 -0
  160. package/src/plugins/elements.ts +60 -0
  161. package/src/plugins/footnotes.ts +298 -0
  162. package/src/plugins/link.ts +53 -0
  163. package/src/plugins/lock-body.ts +136 -0
  164. package/src/plugins/move-node.ts +55 -0
  165. package/src/plugins/objects.ts +125 -0
  166. package/src/plugins/paragraphs.ts +90 -0
  167. package/src/plugins/persist.ts +64 -0
  168. package/src/plugins/persistent-cursor.ts +66 -0
  169. package/src/plugins/placeholder.ts +219 -0
  170. package/src/plugins/prevent-empty.ts +50 -0
  171. package/src/plugins/search-replace/index.ts +157 -0
  172. package/src/plugins/search-replace/lib.ts +95 -0
  173. package/src/plugins/section_category.ts +212 -0
  174. package/src/plugins/section_title/autocompletion.ts +87 -0
  175. package/src/plugins/section_title/index.ts +105 -0
  176. package/src/plugins/selected-suggestion.ts +243 -0
  177. package/src/plugins/tables-cursor-fix.ts +143 -0
  178. package/src/plugins/translations.ts +263 -0
  179. package/src/rules.ts +60 -0
  180. package/src/selection.ts +112 -0
  181. package/src/testing/default-capabilities.ts +48 -0
  182. package/src/testing/default-editor-data.ts +73 -0
  183. package/src/testing/empty-editor-doc.json +62 -0
  184. package/src/testing/polyfill-dom.ts +43 -0
  185. package/src/testing/section-categories.json +161 -0
  186. package/src/testing/setup-editor.ts +55 -0
  187. package/src/tests.ts +42 -0
  188. package/src/toolbar.tsx +259 -0
  189. package/src/types/@citation-js/core.d.ts +16 -0
  190. package/src/types/@citation-js/index.d.ts +16 -0
  191. package/src/types/@citation-js/plugin-bibtex.d.ts +16 -0
  192. package/src/types/@citation-js/plugin-csl.d.ts +16 -0
  193. package/src/types/@citation-js/plugin-doi.d.ts +16 -0
  194. package/src/types/@citation-js/plugin-enw.d.ts +16 -0
  195. package/src/types/@citation-js/plugin-pubmed.d.ts +16 -0
  196. package/src/types/@citation-js/plugin-ris.d.ts +16 -0
  197. package/src/types/citeproc.d.ts +124 -0
  198. package/src/types/css.d.ts +16 -0
  199. package/src/types/json.d.ts +17 -0
  200. package/src/types/prosemirror-dev-tools.d.ts +16 -0
  201. package/src/types/prosemirror-dropcursor.d.ts +16 -0
  202. package/src/types/styled-components.d.ts +23 -0
  203. package/src/types.ts +49 -0
  204. package/src/useEditor.ts +244 -0
  205. package/src/versions.ts +3 -0
  206. package/src/views/ReactSubView.tsx +145 -0
  207. package/src/views/abstracts.ts +46 -0
  208. package/src/views/accessibility_element.ts +86 -0
  209. package/src/views/affiliations.ts +254 -0
  210. package/src/views/alt_title.ts +85 -0
  211. package/src/views/alt_titles_section.ts +97 -0
  212. package/src/views/attachment.ts +160 -0
  213. package/src/views/attachments.ts +72 -0
  214. package/src/views/author_notes.ts +96 -0
  215. package/src/views/award.ts +194 -0
  216. package/src/views/awards.ts +59 -0
  217. package/src/views/base_node_view.ts +85 -0
  218. package/src/views/bibliography_element.ts +313 -0
  219. package/src/views/block_view.ts +100 -0
  220. package/src/views/blockquote_element.ts +26 -0
  221. package/src/views/blockquote_element_editable.ts +24 -0
  222. package/src/views/box_element.ts +33 -0
  223. package/src/views/caption.ts +37 -0
  224. package/src/views/caption_title.ts +33 -0
  225. package/src/views/citation.ts +67 -0
  226. package/src/views/citation_editable.ts +312 -0
  227. package/src/views/contributors.ts +341 -0
  228. package/src/views/creators.ts +82 -0
  229. package/src/views/cross_reference.ts +74 -0
  230. package/src/views/cross_reference_editable.ts +113 -0
  231. package/src/views/editable_block.ts +141 -0
  232. package/src/views/embed.ts +345 -0
  233. package/src/views/empty.ts +26 -0
  234. package/src/views/equation.ts +47 -0
  235. package/src/views/equation_editable.ts +84 -0
  236. package/src/views/equation_element.ts +29 -0
  237. package/src/views/equation_element_editable.ts +21 -0
  238. package/src/views/figure.ts +84 -0
  239. package/src/views/figure_editable.ts +361 -0
  240. package/src/views/figure_element.ts +197 -0
  241. package/src/views/figure_element_editable.ts +21 -0
  242. package/src/views/footnote.ts +236 -0
  243. package/src/views/footnotes_element.ts +54 -0
  244. package/src/views/general_table_footnote.ts +140 -0
  245. package/src/views/hero_image.ts +76 -0
  246. package/src/views/hero_image_editable.ts +21 -0
  247. package/src/views/image_element.ts +373 -0
  248. package/src/views/image_element_editable.ts +21 -0
  249. package/src/views/inline_equation.ts +47 -0
  250. package/src/views/inline_equation_editable.ts +70 -0
  251. package/src/views/inline_footnote.ts +264 -0
  252. package/src/views/inline_footnote_editable.ts +20 -0
  253. package/src/views/keyword.ts +127 -0
  254. package/src/views/keyword_group.ts +109 -0
  255. package/src/views/link.ts +27 -0
  256. package/src/views/link_editable.ts +169 -0
  257. package/src/views/list.ts +46 -0
  258. package/src/views/list_item.ts +26 -0
  259. package/src/views/paragraph.ts +26 -0
  260. package/src/views/paragraph_editable.ts +21 -0
  261. package/src/views/placeholder.ts +35 -0
  262. package/src/views/placeholder_element.ts +38 -0
  263. package/src/views/placeholder_element_editable.ts +21 -0
  264. package/src/views/pullquote_element.ts +106 -0
  265. package/src/views/pullquote_element_editable.ts +24 -0
  266. package/src/views/quote_image_editable.ts +84 -0
  267. package/src/views/section.ts +64 -0
  268. package/src/views/section_label.ts +20 -0
  269. package/src/views/section_title.ts +87 -0
  270. package/src/views/section_title_editable.ts +21 -0
  271. package/src/views/subtitle.ts +42 -0
  272. package/src/views/subtitle_editable.ts +20 -0
  273. package/src/views/subtitles.ts +34 -0
  274. package/src/views/subtitles_editable.ts +21 -0
  275. package/src/views/supplement.ts +236 -0
  276. package/src/views/supplement_editable.ts +20 -0
  277. package/src/views/supplements.ts +65 -0
  278. package/src/views/supplements_editable.ts +21 -0
  279. package/src/views/table_cell.ts +150 -0
  280. package/src/views/table_element.ts +33 -0
  281. package/src/views/table_element_editable.ts +21 -0
  282. package/src/views/table_element_footer.ts +41 -0
  283. package/src/views/title.ts +46 -0
  284. package/src/views/title_editable.ts +20 -0
  285. package/src/views/trans_graphical_abstract.ts +55 -0
  286. package/src/views/trans_graphical_abstract_editable.ts +21 -0
  287. package/src/views/translated_abstract.ts +53 -0
  288. package/src/views/translated_abstract_editable.ts +21 -0
  289. package/dist/cjs/lib/statistics.js +0 -38
@@ -0,0 +1,676 @@
1
+ /*!
2
+ * © 2025 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {
17
+ AddIcon,
18
+ AffiliationPlaceholderIcon,
19
+ CloseButton,
20
+ InspectorTabPanel,
21
+ InspectorTabPanels,
22
+ InspectorTabs,
23
+ outlineStyle,
24
+ ModalBody,
25
+ ModalContainer,
26
+ ModalHeader,
27
+ ModalSidebar,
28
+ ModalSidebarHeader,
29
+ ModalSidebarTitle,
30
+ ScrollableModalContent,
31
+ SidebarContent,
32
+ StyledModal,
33
+ } from '@manuscripts/style-guide'
34
+ import { generateNodeID, schema } from '@manuscripts/transform'
35
+ import { isEqual } from 'lodash'
36
+ import React, {
37
+ useCallback,
38
+ useEffect,
39
+ useReducer,
40
+ useRef,
41
+ useState,
42
+ } from 'react'
43
+ import styled from 'styled-components'
44
+
45
+ import {
46
+ AffiliationAttrs,
47
+ authorComparator,
48
+ ContributorAttrs,
49
+ } from '../../lib/authors'
50
+ import { checkID } from '../../lib/normalize'
51
+ import { authorsReducer } from '../authors/AuthorsModal'
52
+ import { affiliationsReducer } from '../authors/useManageAffiliations'
53
+ import { ConfirmationDialog, DialogType } from '../dialog/ConfirmationDialog'
54
+ import FormFooter from '../form/FormFooter'
55
+ import { FormPlaceholder } from '../form/FormPlaceholder'
56
+ import { ModalFormActions, ModalFormSaveButton } from '../form/ModalFormActions'
57
+ import { ModalTabs } from '../authors-affiliations/ModalTabs'
58
+ import { AuthorsPanel } from '../authors/AuthorsPanel'
59
+ import { AffiliationForm, FormActions } from './AffiliationForm'
60
+ import { AffiliationList } from './AffiliationList'
61
+
62
+ export interface AffiliationsModalProps {
63
+ affiliation?: AffiliationAttrs
64
+ authors: ContributorAttrs[]
65
+ affiliations: AffiliationAttrs[]
66
+ onSaveAffiliation: (affiliation: AffiliationAttrs) => void
67
+ onDeleteAffiliation: (affiliation: AffiliationAttrs) => void
68
+ onUpdateAuthors: (authors: ContributorAttrs[]) => void
69
+ addNewAffiliation?: boolean
70
+ onClose?: () => void
71
+ openAuthorsModal?: () => void
72
+ }
73
+
74
+ const MODAL_ON_CLOSE_NOTIFY_DELAY_MS = 220
75
+
76
+ function makeAuthorItems(authors: ContributorAttrs[]) {
77
+ return authors.map((author) => ({
78
+ id: author.id,
79
+ label: `${author.given} ${author.family}`,
80
+ }))
81
+ }
82
+
83
+ export const AffiliationsModal: React.FC<AffiliationsModalProps> = ({
84
+ authors: $authors,
85
+ affiliations: $affiliations,
86
+ affiliation,
87
+ onSaveAffiliation,
88
+ onDeleteAffiliation,
89
+ onUpdateAuthors,
90
+ addNewAffiliation = false,
91
+ onClose,
92
+ openAuthorsModal,
93
+ }) => {
94
+ const [isOpen, setIsOpen] = useState(true)
95
+ const [selection, setSelection] = useState(affiliation)
96
+ const [showingDeleteDialog, setShowDeleteDialog] = useState(false)
97
+ const valuesRef = useRef<AffiliationAttrs>(undefined)
98
+ const actionsRef = useRef<FormActions>(undefined)
99
+ const [authors, dispatchAuthors] = useReducer(
100
+ authorsReducer,
101
+ $authors.sort(authorComparator)
102
+ )
103
+ useEffect(() => {
104
+ dispatchAuthors({
105
+ type: 'set',
106
+ state: [...$authors].sort(authorComparator),
107
+ })
108
+ }, [$authors])
109
+ const [affiliations, dispatchAffiliations] = useReducer(
110
+ affiliationsReducer,
111
+ $affiliations
112
+ )
113
+
114
+ const [isDisableSave, setIsDisableSave] = useState(true)
115
+ const [newAffiliation, setNewAffiliation] = useState(false)
116
+ const [
117
+ showRequiredFieldConfirmationDialog,
118
+ setShowRequiredFieldConfirmationDialog,
119
+ ] = useState(false)
120
+ const [showConfirmationDialog, setShowConfirmationDialog] = useState(false)
121
+ const [selectedAuthorIds, setSelectedAuthorIds] = useState<string[]>([])
122
+ const [pendingSelection, setPendingSelection] =
123
+ useState<AffiliationAttrs | null>(null)
124
+ const [pendingAction, setPendingAction] = useState<
125
+ 'select' | 'new' | 'close' | null
126
+ >(null)
127
+ const [savedAffiliationId, setSavedAffiliationId] = useState<
128
+ string | undefined
129
+ >(undefined)
130
+ const [affiliationAuthorMap, setAffiliationAuthorMap] = useState<
131
+ Map<string, string[]>
132
+ >(new Map())
133
+ const [affiliationTabHasError, setAffiliationTabHasError] = useState(false)
134
+ const [
135
+ affiliationDetailsUnsavedContinue,
136
+ setAffiliationDetailsUnsavedContinue,
137
+ ] = useState(false)
138
+ const [affiliationTabIndex, setAffiliationTabIndex] = useState(0)
139
+
140
+ const prevIsOpenRef = useRef(true)
141
+ useEffect(() => {
142
+ if (prevIsOpenRef.current && !isOpen) {
143
+ prevIsOpenRef.current = isOpen
144
+ const id = window.setTimeout(() => {
145
+ onClose?.()
146
+ }, MODAL_ON_CLOSE_NOTIFY_DELAY_MS)
147
+ return () => window.clearTimeout(id)
148
+ }
149
+ prevIsOpenRef.current = isOpen
150
+ }, [isOpen, onClose])
151
+
152
+ useEffect(() => {
153
+ if (isDisableSave) {
154
+ setAffiliationDetailsUnsavedContinue(false)
155
+ }
156
+ }, [isDisableSave])
157
+
158
+ useEffect(() => {
159
+ setAffiliationDetailsUnsavedContinue(false)
160
+ if (selection?.id) {
161
+ setAffiliationTabIndex(0)
162
+ }
163
+ }, [selection?.id])
164
+
165
+ useEffect(() => {
166
+ if (!selection) {
167
+ return
168
+ }
169
+ const currentAffiliation = selection
170
+ const affiliatedAuthorIds = authors
171
+ .filter((author) =>
172
+ author.affiliationIDs?.includes(currentAffiliation.id)
173
+ )
174
+ .map((author) => author.id)
175
+ setSelectedAuthorIds(affiliatedAuthorIds)
176
+ setAffiliationAuthorMap((prevMap) => {
177
+ const newMap = new Map(prevMap)
178
+ newMap.set(currentAffiliation.id, affiliatedAuthorIds)
179
+ return newMap
180
+ })
181
+ // eslint-disable-next-line react-hooks/exhaustive-deps
182
+ }, [])
183
+
184
+ const handleClose = () => {
185
+ const values = valuesRef.current
186
+ const hasAffiliationChanges =
187
+ selection && !isEqual(values, checkID(selection, 'affiliation'))
188
+ const originalAuthors = selection
189
+ ? (affiliationAuthorMap.get(selection.id) ?? [])
190
+ : []
191
+ const hasAuthorChanges = !isEqual(
192
+ originalAuthors.sort(),
193
+ selectedAuthorIds.sort()
194
+ )
195
+ const hasChanges = hasAffiliationChanges || (selection && hasAuthorChanges)
196
+ const isInstitutionEmpty = values?.institution?.trim() === ''
197
+ if (hasChanges) {
198
+ if (isInstitutionEmpty && hasAffiliationChanges) {
199
+ setShowRequiredFieldConfirmationDialog(true)
200
+ } else {
201
+ setShowConfirmationDialog(true)
202
+ }
203
+ setPendingAction('close')
204
+ } else {
205
+ setIsOpen(false)
206
+ setSelection(undefined)
207
+ }
208
+ }
209
+
210
+ const handleSelect = (affiliation: AffiliationAttrs) => {
211
+ const values = valuesRef.current
212
+ const hasAffiliationChanges =
213
+ selection && !isEqual(values, checkID(selection, 'affiliation'))
214
+ const originalAuthors = selection
215
+ ? (affiliationAuthorMap.get(selection.id) ?? [])
216
+ : []
217
+ const hasAuthorChanges = !isEqual(
218
+ originalAuthors.sort(),
219
+ selectedAuthorIds.sort()
220
+ )
221
+ const hasChanges = hasAffiliationChanges || hasAuthorChanges
222
+ const isInstitutionEmpty = values?.institution?.trim() === ''
223
+ if (hasChanges) {
224
+ setPendingSelection(affiliation)
225
+ setPendingAction('select')
226
+ if (isInstitutionEmpty && hasAffiliationChanges) {
227
+ setShowRequiredFieldConfirmationDialog(true)
228
+ } else {
229
+ setShowConfirmationDialog(true)
230
+ }
231
+ } else {
232
+ const affiliatedAuthorIds = authors
233
+ .filter((author) => author.affiliationIDs?.includes(affiliation.id))
234
+ .map((author) => author.id)
235
+ setNewAffiliation(false)
236
+ setSelection(affiliation)
237
+ setSelectedAuthorIds(affiliatedAuthorIds)
238
+ setAffiliationAuthorMap((prevMap) => {
239
+ const newMap = new Map(prevMap)
240
+ newMap.set(affiliation.id, affiliatedAuthorIds)
241
+ return newMap
242
+ })
243
+ }
244
+ }
245
+
246
+ const handleSaveAffiliation = useCallback(
247
+ (values: AffiliationAttrs | undefined) => {
248
+ if (!values || !selection) {
249
+ return
250
+ }
251
+ setIsDisableSave(true)
252
+ const affiliation = {
253
+ ...checkID(selection, 'affiliation'),
254
+ ...values,
255
+ }
256
+ onSaveAffiliation(affiliation)
257
+ dispatchAffiliations({
258
+ type: 'update',
259
+ items: [affiliation],
260
+ })
261
+ setSelection(affiliation)
262
+
263
+ const updatedAuthors = authors.map((author) => ({
264
+ ...author,
265
+ affiliationIDs: selectedAuthorIds.includes(author.id)
266
+ ? [...new Set([...(author.affiliationIDs || []), affiliation.id])]
267
+ : (author.affiliationIDs || []).filter((id) => id !== affiliation.id),
268
+ }))
269
+
270
+ dispatchAuthors({
271
+ type: 'update',
272
+ items: updatedAuthors,
273
+ })
274
+
275
+ onUpdateAuthors(updatedAuthors)
276
+ setNewAffiliation(false)
277
+ setAffiliationAuthorMap((prevMap) => {
278
+ const newMap = new Map(prevMap)
279
+ newMap.set(affiliation.id, selectedAuthorIds)
280
+ return newMap
281
+ })
282
+ setAffiliationDetailsUnsavedContinue(false)
283
+
284
+ setSavedAffiliationId(affiliation.id)
285
+
286
+ setTimeout(() => {
287
+ setSavedAffiliationId(undefined)
288
+ }, 3200)
289
+ },
290
+ [
291
+ authors,
292
+ dispatchAffiliations,
293
+ dispatchAuthors,
294
+ onSaveAffiliation,
295
+ onUpdateAuthors,
296
+ selectedAuthorIds,
297
+ selection,
298
+ ]
299
+ )
300
+
301
+ const handleAffiliationChange = (values: AffiliationAttrs) => {
302
+ valuesRef.current = values
303
+ if (!selection || selection.id !== values.id) {
304
+ setIsDisableSave(true)
305
+ return
306
+ }
307
+ const isInstitutionEmpty = !values.institution?.trim()
308
+ const hasAffiliationChanges =
309
+ selection &&
310
+ !isEqual(
311
+ checkID(values, 'affiliation'),
312
+ checkID(selection, 'affiliation')
313
+ )
314
+ const originalAuthors = affiliationAuthorMap.get(selection.id) ?? []
315
+ const hasAuthorChanges =
316
+ selection && !isEqual(originalAuthors.sort(), selectedAuthorIds.sort())
317
+ const shouldEnableSave =
318
+ !isInstitutionEmpty && (hasAffiliationChanges || hasAuthorChanges)
319
+ setIsDisableSave(!shouldEnableSave)
320
+ }
321
+
322
+ const handleShowDeleteDialog = () => {
323
+ setShowDeleteDialog((prev) => !prev)
324
+ }
325
+
326
+ const handleDeleteAffiliation = () => {
327
+ if (!selection) {
328
+ return
329
+ }
330
+
331
+ const updatedAuthors = authors.map((author) => ({
332
+ ...author,
333
+ affiliationIDs: (author.affiliationIDs || []).filter(
334
+ (id) => id !== selection.id
335
+ ),
336
+ }))
337
+
338
+ dispatchAuthors({
339
+ type: 'update',
340
+ items: updatedAuthors,
341
+ })
342
+ onUpdateAuthors(updatedAuthors)
343
+
344
+ onDeleteAffiliation(selection)
345
+ dispatchAffiliations({
346
+ type: 'delete',
347
+ item: selection,
348
+ })
349
+ setSelectedAuthorIds([])
350
+ setSelection(undefined)
351
+ }
352
+
353
+ const selectAuthor = (authorId: string) => {
354
+ if (!selection) {
355
+ return
356
+ }
357
+
358
+ const newSelectedAuthorIds = selectedAuthorIds.includes(authorId)
359
+ ? selectedAuthorIds.filter((id) => id !== authorId)
360
+ : [...selectedAuthorIds, authorId]
361
+ setSelectedAuthorIds(newSelectedAuthorIds)
362
+
363
+ const hasAffiliationChanges = !isEqual(
364
+ valuesRef.current,
365
+ checkID(selection, 'affiliation')
366
+ )
367
+ const originalAuthors = affiliationAuthorMap.get(selection.id) ?? []
368
+ const hasAuthorChanges = !isEqual(
369
+ originalAuthors.sort(),
370
+ newSelectedAuthorIds.sort()
371
+ )
372
+
373
+ const isInstitutionEmpty = !valuesRef.current?.institution?.trim()
374
+ const shouldEnableSave =
375
+ !isInstitutionEmpty && (hasAffiliationChanges || hasAuthorChanges)
376
+ setIsDisableSave(!shouldEnableSave)
377
+ }
378
+
379
+ const selectedAuthors = selectedAuthorIds
380
+ .map((authorId) => {
381
+ const author = authors.find((a) => a.id === authorId)
382
+ return {
383
+ id: authorId,
384
+ label: author ? `${author.given} ${author.family}` : '',
385
+ }
386
+ })
387
+ .filter((author) => author.label)
388
+
389
+ const handleAddAffiliation = () => {
390
+ const values = valuesRef.current
391
+ const hasChanges = !isDisableSave
392
+ const isInstitutionEmpty = values?.institution?.trim() === ''
393
+ const emptyAffiliation = createEmptyAffiliation(affiliations.length)
394
+
395
+ if (hasChanges) {
396
+ setPendingAction('new')
397
+ if (isInstitutionEmpty) {
398
+ setShowRequiredFieldConfirmationDialog(true)
399
+ } else {
400
+ setShowConfirmationDialog(true)
401
+ }
402
+ return
403
+ }
404
+
405
+ setNewAffiliation(true)
406
+ setSelection(emptyAffiliation)
407
+ setSelectedAuthorIds([])
408
+ }
409
+
410
+ useEffect(() => {
411
+ if (addNewAffiliation) {
412
+ handleAddAffiliation()
413
+ setNewAffiliation(true)
414
+ }
415
+ // eslint-disable-next-line react-hooks/exhaustive-deps
416
+ }, [addNewAffiliation])
417
+
418
+ const handleConfirmationCancel = () => {
419
+ const action = pendingAction
420
+ const pending = pendingSelection
421
+
422
+ setShowConfirmationDialog(false)
423
+ setShowRequiredFieldConfirmationDialog(false)
424
+
425
+ if (action === 'close') {
426
+ setIsOpen(false)
427
+ setSelection(undefined)
428
+ valuesRef.current = undefined
429
+ setPendingSelection(null)
430
+ setPendingAction(null)
431
+ return
432
+ }
433
+
434
+ if (action === 'select' && pending) {
435
+ setSelection(pending)
436
+ setNewAffiliation(false)
437
+ const affiliatedAuthorIds = authors
438
+ .filter((author) =>
439
+ author.affiliationIDs?.some((aff) => aff === pending.id)
440
+ )
441
+ .map((author) => author.id)
442
+ setSelectedAuthorIds(affiliatedAuthorIds)
443
+ valuesRef.current = checkID(pending, 'affiliation')
444
+ setAffiliationAuthorMap((prevMap) => {
445
+ const newMap = new Map(prevMap)
446
+ newMap.set(pending.id, affiliatedAuthorIds)
447
+ return newMap
448
+ })
449
+ } else if (action === 'new') {
450
+ setNewAffiliation(true)
451
+ setSelection(affiliation)
452
+ setSelectedAuthorIds([])
453
+ valuesRef.current = affiliation
454
+ ? checkID(affiliation, 'affiliation')
455
+ : undefined
456
+ } else if (pending) {
457
+ valuesRef.current = checkID(pending, 'affiliation')
458
+ } else {
459
+ valuesRef.current = undefined
460
+ }
461
+
462
+ setPendingSelection(null)
463
+ setPendingAction(null)
464
+ }
465
+
466
+ return (
467
+ <StyledModal
468
+ isOpen={isOpen}
469
+ onRequestClose={() => handleClose()}
470
+ shouldCloseOnOverlayClick={true}
471
+ >
472
+ <ModalContainer data-cy="affiliations-modal">
473
+ <ModalHeader>
474
+ <CloseButton
475
+ onClick={() => handleClose()}
476
+ data-cy="modal-close-button"
477
+ />
478
+ </ModalHeader>
479
+
480
+ <StyledModalBody>
481
+ <ModalSidebar>
482
+ <StyledModalSidebarHeader>
483
+ <ModalSidebarTitle>Institutional Affiliations</ModalSidebarTitle>
484
+ </StyledModalSidebarHeader>
485
+ <StyledSidebarContent>
486
+ <AddAffiliationButton
487
+ data-cy="add-affiliation-button"
488
+ onClick={handleAddAffiliation}
489
+ data-active={newAffiliation}
490
+ >
491
+ <AddIcon width={18} height={18} />
492
+ <ActionTitle>New Affiliation</ActionTitle>
493
+ </AddAffiliationButton>
494
+ <AffiliationList
495
+ affiliation={selection}
496
+ affiliations={affiliations}
497
+ onSelect={handleSelect}
498
+ onDelete={handleShowDeleteDialog}
499
+ lastSavedAffiliationId={savedAffiliationId}
500
+ />
501
+ </StyledSidebarContent>
502
+ </ModalSidebar>
503
+ <StyledScrollableModalContent data-cy="affiliations-modal-content">
504
+ {selection ? (
505
+ <>
506
+ <AffiliationTabs
507
+ selectedIndex={affiliationTabIndex}
508
+ onChange={setAffiliationTabIndex}
509
+ >
510
+ <ModalFormActions
511
+ type="affiliation"
512
+ onDelete={handleDeleteAffiliation}
513
+ showingDeleteDialog={
514
+ showingDeleteDialog &&
515
+ !(
516
+ showConfirmationDialog ||
517
+ showRequiredFieldConfirmationDialog
518
+ )
519
+ }
520
+ showDeleteDialog={handleShowDeleteDialog}
521
+ />
522
+ <ModalTabs
523
+ tabLabels={['Affiliation Details', 'Authors']}
524
+ tabErrorIndicators={[affiliationTabHasError, false]}
525
+ tabWarningIndicators={[
526
+ affiliationDetailsUnsavedContinue &&
527
+ !affiliationTabHasError,
528
+ false,
529
+ ]}
530
+ />
531
+ <InspectorTabPanels>
532
+ <AffiliationTabPanel>
533
+ <AffiliationForm
534
+ values={checkID(selection, 'affiliation')}
535
+ onSave={(attrs) => handleSaveAffiliation(attrs)}
536
+ onChange={handleAffiliationChange}
537
+ actionsRef={actionsRef}
538
+ newEntity={newAffiliation}
539
+ onAffiliationErrorChange={setAffiliationTabHasError}
540
+ unsavedContinueActive={
541
+ affiliationDetailsUnsavedContinue
542
+ }
543
+ />
544
+ </AffiliationTabPanel>
545
+ {openAuthorsModal && (
546
+ <AffiliationTabPanel>
547
+ <AuthorsPanel
548
+ items={makeAuthorItems(authors)}
549
+ selectedItems={selectedAuthors}
550
+ onSelect={selectAuthor}
551
+ openAuthorsModal={openAuthorsModal}
552
+ />
553
+ </AffiliationTabPanel>
554
+ )}
555
+ </InspectorTabPanels>
556
+ </AffiliationTabs>
557
+ <ConfirmationDialog
558
+ isOpen={showRequiredFieldConfirmationDialog}
559
+ onPrimary={() =>
560
+ setShowRequiredFieldConfirmationDialog(false)
561
+ }
562
+ onSecondary={handleConfirmationCancel}
563
+ type={DialogType.REQUIRED}
564
+ entityType="affiliation"
565
+ />
566
+ <ConfirmationDialog
567
+ isOpen={showConfirmationDialog}
568
+ onPrimary={() => {
569
+ setShowConfirmationDialog(false)
570
+ setPendingSelection(null)
571
+ setPendingAction(null)
572
+ setAffiliationDetailsUnsavedContinue(true)
573
+ }}
574
+ onSecondary={handleConfirmationCancel}
575
+ type={DialogType.SAVE}
576
+ entityType="affiliation"
577
+ />
578
+ </>
579
+ ) : (
580
+ <FormPlaceholder
581
+ type="affiliation"
582
+ title="Affiliation Details"
583
+ message="Select an affiliation from the list to display it's details here."
584
+ placeholderIcon={<AffiliationPlaceholderIcon />}
585
+ />
586
+ )}
587
+ </StyledScrollableModalContent>
588
+ </StyledModalBody>
589
+ <FormFooter
590
+ onCancel={handleClose}
591
+ primaryAction={
592
+ selection ? (
593
+ <ModalFormSaveButton
594
+ form="affiliation-form"
595
+ isDisableSave={isDisableSave}
596
+ onSubmitForm={() => actionsRef.current?.submitForm?.()}
597
+ />
598
+ ) : undefined
599
+ }
600
+ />
601
+ </ModalContainer>
602
+ </StyledModal>
603
+ )
604
+ }
605
+
606
+ function createEmptyAffiliation(priority: number): AffiliationAttrs {
607
+ return {
608
+ id: generateNodeID(schema.nodes.affiliation),
609
+ institution: '',
610
+ department: '',
611
+ addressLine1: '',
612
+ addressLine2: '',
613
+ addressLine3: '',
614
+ postCode: '',
615
+ country: '',
616
+ county: '',
617
+ city: '',
618
+ email: {
619
+ href: '',
620
+ text: '',
621
+ },
622
+ priority: priority,
623
+ }
624
+ }
625
+
626
+ const StyledSidebarContent = styled(SidebarContent)`
627
+ padding: 8px;
628
+ `
629
+ const AddAffiliationButton = styled.button`
630
+ background: none;
631
+ border: none;
632
+ margin: 0;
633
+ font: inherit;
634
+ color: inherit;
635
+ width: 100%;
636
+ display: flex;
637
+ align-items: center;
638
+ padding: 12px 8px 12px 12px;
639
+ cursor: pointer;
640
+ &[data-active='true'] {
641
+ background: ${(props) => props.theme.colors.background.fifth};
642
+ border: 1px solid ${(props) => props.theme.colors.border.primary};
643
+ padding: 11px 8px 11px 12px;
644
+ border-left: 0;
645
+ border-right: 0;
646
+ }
647
+ ${outlineStyle}
648
+ `
649
+
650
+ const ActionTitle = styled.div`
651
+ padding-left: ${(props) => props.theme.grid.unit * 2}px;
652
+ `
653
+
654
+ const AffiliationTabs = styled(InspectorTabs)`
655
+ position: relative;
656
+ `
657
+ const AffiliationTabPanel = styled(InspectorTabPanel).attrs({
658
+ tabIndex: -1,
659
+ unmount: false,
660
+ })`
661
+ margin-top: ${(props) => props.theme.grid.unit * 4}px;
662
+ height: calc(100% - 16px);
663
+ `
664
+
665
+ const StyledModalBody = styled(ModalBody)`
666
+ position: relative;
667
+ height: calc(90vh - 40px);
668
+ `
669
+ const StyledModalSidebarHeader = styled(ModalSidebarHeader)`
670
+ margin-top: 8px;
671
+ margin-bottom: 16px;
672
+ `
673
+
674
+ const StyledScrollableModalContent = styled(ScrollableModalContent)`
675
+ padding: 45px 16px 16px;
676
+ `