@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,76 @@
1
+ /*!
2
+ * © 2026 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 { InfoCircleIcon } from '@manuscripts/style-guide'
17
+ import React from 'react'
18
+
19
+ import { AffiliationAttrs } from '../../lib/authors'
20
+ import {
21
+ GenericPanel,
22
+ ListItem,
23
+ ListItems,
24
+ useListSelectedIds,
25
+ } from '../authors-affiliations/GenericPanel'
26
+
27
+ export interface AffiliationsPanelProps {
28
+ items: AffiliationAttrs[]
29
+ selectedItems: { id: string }[]
30
+ onSelect: (id: string) => void
31
+ onOpenAffiliationsModal: () => void
32
+ }
33
+
34
+ function affiliationSecondaryLine(item: AffiliationAttrs): string | undefined {
35
+ const line = [item.city, item.county, item.country].filter(Boolean).join(', ')
36
+ return line || undefined
37
+ }
38
+
39
+ export const AffiliationsPanel: React.FC<AffiliationsPanelProps> = ({
40
+ items,
41
+ selectedItems = [],
42
+ onSelect,
43
+ onOpenAffiliationsModal,
44
+ }) => {
45
+ const selectedIds = useListSelectedIds(selectedItems)
46
+
47
+ return (
48
+ <GenericPanel
49
+ createLabel="New Affiliation"
50
+ onCreate={onOpenAffiliationsModal}
51
+ createDataCy="add-affiliations-link"
52
+ emptyDataCy="affiliations-panel-empty"
53
+ isEmpty={items.length === 0}
54
+ emptyIcon={<InfoCircleIcon />}
55
+ emptyMessage={
56
+ <>
57
+ There are no affiliations attributed yet!
58
+ <br />
59
+ Click &lsquo;+ New Affiliation&rsquo;
60
+ </>
61
+ }
62
+ >
63
+ <ListItems data-cy="affiliations-panel">
64
+ {items.map((item) => (
65
+ <ListItem
66
+ key={item.id}
67
+ selected={selectedIds.has(item.id)}
68
+ onClick={() => onSelect(item.id)}
69
+ primary={item.institution}
70
+ secondary={affiliationSecondaryLine(item)}
71
+ />
72
+ ))}
73
+ </ListItems>
74
+ </GenericPanel>
75
+ )
76
+ }
@@ -0,0 +1,135 @@
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
+ CloseButton,
18
+ ModalContainer,
19
+ ModalHeader,
20
+ StyledModal,
21
+ } from '@manuscripts/style-guide'
22
+ import { generateNodeID, schema } from '@manuscripts/transform'
23
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
24
+
25
+ import { AffiliationAttrs } from '../../lib/authors'
26
+ import { checkID } from '../../lib/normalize'
27
+ import {
28
+ MODAL_ON_CLOSE_NOTIFY_DELAY_MS,
29
+ FormTitle,
30
+ StyledModalBody,
31
+ StyledScrollableModalContent,
32
+ } from '../form/CreateModalStyles'
33
+ import FormFooter from '../form/FormFooter'
34
+ import { ModalFormSaveButton } from '../form/ModalFormActions'
35
+ import { AffiliationForm, FormActions } from './AffiliationForm'
36
+
37
+ export interface CreateAffiliationModalProps {
38
+ affiliationsCount: number
39
+ onSave: (affiliation: AffiliationAttrs) => void
40
+ onClose: () => void
41
+ }
42
+
43
+ export const CreateAffiliationModal: React.FC<CreateAffiliationModalProps> = ({
44
+ affiliationsCount,
45
+ onSave,
46
+ onClose,
47
+ }) => {
48
+ const [isOpen, setIsOpen] = useState(true)
49
+ const [hasError, setHasError] = useState(false)
50
+ const [isDisableSave, setIsDisableSave] = useState(true)
51
+ const actionsRef = useRef<FormActions>(undefined)
52
+
53
+ // Wait for the close animation to finish before calling onClose.
54
+ const prevIsOpenRef = useRef(true)
55
+ useEffect(() => {
56
+ if (prevIsOpenRef.current && !isOpen) {
57
+ prevIsOpenRef.current = isOpen
58
+ const id = window.setTimeout(() => {
59
+ onClose()
60
+ }, MODAL_ON_CLOSE_NOTIFY_DELAY_MS)
61
+ return () => window.clearTimeout(id)
62
+ }
63
+ prevIsOpenRef.current = isOpen
64
+ }, [isOpen, onClose])
65
+
66
+ const selection = useMemo(
67
+ () => ({
68
+ id: generateNodeID(schema.nodes.affiliation),
69
+ institution: '',
70
+ department: '',
71
+ addressLine1: '',
72
+ addressLine2: '',
73
+ addressLine3: '',
74
+ postCode: '',
75
+ country: '',
76
+ county: '',
77
+ city: '',
78
+ email: { href: '', text: '' },
79
+ priority: affiliationsCount,
80
+ }),
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ []
83
+ )
84
+
85
+ const handleSave = (values: AffiliationAttrs) => {
86
+ onSave({ ...checkID(selection, 'affiliation'), ...values })
87
+ setIsOpen(false)
88
+ }
89
+
90
+ const handleChange = (values: AffiliationAttrs) => {
91
+ const isInstitutionEmpty = !values.institution?.trim()
92
+ setIsDisableSave(isInstitutionEmpty)
93
+ }
94
+
95
+ return (
96
+ <StyledModal
97
+ isOpen={isOpen}
98
+ onRequestClose={() => setIsOpen(false)}
99
+ shouldCloseOnOverlayClick={true}
100
+ >
101
+ <ModalContainer data-cy="create-affiliation-modal">
102
+ <ModalHeader>
103
+ <CloseButton
104
+ onClick={() => setIsOpen(false)}
105
+ data-cy="modal-close-button"
106
+ />
107
+ </ModalHeader>
108
+ <StyledModalBody>
109
+ <StyledScrollableModalContent>
110
+ <FormTitle>Create Affiliation</FormTitle>
111
+ <AffiliationForm
112
+ values={checkID(selection, 'affiliation')}
113
+ onSave={handleSave}
114
+ onChange={handleChange}
115
+ actionsRef={actionsRef}
116
+ newEntity={true}
117
+ onAffiliationErrorChange={setHasError}
118
+ />
119
+ </StyledScrollableModalContent>
120
+ </StyledModalBody>
121
+ <FormFooter
122
+ onCancel={() => setIsOpen(false)}
123
+ primaryAction={
124
+ <ModalFormSaveButton
125
+ form="affiliation-form"
126
+ isDisableSave={isDisableSave || hasError}
127
+ label="New Affiliation"
128
+ onSubmitForm={() => actionsRef.current?.submitForm?.()}
129
+ />
130
+ }
131
+ />
132
+ </ModalContainer>
133
+ </StyledModal>
134
+ )
135
+ }
@@ -0,0 +1,438 @@
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
+
17
+ import {
18
+ CheckboxField,
19
+ CheckboxLabel,
20
+ TextField,
21
+ InputErrorText,
22
+ FormRow,
23
+ FormGroup,
24
+ LabelText,
25
+ RequiredIndicator,
26
+ MultiValueInput,
27
+ } from '@manuscripts/style-guide'
28
+ import { CreditRole } from '@manuscripts/transform'
29
+ import {
30
+ Field,
31
+ FieldProps,
32
+ FormikErrors,
33
+ FormikProps,
34
+ FormikProvider,
35
+ getIn,
36
+ useFormik,
37
+ } from 'formik'
38
+ import React, { MutableRefObject, useEffect } from 'react'
39
+ import styled from 'styled-components'
40
+
41
+ import { ContributorAttrs } from '../../lib/authors'
42
+ import { normalizeAuthor } from '../../lib/normalize'
43
+ import { ChangeHandlingForm } from '../ChangeHandlingForm'
44
+ import {
45
+ UnsavedLabel,
46
+ UnsavedLabelRow,
47
+ FieldUnsavedDot,
48
+ } from '../form/UnsavedLabel'
49
+ import {
50
+ isNamePairError,
51
+ useAuthorShowsErrorIndicator,
52
+ } from '../hooks/useAuthorShowsErrorIndicator'
53
+
54
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
55
+ const ORCID_URL_REGEX =
56
+ /^https:\/\/orcid\.org\/\d{4}-\d{4}-\d{4}-\d{3}[0-9Xx]\/?$/
57
+ const ORCID_INPUT_PATTERN = ORCID_URL_REGEX.source.slice(1, -1)
58
+
59
+ const NAME_PAIR_REQUIRED_MESSAGE = 'Please enter Given Name or Family Name'
60
+
61
+ function isAuthorFieldChanged(
62
+ formik: FormikProps<ContributorAttrs>,
63
+ key: string
64
+ ) {
65
+ const v = normalizeAuthor(formik.values)
66
+ const i = normalizeAuthor(formik.initialValues)
67
+ const va = getIn(v, key as keyof ContributorAttrs)
68
+ const vb = getIn(i, key as keyof ContributorAttrs)
69
+ if (key === 'affiliationIDs' || key === 'creditRoles' || key === 'degrees') {
70
+ return (
71
+ va.length !== vb.length ||
72
+ (va as string[]).some((item, i) => item !== vb[i])
73
+ )
74
+ }
75
+ return va !== vb
76
+ }
77
+
78
+ export { authorShowsErrorIndicator as authorDetailsTabShowsErrorIndicator } from '../hooks/useAuthorShowsErrorIndicator'
79
+
80
+ const AuthorErrorEffect: React.FC<{
81
+ newEntity: boolean
82
+ requiredContinueActive: boolean
83
+ onChange?: (hasError: boolean) => void
84
+ }> = ({ newEntity, requiredContinueActive, onChange }) => {
85
+ useAuthorShowsErrorIndicator(newEntity, requiredContinueActive, onChange)
86
+ return null
87
+ }
88
+
89
+ export interface FormActions {
90
+ reset: () => void
91
+ submitForm: () => Promise<void> | void
92
+ }
93
+
94
+ interface AuthorDetailsFormProps {
95
+ values: ContributorAttrs
96
+ onChange: (values: ContributorAttrs) => void
97
+ onSave: (values: ContributorAttrs) => void
98
+ authorFormRef?: MutableRefObject<HTMLFormElement | null>
99
+ actionsRef?: MutableRefObject<FormActions | undefined>
100
+ isEmailRequired?: boolean
101
+ selectedAffiliations?: string[]
102
+ selectedCreditRoles: CreditRole[]
103
+ newEntity: boolean
104
+ onAuthorDetailsTabErrorChange?: (hasError: boolean) => void
105
+ unsavedContinueActive?: boolean
106
+ requiredContinueActive?: boolean
107
+ }
108
+
109
+ export const AuthorDetailsForm: React.FC<AuthorDetailsFormProps> = ({
110
+ values,
111
+ onChange,
112
+ onSave,
113
+ actionsRef,
114
+ isEmailRequired,
115
+ selectedAffiliations,
116
+ selectedCreditRoles,
117
+ authorFormRef,
118
+ newEntity,
119
+ onAuthorDetailsTabErrorChange,
120
+ unsavedContinueActive = false,
121
+ requiredContinueActive = false,
122
+ }) => {
123
+ const validateAuthor = (values: ContributorAttrs) => {
124
+ const errors: FormikErrors<ContributorAttrs> = {}
125
+ const given = values.given?.trim() ?? ''
126
+ const family = values.family?.trim() ?? ''
127
+ if (!given && !family) {
128
+ errors.given = NAME_PAIR_REQUIRED_MESSAGE
129
+ errors.family = NAME_PAIR_REQUIRED_MESSAGE
130
+ }
131
+
132
+ const email = values.email?.trim()
133
+ if (isEmailRequired && !email) {
134
+ errors.email = 'Email address is required'
135
+ } else if (email && !EMAIL_REGEX.test(email)) {
136
+ errors.email = 'Invalid email address'
137
+ }
138
+
139
+ const orcid = values.ORCID?.trim()
140
+ if (orcid && !ORCID_URL_REGEX.test(orcid)) {
141
+ errors.ORCID =
142
+ 'Please enter a valid ORCID URL: https://orcid.org/xxxx-xxxx-xxxx-xxxx'
143
+ }
144
+ return errors
145
+ }
146
+
147
+ const formik = useFormik<ContributorAttrs>({
148
+ initialValues: values,
149
+ onSubmit: (submitted) => onSave(normalizeAuthor(submitted)),
150
+ enableReinitialize: true,
151
+ validateOnChange: true,
152
+ validate: validateAuthor,
153
+ })
154
+
155
+ useEffect(() => {
156
+ if (selectedAffiliations) {
157
+ formik.setFieldValue('affiliationIDs', selectedAffiliations)
158
+ }
159
+ }, [selectedAffiliations])
160
+
161
+ useEffect(() => {
162
+ if (selectedCreditRoles) {
163
+ formik.setFieldValue('creditRoles', selectedCreditRoles)
164
+ }
165
+ }, [selectedCreditRoles])
166
+
167
+ if (actionsRef) {
168
+ actionsRef.current = {
169
+ reset: () => formik.resetForm(),
170
+ submitForm: () => formik.submitForm(),
171
+ }
172
+ }
173
+
174
+ const showNamePairError = isNamePairError(
175
+ formik,
176
+ newEntity,
177
+ requiredContinueActive
178
+ )
179
+ const showUnsavedDot = (key: string) =>
180
+ unsavedContinueActive && isAuthorFieldChanged(formik, key)
181
+
182
+ return (
183
+ <FormikProvider value={formik}>
184
+ <AuthorErrorEffect
185
+ newEntity={newEntity}
186
+ requiredContinueActive={requiredContinueActive}
187
+ onChange={onAuthorDetailsTabErrorChange}
188
+ />
189
+ <ChangeHandlingForm<ContributorAttrs>
190
+ onChange={(next) => onChange(normalizeAuthor(next))}
191
+ id="author-details-form"
192
+ formRef={authorFormRef}
193
+ noValidate
194
+ >
195
+ <StyledFormGroup>
196
+ <FormRow>
197
+ <Field name={'prefix'}>
198
+ {(props: FieldProps) => (
199
+ <>
200
+ <UnsavedLabel
201
+ htmlFor="prefix"
202
+ showDot={showUnsavedDot('prefix')}
203
+ >
204
+ Prefix
205
+ </UnsavedLabel>
206
+ <TextField
207
+ id={'prefix'}
208
+ {...props.field}
209
+ placeholder="E.g. Doctor"
210
+ />
211
+ </>
212
+ )}
213
+ </Field>
214
+ </FormRow>
215
+ <StyledFormRow>
216
+ <Field name={'given'}>
217
+ {(props: FieldProps) => (
218
+ <>
219
+ <UnsavedLabel
220
+ htmlFor="given-name"
221
+ showDot={showUnsavedDot('given')}
222
+ >
223
+ Given name<RequiredIndicator>*</RequiredIndicator>
224
+ </UnsavedLabel>
225
+ <TextFieldWithError
226
+ id={'given-name'}
227
+ {...props.field}
228
+ error={showNamePairError}
229
+ />
230
+ {showNamePairError && (
231
+ <InputErrorText>
232
+ {NAME_PAIR_REQUIRED_MESSAGE}
233
+ </InputErrorText>
234
+ )}
235
+ </>
236
+ )}
237
+ </Field>
238
+ </StyledFormRow>
239
+ </StyledFormGroup>
240
+ <StyledFormGroup>
241
+ <StyledFormRow>
242
+ <Field name={'family'}>
243
+ {(props: FieldProps) => (
244
+ <>
245
+ <UnsavedLabel
246
+ htmlFor="family-name"
247
+ showDot={showUnsavedDot('family')}
248
+ >
249
+ Family name<RequiredIndicator>*</RequiredIndicator>
250
+ </UnsavedLabel>
251
+ <TextFieldWithError
252
+ id={'family-name'}
253
+ {...props.field}
254
+ error={showNamePairError}
255
+ />
256
+ {showNamePairError && (
257
+ <InputErrorText>
258
+ {NAME_PAIR_REQUIRED_MESSAGE}
259
+ </InputErrorText>
260
+ )}
261
+ </>
262
+ )}
263
+ </Field>
264
+ </StyledFormRow>
265
+ <FormRow>
266
+ <Field name={'suffix'}>
267
+ {(props: FieldProps) => (
268
+ <>
269
+ <UnsavedLabel
270
+ htmlFor="suffix"
271
+ showDot={showUnsavedDot('suffix')}
272
+ >
273
+ Suffix
274
+ </UnsavedLabel>
275
+ <TextField
276
+ id={'suffix'}
277
+ {...props.field}
278
+ placeholder="E.g. Junior"
279
+ />
280
+ </>
281
+ )}
282
+ </Field>
283
+ </FormRow>
284
+ </StyledFormGroup>
285
+ <FormRow>
286
+ <CheckboxContainer data-cy="corresponding-author-container">
287
+ <CheckboxLabel>
288
+ <Field name={'isCorresponding'}>
289
+ {(props: FieldProps) => (
290
+ <CheckboxField
291
+ id={'isCorresponding'}
292
+ checked={props.field.value}
293
+ {...props.field}
294
+ />
295
+ )}
296
+ </Field>
297
+ <UnsavedLabelRow>
298
+ {showUnsavedDot('isCorresponding') ? (
299
+ <FieldUnsavedDot aria-hidden />
300
+ ) : null}
301
+ <LabelText>Corresponding Author</LabelText>
302
+ </UnsavedLabelRow>
303
+ </CheckboxLabel>
304
+ </CheckboxContainer>
305
+ </FormRow>
306
+ <FormRow>
307
+ <Field name={'email'} type={'email'}>
308
+ {(props: FieldProps) => {
309
+ const hasError =
310
+ (getIn(formik.touched, 'email') || requiredContinueActive) &&
311
+ getIn(formik.errors, 'email')
312
+ return (
313
+ <>
314
+ <UnsavedLabel
315
+ htmlFor="email"
316
+ showDot={showUnsavedDot('email')}
317
+ >
318
+ {isEmailRequired ? (
319
+ <LabelText>
320
+ Email address
321
+ <RequiredIndicator>*</RequiredIndicator>
322
+ </LabelText>
323
+ ) : (
324
+ 'Email address'
325
+ )}
326
+ </UnsavedLabel>
327
+ <TextFieldWithError
328
+ id={'email'}
329
+ type="email"
330
+ required={isEmailRequired}
331
+ {...props.field}
332
+ error={hasError}
333
+ />
334
+ {hasError && (
335
+ <InputErrorText>
336
+ {getIn(formik.errors, 'email')}
337
+ </InputErrorText>
338
+ )}
339
+ </>
340
+ )
341
+ }}
342
+ </Field>
343
+ </FormRow>
344
+ <FormRow>
345
+ <Field name={'role'}>
346
+ {(props: FieldProps) => (
347
+ <>
348
+ <UnsavedLabel htmlFor="role" showDot={showUnsavedDot('role')}>
349
+ Job title
350
+ </UnsavedLabel>
351
+ <TextField id={'role'} {...props.field} />
352
+ </>
353
+ )}
354
+ </Field>
355
+ </FormRow>
356
+ <FormRow>
357
+ <Field name={'ORCID'} type={'text'}>
358
+ {(props: FieldProps) => {
359
+ const hasError =
360
+ getIn(formik.touched, 'ORCID') && getIn(formik.errors, 'ORCID')
361
+ return (
362
+ <>
363
+ <UnsavedLabel
364
+ htmlFor="orcid"
365
+ showDot={showUnsavedDot('ORCID')}
366
+ >
367
+ ORCID
368
+ </UnsavedLabel>
369
+ <TextFieldWithError
370
+ id={'orcid'}
371
+ type="url"
372
+ placeholder={'https://orcid.org/...'}
373
+ disabled={values.isAuthenticated}
374
+ {...props.field}
375
+ pattern={ORCID_INPUT_PATTERN}
376
+ title="Please enter a valid ORCID URL: https://orcid.org/xxxx-xxxx-xxxx-xxxx"
377
+ error={hasError}
378
+ />
379
+ {hasError && (
380
+ <InputErrorText>
381
+ {getIn(formik.errors, 'ORCID')}
382
+ </InputErrorText>
383
+ )}
384
+ </>
385
+ )
386
+ }}
387
+ </Field>
388
+ </FormRow>
389
+ <FormRow>
390
+ <UnsavedLabel htmlFor="degrees" showDot={showUnsavedDot('degrees')}>
391
+ Qualification
392
+ </UnsavedLabel>
393
+ <MultiValueInput
394
+ id={'degrees'}
395
+ inputType="text"
396
+ placeholder="E.g. Bsc Computer Science"
397
+ initialValues={
398
+ Array.isArray(formik.values.degrees) ? formik.values.degrees : []
399
+ }
400
+ onChange={(values) => {
401
+ formik.setFieldValue('degrees', values)
402
+ }}
403
+ />
404
+ </FormRow>
405
+ </ChangeHandlingForm>
406
+ </FormikProvider>
407
+ )
408
+ }
409
+
410
+ export const Fieldset = styled.fieldset`
411
+ padding: 0;
412
+ margin: 0;
413
+ border: none;
414
+ `
415
+
416
+ const TextFieldWithError = styled(TextField)`
417
+ &:required::placeholder {
418
+ color: ${(props) => props.theme.colors.text.error};
419
+ }
420
+ `
421
+
422
+ export const CheckboxContainer = styled.div`
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 32px;
426
+ `
427
+ export const StyledFormGroup = styled(FormGroup)`
428
+ [name='prefix'] {
429
+ width: 180px;
430
+ }
431
+ [name='suffix'] {
432
+ width: 180px;
433
+ }
434
+ `
435
+
436
+ export const StyledFormRow = styled(FormRow)`
437
+ flex: 1;
438
+ `