@manuscripts/body-editor 3.12.1 → 3.12.3

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 (67) hide show
  1. package/dist/cjs/components/affiliations/AffiliationItem.js +7 -11
  2. package/dist/cjs/components/affiliations/AffiliationList.js +3 -5
  3. package/dist/cjs/components/affiliations/AffiliationsModal.js +13 -4
  4. package/dist/cjs/components/authors/AuthorDetailsForm.js +44 -34
  5. package/dist/cjs/components/authors/AuthorList.js +4 -2
  6. package/dist/cjs/components/authors/AuthorsModal.js +10 -3
  7. package/dist/cjs/components/authors/DraggableAuthor.js +7 -11
  8. package/dist/cjs/components/awards/AwardForm.js +108 -100
  9. package/dist/cjs/components/hooks/use-debounce.js +15 -0
  10. package/dist/cjs/components/modal-drawer/GenericDrawerGroup.js +1 -0
  11. package/dist/cjs/components/references/CitationEditor.js +6 -7
  12. package/dist/cjs/components/references/ImportBibliographyForm.js +12 -3
  13. package/dist/cjs/components/references/ReferenceForm/PersonDropDown.js +1 -1
  14. package/dist/cjs/components/references/ReferenceForm/styled-components.js +12 -3
  15. package/dist/cjs/components/references/ReferenceSearch.js +2 -2
  16. package/dist/cjs/components/references/ReferenceSearchResults.js +58 -11
  17. package/dist/cjs/components/references/ReferenceSearchSection.js +3 -11
  18. package/dist/cjs/components/references/ReferencesModal.js +5 -4
  19. package/dist/cjs/components/toolbar/InsertTableDialog.js +11 -3
  20. package/dist/cjs/components/views/CrossReferenceItems.js +8 -14
  21. package/dist/cjs/components/views/FootnotesSelector.js +6 -6
  22. package/dist/cjs/components/views/InsertSpecialCharacter.js +24 -10
  23. package/dist/cjs/components/views/LinkForm.js +8 -1
  24. package/dist/cjs/lib/normalize.js +8 -6
  25. package/dist/cjs/lib/popper.js +4 -2
  26. package/dist/cjs/versions.js +1 -1
  27. package/dist/cjs/views/cross_reference_editable.js +1 -0
  28. package/dist/cjs/views/equation_editable.js +6 -0
  29. package/dist/cjs/views/inline_equation_editable.js +6 -0
  30. package/dist/cjs/views/inline_footnote.js +1 -0
  31. package/dist/es/components/affiliations/AffiliationItem.js +8 -12
  32. package/dist/es/components/affiliations/AffiliationList.js +3 -5
  33. package/dist/es/components/affiliations/AffiliationsModal.js +14 -5
  34. package/dist/es/components/authors/AuthorDetailsForm.js +43 -33
  35. package/dist/es/components/authors/AuthorList.js +4 -2
  36. package/dist/es/components/authors/AuthorsModal.js +11 -4
  37. package/dist/es/components/authors/DraggableAuthor.js +8 -12
  38. package/dist/es/components/awards/AwardForm.js +110 -102
  39. package/dist/es/components/hooks/use-debounce.js +15 -1
  40. package/dist/es/components/modal-drawer/GenericDrawerGroup.js +2 -1
  41. package/dist/es/components/references/CitationEditor.js +7 -8
  42. package/dist/es/components/references/ImportBibliographyForm.js +14 -5
  43. package/dist/es/components/references/ReferenceForm/PersonDropDown.js +1 -1
  44. package/dist/es/components/references/ReferenceForm/styled-components.js +13 -4
  45. package/dist/es/components/references/ReferenceSearch.js +3 -3
  46. package/dist/es/components/references/ReferenceSearchResults.js +26 -12
  47. package/dist/es/components/references/ReferenceSearchSection.js +4 -12
  48. package/dist/es/components/references/ReferencesModal.js +6 -5
  49. package/dist/es/components/toolbar/InsertTableDialog.js +11 -3
  50. package/dist/es/components/views/CrossReferenceItems.js +9 -15
  51. package/dist/es/components/views/FootnotesSelector.js +7 -7
  52. package/dist/es/components/views/InsertSpecialCharacter.js +25 -11
  53. package/dist/es/components/views/LinkForm.js +9 -2
  54. package/dist/es/lib/normalize.js +8 -6
  55. package/dist/es/lib/popper.js +4 -2
  56. package/dist/es/versions.js +1 -1
  57. package/dist/es/views/cross_reference_editable.js +1 -0
  58. package/dist/es/views/equation_editable.js +6 -0
  59. package/dist/es/views/inline_equation_editable.js +6 -0
  60. package/dist/es/views/inline_footnote.js +1 -0
  61. package/dist/types/components/authors/AuthorDetailsForm.d.ts +2 -2
  62. package/dist/types/components/hooks/use-debounce.d.ts +1 -0
  63. package/dist/types/components/references/ReferenceForm/styled-components.d.ts +11 -1
  64. package/dist/types/components/references/ReferenceSearchResults.d.ts +20 -2
  65. package/dist/types/versions.d.ts +1 -1
  66. package/package.json +2 -2
  67. package/styles/popper.css +2 -0
@@ -1,7 +1,7 @@
1
- import { AffiliationIcon, CrclTickAnimation, DeleteIcon, } from '@manuscripts/style-guide';
1
+ import { AffiliationIcon, CrclTickAnimation, DeleteIcon, SecondaryIconButton, withNavigableListItem, } from '@manuscripts/style-guide';
2
2
  import React, { useRef } from 'react';
3
3
  import styled from 'styled-components';
4
- const AffiliationContainer = styled.div `
4
+ const AffiliationContainer = withNavigableListItem(styled.div `
5
5
  padding: ${(props) => props.theme.grid.unit * 2}px 0
6
6
  ${(props) => props.theme.grid.unit * 2}px;
7
7
  display: flex;
@@ -19,7 +19,7 @@ const AffiliationContainer = styled.div `
19
19
  &.active {
20
20
  border-color: ${(props) => props.theme.colors.border.primary};
21
21
  }
22
- `;
22
+ `);
23
23
  const AffiliationName = styled.div `
24
24
  margin-left: 12px;
25
25
  flex: 1;
@@ -43,15 +43,11 @@ const AffiliationBox = styled.div `
43
43
  flex-direction: column;
44
44
  margin-left: 4px;
45
45
  `;
46
- const RemoveButton = styled.div `
47
- display: flex;
48
- align-items: center;
49
- margin-right: 8px;
46
+ const RemoveButton = styled(SecondaryIconButton) `
50
47
  svg {
51
- cursor: pointer;
52
- }
53
- .icon_element {
54
- fill: #6e6e6e;
48
+ path[fill='white'] {
49
+ fill: white;
50
+ }
55
51
  }
56
52
  `;
57
53
  export const AffiliationItem = ({ affiliation, isSelected, onClick, onDelete, showSuccessIcon, }) => {
@@ -75,6 +71,6 @@ export const AffiliationItem = ({ affiliation, isSelected, onClick, onDelete, sh
75
71
  affiliation.country ? ', ' : '')),
76
72
  affiliation.country && React.createElement(React.Fragment, null, affiliation.country))),
77
73
  showSuccessIcon && (React.createElement(CrclTickAnimation, { size: 36, style: { position: 'absolute', left: '10px' } })),
78
- isSelected && (React.createElement(RemoveButton, { onClick: () => onDelete(), "data-tooltip-content": "Delete" },
74
+ isSelected && (React.createElement(RemoveButton, { size: 13, onClick: () => onDelete(), "data-tooltip-content": "Delete" },
79
75
  React.createElement(DeleteIcon, { fill: '#6E6E6E' })))));
80
76
  };
@@ -16,18 +16,16 @@
16
16
  import React from 'react';
17
17
  import styled from 'styled-components';
18
18
  import { AffiliationItem } from './AffiliationItem';
19
- const AffiliationListContainer = styled.div `
19
+ import { withListNavigation } from '@manuscripts/style-guide';
20
+ const AffiliationListContainer = withListNavigation(styled.div `
20
21
  flex: 1;
21
22
  overflow-y: visible;
22
- margin-left: 16px;
23
- `;
23
+ `);
24
24
  const AffiliationListTitle = styled.h2 `
25
25
  color: #6e6e6e;
26
26
  font-size: 18px;
27
27
  font-weight: 400;
28
28
  line-height: 24px;
29
- margin-left: 14px;
30
- margin-top: 20px;
31
29
  `;
32
30
  export const AffiliationList = ({ affiliation, affiliations, onSelect, onDelete, lastSavedAffiliationId, }) => {
33
31
  return (React.createElement(React.Fragment, null,
@@ -1,4 +1,4 @@
1
- import { AddIcon, AddUserIcon, AffiliationPlaceholderIcon, CloseButton, InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabPanels, InspectorTabs, ModalBody, ModalContainer, ModalHeader, ModalSidebar, ModalSidebarHeader, ModalSidebarTitle, ScrollableModalContent, SidebarContent, StyledModal, } from '@manuscripts/style-guide';
1
+ import { AddIcon, AddUserIcon, AffiliationPlaceholderIcon, CloseButton, InspectorTab, InspectorTabList, InspectorTabPanel, InspectorTabPanels, InspectorTabs, outlineStyle, ModalBody, ModalContainer, ModalHeader, ModalSidebar, ModalSidebarHeader, ModalSidebarTitle, ScrollableModalContent, SidebarContent, StyledModal, } from '@manuscripts/style-guide';
2
2
  import { generateNodeID, schema } from '@manuscripts/transform';
3
3
  import { isEqual } from 'lodash';
4
4
  import React, { useCallback, useEffect, useReducer, useRef, useState, } from 'react';
@@ -328,7 +328,9 @@ export const AffiliationsModal = ({ authors: $authors, affiliations: $affiliatio
328
328
  React.createElement(AffiliationList, { affiliation: selection, affiliations: affiliations, onSelect: handleSelect, onDelete: handleShowDeleteDialog, lastSavedAffiliationId: savedAffiliationId }))),
329
329
  React.createElement(ScrollableModalContent, { "data-cy": "affiliations-modal-content" }, selection ? (React.createElement(React.Fragment, null,
330
330
  React.createElement(AffiliationTabs, null,
331
- React.createElement(ModalFormActions, { type: 'affiliation', form: 'affiliation-form', onDelete: handleDeleteAffiliation, showingDeleteDialog: showingDeleteDialog, showDeleteDialog: handleShowDeleteDialog, newEntity: newAffiliation, isDisableSave: isDisableSave }),
331
+ React.createElement(ModalFormActions, { type: 'affiliation', form: 'affiliation-form', onDelete: handleDeleteAffiliation, showingDeleteDialog: showingDeleteDialog &&
332
+ !(showConfirmationDialog ||
333
+ showRequiredFieldConfirmationDialog), showDeleteDialog: handleShowDeleteDialog, newEntity: newAffiliation, isDisableSave: isDisableSave }),
332
334
  React.createElement(InspectorTabList, null,
333
335
  React.createElement(InspectorTab, null, "Details"),
334
336
  React.createElement(InspectorTab, null, "Authors")),
@@ -363,9 +365,15 @@ function createEmptyAffiliation(priority) {
363
365
  };
364
366
  }
365
367
  const StyledSidebarContent = styled(SidebarContent) `
366
- padding: 0;
368
+ padding: 8px;
367
369
  `;
368
- const AddAffiliationButton = styled.div `
370
+ const AddAffiliationButton = styled.button `
371
+ background: none;
372
+ border: none;
373
+ margin: 0;
374
+ font: inherit;
375
+ color: inherit;
376
+ width: 100%;
369
377
  display: flex;
370
378
  align-items: center;
371
379
  padding: 12px 8px 12px 12px;
@@ -376,6 +384,7 @@ const AddAffiliationButton = styled.div `
376
384
  border-left: 0;
377
385
  border-right: 0;
378
386
  }
387
+ ${outlineStyle}
379
388
  `;
380
389
  const ActionTitle = styled.div `
381
390
  padding-left: ${(props) => props.theme.grid.unit * 2}px;
@@ -383,7 +392,7 @@ const ActionTitle = styled.div `
383
392
  const AffiliationTabs = styled(InspectorTabs) `
384
393
  position: relative;
385
394
  `;
386
- const AffiliationTabPanel = styled(InspectorTabPanel).attrs({ unmount: false }) `
395
+ const AffiliationTabPanel = styled(InspectorTabPanel).attrs({ tabIndex: -1, unmount: false }) `
387
396
  margin-top: ${(props) => props.theme.grid.unit * 4}px;
388
397
  `;
389
398
  const StyledModalBody = styled(ModalBody) `
@@ -17,25 +17,11 @@ import { CheckboxField, CheckboxLabel, TextField, InputErrorText, Label, FormRow
17
17
  import { Field, Formik, getIn, } from 'formik';
18
18
  import React, { useEffect, useRef } from 'react';
19
19
  import styled from 'styled-components';
20
+ import { normalizeAuthor } from '../../lib/normalize';
20
21
  import { ChangeHandlingForm } from '../ChangeHandlingForm';
21
- export const Fieldset = styled.fieldset `
22
- padding: 0;
23
- margin: 0;
24
- border: none;
25
- `;
26
- const OrcidContainer = styled.div `
27
- margin: 16px 0 0;
28
- `;
29
- const TextFieldWithError = styled(TextField) `
30
- &:required::placeholder {
31
- color: ${(props) => props.theme.colors.text.error};
32
- }
33
- `;
34
- export const CheckboxContainer = styled.div `
35
- display: flex;
36
- align-items: center;
37
- gap: 32px;
38
- `;
22
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
23
+ const ORCID_URL_REGEX = /^https:\/\/orcid\.org\/\d{4}-\d{4}-\d{4}-\d{3}[0-9Xx]\/?$/;
24
+ const ORCID_INPUT_PATTERN = ORCID_URL_REGEX.source.slice(1, -1);
39
25
  export const AuthorDetailsForm = ({ values, onChange, onSave, actionsRef, isEmailRequired, selectedAffiliations, selectedCreditRoles, authorFormRef, }) => {
40
26
  const formRef = useRef(null);
41
27
  useEffect(() => {
@@ -57,17 +43,22 @@ export const AuthorDetailsForm = ({ values, onChange, onSave, actionsRef, isEmai
57
43
  }
58
44
  const validateAuthor = (values) => {
59
45
  const errors = {};
60
- if (isEmailRequired && !values.email) {
46
+ const email = values.email?.trim();
47
+ if (isEmailRequired && !email) {
61
48
  errors.email = 'Email address is required';
62
49
  }
63
- else if (values.email &&
64
- !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
50
+ else if (email && !EMAIL_REGEX.test(email)) {
65
51
  errors.email = 'Invalid email address';
66
52
  }
53
+ const orcid = values.ORCID?.trim();
54
+ if (orcid && !ORCID_URL_REGEX.test(orcid)) {
55
+ errors.ORCID =
56
+ 'Please enter a valid ORCID URL: https://orcid.org/xxxx-xxxx-xxxx-xxxx';
57
+ }
67
58
  return errors;
68
59
  };
69
- return (React.createElement(Formik, { initialValues: values, onSubmit: onSave, enableReinitialize: true, validateOnChange: true, innerRef: formRef, validate: validateAuthor }, (formik) => {
70
- return (React.createElement(ChangeHandlingForm, { onChange: onChange, id: "author-details-form", formRef: authorFormRef, noValidate: true },
60
+ return (React.createElement(Formik, { initialValues: values, onSubmit: (submitted) => onSave(normalizeAuthor(submitted)), enableReinitialize: true, validateOnChange: true, innerRef: formRef, validate: validateAuthor }, (formik) => {
61
+ return (React.createElement(ChangeHandlingForm, { onChange: (next) => onChange(normalizeAuthor(next)), id: "author-details-form", formRef: authorFormRef, noValidate: true },
71
62
  React.createElement(FormRow, null,
72
63
  React.createElement(Field, { name: 'prefix' }, (props) => (React.createElement(React.Fragment, null,
73
64
  React.createElement(Label, { htmlFor: "prefix" }, "Prefix"),
@@ -95,15 +86,34 @@ export const AuthorDetailsForm = ({ values, onChange, onSave, actionsRef, isEmai
95
86
  React.createElement(TextFieldWithError, { id: 'email', type: "email", required: isEmailRequired, ...props.field, error: hasError }),
96
87
  hasError && (React.createElement(InputErrorText, null, getIn(formik.errors, 'email')))));
97
88
  })),
98
- React.createElement(CheckboxContainer, null,
99
- React.createElement(CheckboxLabel, null,
100
- React.createElement(Field, { name: 'isCorresponding' }, (props) => (React.createElement(CheckboxField, { id: 'isCorresponding', checked: props.field.value, ...props.field }))),
101
- React.createElement(LabelText, null, "Corresponding Author"))),
102
- React.createElement(OrcidContainer, null,
103
- React.createElement(FormRow, null,
104
- React.createElement(Label, { htmlFor: "orcid" }, "ORCID"),
105
- React.createElement(Field, { name: 'ORCID', type: 'text' }, (props) => (React.createElement(React.Fragment, null,
106
- React.createElement(Label, { htmlFor: "orcid", className: "sr-only" }, "ORCID"),
107
- React.createElement(TextField, { id: 'orcid', placeholder: 'https://orcid.org/...', pattern: "https://orcid\\.org/\\d{4}-\\d{4}-\\d{4}-\\d{3}[0-9Xx]", title: "Please enter a valid ORCID URL format: https://orcid.org/xxxx-xxxx-xxxx-xxxx", ...props.field }))))))));
89
+ React.createElement(FormRow, null,
90
+ React.createElement(CheckboxContainer, { "data-cy": "corresponding-author-container" },
91
+ React.createElement(CheckboxLabel, null,
92
+ React.createElement(Field, { name: 'isCorresponding' }, (props) => (React.createElement(CheckboxField, { id: 'isCorresponding', checked: props.field.value, ...props.field }))),
93
+ React.createElement(LabelText, null, "Corresponding Author")))),
94
+ React.createElement(FormRow, null,
95
+ React.createElement(Field, { name: 'ORCID', type: 'text' }, (props) => {
96
+ const hasError = getIn(formik.touched, 'ORCID') &&
97
+ getIn(formik.errors, 'ORCID');
98
+ return (React.createElement(React.Fragment, null,
99
+ React.createElement(Label, { htmlFor: "orcid" }, "ORCID"),
100
+ React.createElement(TextFieldWithError, { id: 'orcid', type: "url", placeholder: 'https://orcid.org/...', ...props.field, pattern: ORCID_INPUT_PATTERN, title: "Please enter a valid ORCID URL: https://orcid.org/xxxx-xxxx-xxxx-xxxx", error: hasError }),
101
+ hasError && (React.createElement(InputErrorText, null, getIn(formik.errors, 'ORCID')))));
102
+ }))));
108
103
  }));
109
104
  };
105
+ export const Fieldset = styled.fieldset `
106
+ padding: 0;
107
+ margin: 0;
108
+ border: none;
109
+ `;
110
+ const TextFieldWithError = styled(TextField) `
111
+ &:required::placeholder {
112
+ color: ${(props) => props.theme.colors.text.error};
113
+ }
114
+ `;
115
+ export const CheckboxContainer = styled.div `
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 32px;
119
+ `;
@@ -18,10 +18,12 @@ import { DndProvider } from 'react-dnd';
18
18
  import { HTML5Backend } from 'react-dnd-html5-backend';
19
19
  import styled from 'styled-components';
20
20
  import { DraggableAuthor } from './DraggableAuthor';
21
- const AuthorListContainer = styled.div `
21
+ import { withListNavigation } from '@manuscripts/style-guide';
22
+ const AuthorListContainer = withListNavigation(styled.div `
22
23
  flex: 1;
23
24
  overflow-y: visible;
24
- `;
25
+ padding: 0 6px;
26
+ `);
25
27
  const AuthorListTitle = styled.h2 `
26
28
  color: #6e6e6e;
27
29
  font-size: 18px;
@@ -13,7 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { AddIcon, AddInstitutionIcon, AddRoleIcon, AuthorPlaceholderIcon, CloseButton, ModalBody, ModalContainer, ModalHeader, ModalSidebar, ModalSidebarHeader, ModalSidebarTitle, ScrollableModalContent, SidebarContent, StyledModal, InspectorTabs, InspectorTabPanel, InspectorTabList, InspectorTab, InspectorTabPanels, } from '@manuscripts/style-guide';
16
+ import { AddIcon, AddInstitutionIcon, AddRoleIcon, AuthorPlaceholderIcon, CloseButton, ModalBody, ModalContainer, ModalHeader, ModalSidebar, ModalSidebarHeader, ModalSidebarTitle, outlineStyle, ScrollableModalContent, SidebarContent, StyledModal, InspectorTabs, InspectorTabPanel, InspectorTabList, InspectorTab, InspectorTabPanels, } from '@manuscripts/style-guide';
17
17
  import { generateNodeID, schema, } from '@manuscripts/transform';
18
18
  import { cloneDeep, isEqual, omit } from 'lodash';
19
19
  import React, { useCallback, useEffect, useReducer, useRef, useState, } from 'react';
@@ -346,7 +346,13 @@ function createEmptyAuthor(priority) {
346
346
  prefix: '',
347
347
  };
348
348
  }
349
- const AddAuthorButton = styled.div `
349
+ const AddAuthorButton = styled.button `
350
+ background: none;
351
+ border: none;
352
+ margin: 0;
353
+ font: inherit;
354
+ color: inherit;
355
+ width: 100%;
350
356
  display: flex;
351
357
  align-items: center;
352
358
  padding: 12px 8px 12px 12px;
@@ -357,6 +363,7 @@ const AddAuthorButton = styled.div `
357
363
  border-left: 0;
358
364
  border-right: 0;
359
365
  }
366
+ ${outlineStyle}
360
367
  `;
361
368
  const ActionTitle = styled.div `
362
369
  padding-left: ${(props) => props.theme.grid.unit * 2}px;
@@ -364,11 +371,11 @@ const ActionTitle = styled.div `
364
371
  const AuthorTabs = styled(InspectorTabs) `
365
372
  position: relative;
366
373
  `;
367
- const AuthorTabPanel = styled(InspectorTabPanel).attrs({ unmount: false }) `
374
+ const AuthorTabPanel = styled(InspectorTabPanel).attrs({ tabIndex: -1, unmount: false }) `
368
375
  margin-top: ${(props) => props.theme.grid.unit * 4}px;
369
376
  `;
370
377
  const StyledSidebarContent = styled(SidebarContent) `
371
- padding: 0;
378
+ padding: 8px;
372
379
  `;
373
380
  const StyledModalBody = styled(ModalBody) `
374
381
  position: relative;
@@ -13,13 +13,13 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { Avatar, CorrespondingAuthorIcon, CrclTickAnimation, DeleteIcon, DraggableIcon, } from '@manuscripts/style-guide';
16
+ import { Avatar, CorrespondingAuthorIcon, CrclTickAnimation, DeleteIcon, DraggableIcon, SecondaryIconButton, withNavigableListItem, } from '@manuscripts/style-guide';
17
17
  import React, { useRef, useState } from 'react';
18
18
  import { useDrag, useDrop } from 'react-dnd';
19
19
  import styled from 'styled-components';
20
20
  import { authorLabel } from '../../lib/authors';
21
21
  import { getDropSide } from '../../lib/dnd';
22
- const AuthorContainer = styled.div `
22
+ const AuthorContainer = withNavigableListItem(styled.div `
23
23
  padding: ${(props) => props.theme.grid.unit * 2}px 0
24
24
  ${(props) => props.theme.grid.unit * 2}px;
25
25
  display: flex;
@@ -53,7 +53,7 @@ const AuthorContainer = styled.div `
53
53
  cursor: grabbing;
54
54
  border-bottom-color: ${(props) => props.theme.colors.brand.dark};
55
55
  }
56
- `;
56
+ `);
57
57
  const AvatarContainer = styled.div `
58
58
  display: inline-flex;
59
59
  position: relative;
@@ -86,15 +86,11 @@ const DragHandle = styled(DraggableIcon) `
86
86
  margin-left: -4px;
87
87
  margin-right: -12px;
88
88
  `;
89
- const RemoveButton = styled.div `
90
- display: flex;
91
- align-items: center;
92
- margin-right: 8px;
89
+ const RemoveButton = styled(SecondaryIconButton) `
93
90
  svg {
94
- cursor: pointer;
95
- }
96
- .icon_element {
97
- fill: #6e6e6e;
91
+ path[fill='white'] {
92
+ fill: white;
93
+ }
98
94
  }
99
95
  `;
100
96
  const StyledCrclTickAnimation = styled(CrclTickAnimation) `
@@ -162,6 +158,6 @@ export const DraggableAuthor = React.memo(({ author, isSelected, onClick, onDele
162
158
  React.createElement(AuthorNotes, { "data-cy": "author-notes" }, author.isCorresponding && (React.createElement(AuthorBadge, null,
163
159
  React.createElement(CorrespondingAuthorIcon, null))))),
164
160
  React.createElement(AuthorName, { "data-cy": "author-name" }, authorLabel(author)),
165
- isSelected && (React.createElement(RemoveButton, { onClick: () => onDelete(), "data-tooltip-content": "Delete" },
161
+ isSelected && (React.createElement(RemoveButton, { size: 13, onClick: () => onDelete(), "data-tooltip-content": "Delete" },
166
162
  React.createElement(DeleteIcon, { fill: '#6E6E6E' })))));
167
163
  });
@@ -13,58 +13,49 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { MultiValueInput, PrimaryButton, SearchIcon, SecondaryButton, TextField, FormRow, Label, FormActionsBar, } from '@manuscripts/style-guide';
16
+ import { MultiValueInput, PrimaryButton, SearchIcon, SecondaryButton, TextField, FormRow, Label, ButtonGroup, } from '@manuscripts/style-guide';
17
17
  import { Field, Formik } from 'formik';
18
- import React, { useEffect, useRef, useState } from 'react';
19
- import styled from 'styled-components';
18
+ import React, { useMemo, useRef } from 'react';
19
+ import styled, { useTheme } from 'styled-components';
20
20
  import { ChangeHandlingForm } from '../ChangeHandlingForm';
21
- import { useDebounce } from '../hooks/use-debounce';
21
+ import { components, } from 'react-select';
22
+ import AsyncCreatableSelect from 'react-select/async-creatable';
23
+ import useDebounced from '../hooks/use-debounce';
24
+ const loadOptions = async (inputValue) => {
25
+ try {
26
+ const formattedQuery = inputValue.replace(/\s+/g, '+');
27
+ const response = await fetch(`https://api.crossref.org/funders?query=${encodeURIComponent(formattedQuery)}`);
28
+ if (!response.ok) {
29
+ console.log(`HTTP error! status: ${response.status}`);
30
+ return [];
31
+ }
32
+ const data = await response.json();
33
+ const funderOptions = data.message.items.map((funder) => ({
34
+ value: funder.name,
35
+ label: funder.name,
36
+ }));
37
+ return funderOptions.sort((a, b) => a.label.localeCompare(b.label));
38
+ }
39
+ catch (e) {
40
+ console.error('Error fetching funders:', e);
41
+ return [];
42
+ }
43
+ };
22
44
  export const AwardForm = ({ values, onSave, onCancel, onChange, }) => {
23
- const [funders, setFunders] = useState([]);
24
- const [isLoading, setIsLoading] = useState(false);
25
- const [searchQuery, setSearchQuery] = useState('');
26
45
  const formRef = useRef(null);
27
46
  const primaryButtonText = values.source ? 'Update funder' : 'Add funder';
28
- const debouncedSearchQuery = useDebounce(searchQuery, 300);
29
- useEffect(() => {
30
- const searchFunders = async () => {
31
- const query = debouncedSearchQuery;
32
- if (!query) {
33
- setFunders([]);
34
- return;
35
- }
36
- setIsLoading(true);
37
- try {
38
- const formattedQuery = query.replace(/\s+/g, '+');
39
- const response = await fetch(`https://api.crossref.org/funders?query=${encodeURIComponent(formattedQuery)}`);
40
- if (!response.ok) {
41
- throw new Error(`HTTP error! status: ${response.status}`);
42
- }
43
- const data = await response.json();
44
- const funderOptions = data.message.items.map((funder) => ({
45
- value: funder.name,
46
- label: funder.name,
47
- }));
48
- funderOptions.sort((a, b) => a.label.localeCompare(b.label));
49
- setFunders(funderOptions);
50
- }
51
- catch (error) {
52
- console.error('Error fetching funders:', error);
53
- setFunders([]);
54
- }
55
- finally {
56
- setIsLoading(false);
57
- }
58
- };
59
- searchFunders();
60
- }, [debouncedSearchQuery]);
61
- const handleFunderSearch = (event) => {
62
- setSearchQuery(event.target.value);
63
- };
47
+ const themes = useTheme();
48
+ const customStyles = useMemo(() => getCustomStyles(themes), [themes]);
49
+ const handleOnLoadFunders = useDebounced(async (inputValue) => loadOptions(inputValue), 300);
64
50
  const handleCancel = () => {
65
51
  formRef.current?.resetForm();
66
52
  onCancel();
67
53
  };
54
+ const handleInputChange = (value, action) => {
55
+ if (action.action === 'input-change') {
56
+ formRef.current?.setFieldValue('source', value);
57
+ }
58
+ };
68
59
  const validate = (values) => {
69
60
  const errors = {};
70
61
  if (!values.source) {
@@ -80,18 +71,14 @@ export const AwardForm = ({ values, onSave, onCancel, onChange, }) => {
80
71
  React.createElement(Field, { type: "hidden", name: "id" }),
81
72
  React.createElement(FormRow, null,
82
73
  React.createElement(Label, { htmlFor: 'source' }, "Funder name"),
83
- React.createElement(SearchContainer, null,
84
- React.createElement(SearchIconContainer, null,
85
- React.createElement(SearchIcon, null)),
86
- React.createElement(Field, { name: "source" }, (props) => (React.createElement(StyledTextField, { id: "source", placeholder: "Search for funder...", onChange: (e) => {
87
- props.field.onChange(e);
88
- handleFunderSearch(e);
89
- }, value: props.field.value || '' }))),
90
- isLoading && React.createElement(LoadingText, null, "Loading..."),
91
- funders.length > 0 && (React.createElement(SearchResults, null, funders.map((funder) => (React.createElement(SearchResultItem, { key: funder.value, onClick: () => {
92
- formik.setFieldValue('source', funder.value);
93
- setFunders([]);
94
- } }, funder.label)))))),
74
+ React.createElement(Field, { name: "source" }, (props) => (React.createElement(AsyncCreatableSelect, { inputId: 'source', loadOptions: handleOnLoadFunders, onInputChange: handleInputChange, autoFocus: true, onChange: (option) => option &&
75
+ formRef.current?.setFieldValue('source', option.value), inputValue: props.field.value, value: null, isValidNewOption: () => false, components: {
76
+ Input,
77
+ Control,
78
+ Option,
79
+ DropdownIndicator: null,
80
+ IndicatorSeparator: null,
81
+ }, styles: customStyles, placeholder: "Search for funder..." }))),
95
82
  formik.errors.source && formik.touched.source && (React.createElement(ErrorText, null, formik.errors.source))),
96
83
  React.createElement(FormRow, null,
97
84
  React.createElement(Label, { htmlFor: 'code' }, "Grant number"),
@@ -102,68 +89,89 @@ export const AwardForm = ({ values, onSave, onCancel, onChange, }) => {
102
89
  React.createElement(Label, { htmlFor: 'recipient' }, "Recipient name"),
103
90
  React.createElement(Field, { name: "recipient" }, (props) => (React.createElement(TextField, { id: "recipient", placeholder: "Enter full name", ...props.field })))),
104
91
  React.createElement(FormRow, null,
105
- React.createElement(FormActionsBar, null,
92
+ React.createElement(FormActions, null,
106
93
  React.createElement(SecondaryButton, { onClick: handleCancel }, "Cancel"),
107
94
  React.createElement(PrimaryButton, { type: "submit", disabled: !formik.dirty || formik.isSubmitting }, primaryButtonText)))));
108
95
  }));
109
96
  };
110
- const SearchContainer = styled.div `
111
- position: relative;
112
- width: 100%;
113
- `;
114
97
  const SearchIconContainer = styled.span `
115
98
  display: flex;
116
- left: ${(props) => props.theme.grid.unit * 4}px;
117
- position: absolute;
118
- top: 50%;
119
- transform: translateY(-50%);
120
- z-index: 2;
99
+ align-items: center;
100
+ padding-left: ${(props) => props.theme.grid.unit * 4}px;
101
+ padding-right: ${(props) => props.theme.grid.unit * 2}px;
121
102
 
122
103
  path {
123
- stroke: ${(props) => props.theme.colors.text.primary};
104
+ stroke: ${(props) => props.isFocused
105
+ ? props.theme.colors.brand.medium
106
+ : props.theme.colors.text.primary};
124
107
  }
108
+ `;
109
+ const OptionWrapper = styled.div `
110
+ padding-left: ${(props) => props.theme.grid.unit * 4}px;
111
+ padding-top: ${(props) => props.theme.grid.unit * 2}px;
112
+ padding-bottom: ${(props) => props.theme.grid.unit * 2}px;
125
113
 
126
- ${SearchContainer}:hover &,
127
- ${SearchContainer}:focus-within & {
128
- path {
129
- stroke: ${(props) => props.theme.colors.brand.medium};
114
+ background-color: ${(props) => {
115
+ if (props.selected) {
116
+ return props.theme.colors.background.selected;
130
117
  }
131
- }
132
- `;
133
- const StyledTextField = styled(TextField) `
134
- padding-left: ${(props) => props.theme.grid.unit * 11}px;
135
- &:hover,
136
- &:focus {
137
- background-color: ${(props) => props.theme.colors.background.fifth};
138
- }
139
- `;
140
- const SearchResults = styled.div `
141
- position: absolute;
142
- top: 100%;
143
- left: 0;
144
- right: 0;
145
- max-height: 200px;
146
- overflow-y: auto;
147
- background: white;
148
- border: 1px solid ${(props) => props.theme.colors.border.secondary};
149
- border-radius: 4px;
150
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
151
- z-index: 1000;
152
- `;
153
- const SearchResultItem = styled.div `
154
- padding: 8px 12px;
155
- cursor: pointer;
118
+ if (props.focused) {
119
+ return props.theme.colors.background.fifth;
120
+ }
121
+ return 'transparent';
122
+ }};
123
+
156
124
  &:hover {
157
125
  background-color: ${(props) => props.theme.colors.background.fifth};
158
126
  }
159
127
  `;
160
- const LoadingText = styled.div `
161
- position: absolute;
162
- right: 12px;
163
- top: 50%;
164
- transform: translateY(-50%);
165
- color: ${(props) => props.theme.colors.text.secondary};
166
- font-size: ${(props) => props.theme.font.size.small};
128
+ const Option = ({ innerProps, innerRef, data, isFocused, isSelected, }) => {
129
+ return (React.createElement(OptionWrapper, { ...innerProps, ref: innerRef, focused: isFocused, selected: isSelected }, data.label));
130
+ };
131
+ const Control = ({ children, ...props }) => {
132
+ return (React.createElement(components.Control, { ...props },
133
+ React.createElement(SearchIconContainer, { isFocused: props.isFocused },
134
+ React.createElement(SearchIcon, null)),
135
+ children));
136
+ };
137
+ const Input = (props) => (React.createElement(components.Input, { ...props, isHidden: false }));
138
+ const getCustomStyles = (theme) => ({
139
+ control: (provided, state) => ({
140
+ ...provided,
141
+ minHeight: '40px',
142
+ borderRadius: '4px',
143
+ border: `${state.isFocused ? '2px' : '1px'} solid ${state.isFocused ? theme.colors.brand.default : theme.colors.border.secondary}`,
144
+ backgroundColor: state.isFocused || state.menuIsOpen
145
+ ? theme.colors.background.fifth
146
+ : theme.colors.background.primary,
147
+ boxShadow: 'none',
148
+ cursor: 'text',
149
+ transition: 'background-color 0.2s',
150
+ '&:hover': {
151
+ backgroundColor: theme.colors.background.fifth,
152
+ },
153
+ }),
154
+ input: (provided) => ({
155
+ ...provided,
156
+ margin: 0,
157
+ padding: 0,
158
+ color: theme.colors.text.primary,
159
+ fontSize: theme.font.size.normal,
160
+ fontFamily: theme.font.family.sans,
161
+ }),
162
+ placeholder: (provided, state) => ({
163
+ ...provided,
164
+ margin: 0,
165
+ fontStyle: 'italic',
166
+ color: state.isFocused ? '#c9c9c9' : theme.colors.text.secondary,
167
+ fontSize: theme.font.size.medium,
168
+ fontFamily: theme.font.family.sans,
169
+ fontWeight: theme.font.weight.normal,
170
+ lineHeight: theme.font.lineHeight.large,
171
+ }),
172
+ });
173
+ const FormActions = styled(ButtonGroup) `
174
+ margin: ${(props) => props.theme.grid.unit * 2}px;
167
175
  `;
168
176
  const ErrorText = styled.div `
169
177
  color: ${(props) => props.theme.colors.text.error};
@@ -13,7 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { useEffect, useState } from 'react';
16
+ import { useCallback, useEffect, useRef, useState } from 'react';
17
17
  export const useDebounce = (value, delay) => {
18
18
  const [debouncedValue, setDebouncedValue] = useState(value);
19
19
  useEffect(() => {
@@ -26,3 +26,17 @@ export const useDebounce = (value, delay) => {
26
26
  }, [value, delay]);
27
27
  return debouncedValue;
28
28
  };
29
+ export default function useDebounced(fn, delay) {
30
+ const timeoutRef = useRef(null);
31
+ return useCallback((arg) => {
32
+ const timeout = timeoutRef.current;
33
+ if (timeout) {
34
+ clearTimeout(timeout);
35
+ }
36
+ return new Promise((resolve) => {
37
+ timeoutRef.current = setTimeout(async () => {
38
+ resolve(await fn(arg));
39
+ }, delay);
40
+ });
41
+ }, [fn, delay]);
42
+ }
@@ -13,7 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { SelectedItemsBox } from '@manuscripts/style-guide';
16
+ import { outlineStyle, SelectedItemsBox, } from '@manuscripts/style-guide';
17
17
  import React from 'react';
18
18
  import styled from 'styled-components';
19
19
  export function DrawerGroup({ removeItem, selectedItems, onSelect, items, showDrawer, setShowDrawer, title, labelField, Drawer, cy, Icon, buttonText, }) {
@@ -43,4 +43,5 @@ export const AssignButton = styled.button `
43
43
  &:hover {
44
44
  opacity: 0.8;
45
45
  }
46
+ ${outlineStyle}
46
47
  `;