@manuscripts/body-editor 3.12.33 → 3.12.34

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 (65) hide show
  1. package/dist/cjs/components/ChangeHandlingForm.js +3 -2
  2. package/dist/cjs/components/affiliations/AffiliationForm.js +77 -44
  3. package/dist/cjs/components/affiliations/AffiliationsModal.js +69 -58
  4. package/dist/cjs/components/affiliations/AffiliationsPanel.js +1 -1
  5. package/dist/cjs/components/affiliations/CreateAffiliationModal.js +96 -0
  6. package/dist/cjs/components/authors/AuthorDetailsForm.js +110 -54
  7. package/dist/cjs/components/authors/AuthorsModal.js +71 -48
  8. package/dist/cjs/components/authors/AuthorsPanel.js +2 -2
  9. package/dist/cjs/components/authors/CreateAuthorModal.js +121 -0
  10. package/dist/cjs/components/authors/CreditDrawer.js +14 -12
  11. package/dist/cjs/components/authors-affiliations/AuthorsAndAffiliationsModals.js +8 -24
  12. package/dist/cjs/components/authors-affiliations/GenericPanel.js +2 -2
  13. package/dist/cjs/components/authors-affiliations/ModalTabs.js +80 -0
  14. package/dist/cjs/components/dialog/ConfirmationDialog.js +3 -2
  15. package/dist/cjs/components/form/CreateModalStyles.js +24 -0
  16. package/dist/cjs/components/form/FormFooter.js +8 -3
  17. package/dist/cjs/components/form/ModalFormActions.js +19 -42
  18. package/dist/cjs/components/form/UnsavedLabel.js +25 -0
  19. package/dist/cjs/components/hooks/useAffiliationShowsErrorIndicator.js +27 -0
  20. package/dist/cjs/components/hooks/useAuthorShowsErrorIndicator.js +40 -0
  21. package/dist/cjs/lib/authors-and-affiliations.js +51 -0
  22. package/dist/cjs/lib/normalize.js +17 -5
  23. package/dist/cjs/versions.js +1 -1
  24. package/dist/es/components/ChangeHandlingForm.js +3 -2
  25. package/dist/es/components/affiliations/AffiliationForm.js +75 -46
  26. package/dist/es/components/affiliations/AffiliationsModal.js +71 -60
  27. package/dist/es/components/affiliations/AffiliationsPanel.js +2 -2
  28. package/dist/es/components/affiliations/CreateAffiliationModal.js +56 -0
  29. package/dist/es/components/authors/AuthorDetailsForm.js +111 -56
  30. package/dist/es/components/authors/AuthorsModal.js +74 -51
  31. package/dist/es/components/authors/AuthorsPanel.js +3 -3
  32. package/dist/es/components/authors/CreateAuthorModal.js +81 -0
  33. package/dist/es/components/authors/CreditDrawer.js +13 -11
  34. package/dist/es/components/authors-affiliations/AuthorsAndAffiliationsModals.js +8 -24
  35. package/dist/es/components/authors-affiliations/GenericPanel.js +2 -2
  36. package/dist/es/components/authors-affiliations/ModalTabs.js +73 -0
  37. package/dist/es/components/dialog/ConfirmationDialog.js +3 -2
  38. package/dist/es/components/form/CreateModalStyles.js +18 -0
  39. package/dist/es/components/form/FormFooter.js +9 -4
  40. package/dist/es/components/form/ModalFormActions.js +18 -42
  41. package/dist/es/components/form/UnsavedLabel.js +18 -0
  42. package/dist/es/components/hooks/useAffiliationShowsErrorIndicator.js +21 -0
  43. package/dist/es/components/hooks/useAuthorShowsErrorIndicator.js +34 -0
  44. package/dist/es/lib/authors-and-affiliations.js +48 -0
  45. package/dist/es/lib/normalize.js +15 -4
  46. package/dist/es/versions.js +1 -1
  47. package/dist/es/views/affiliations.js +1 -1
  48. package/dist/types/components/affiliations/AffiliationForm.d.ts +6 -2
  49. package/dist/types/components/affiliations/AffiliationsModal.d.ts +1 -1
  50. package/dist/types/components/affiliations/CreateAffiliationModal.d.ts +8 -0
  51. package/dist/types/components/authors/AuthorDetailsForm.d.ts +11 -1
  52. package/dist/types/components/authors/AuthorsPanel.d.ts +1 -1
  53. package/dist/types/components/authors/CreateAuthorModal.d.ts +9 -0
  54. package/dist/types/components/authors/CreditDrawer.d.ts +2 -4
  55. package/dist/types/components/authors-affiliations/ModalTabs.d.ts +22 -0
  56. package/dist/types/components/form/CreateModalStyles.d.ts +4 -0
  57. package/dist/types/components/form/FormFooter.d.ts +2 -1
  58. package/dist/types/components/form/ModalFormActions.d.ts +12 -6
  59. package/dist/types/components/form/UnsavedLabel.d.ts +10 -0
  60. package/dist/types/components/hooks/useAffiliationShowsErrorIndicator.d.ts +5 -0
  61. package/dist/types/components/hooks/useAuthorShowsErrorIndicator.d.ts +5 -0
  62. package/dist/types/lib/authors-and-affiliations.d.ts +19 -0
  63. package/dist/types/lib/normalize.d.ts +2 -1
  64. package/dist/types/versions.d.ts +1 -1
  65. package/package.json +2 -2
@@ -13,35 +13,43 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { CheckboxField, CheckboxLabel, TextField, InputErrorText, Label, FormRow, LabelText, MultiValueInput, } from '@manuscripts/style-guide';
17
- import { Field, Formik, getIn, } from 'formik';
18
- import React, { useEffect, useRef } from 'react';
16
+ import { CheckboxField, CheckboxLabel, TextField, InputErrorText, FormRow, FormGroup, LabelText, RequiredIndicator, MultiValueInput, } from '@manuscripts/style-guide';
17
+ import { Field, FormikProvider, getIn, useFormik, } from 'formik';
18
+ import React, { useEffect } from 'react';
19
19
  import styled from 'styled-components';
20
20
  import { normalizeAuthor } from '../../lib/normalize';
21
21
  import { ChangeHandlingForm } from '../ChangeHandlingForm';
22
+ import { UnsavedLabel, UnsavedLabelRow, FieldUnsavedDot, } from '../form/UnsavedLabel';
23
+ import { isNamePairError, useAuthorShowsErrorIndicator, } from '../hooks/useAuthorShowsErrorIndicator';
22
24
  const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
23
25
  const ORCID_URL_REGEX = /^https:\/\/orcid\.org\/\d{4}-\d{4}-\d{4}-\d{3}[0-9Xx]\/?$/;
24
26
  const ORCID_INPUT_PATTERN = ORCID_URL_REGEX.source.slice(1, -1);
25
- export const AuthorDetailsForm = ({ values, onChange, onSave, actionsRef, isEmailRequired, selectedAffiliations, selectedCreditRoles, authorFormRef, }) => {
26
- const formRef = useRef(null);
27
- useEffect(() => {
28
- if (selectedAffiliations && formRef.current) {
29
- formRef.current.setFieldValue('affiliationIDs', selectedAffiliations);
30
- }
31
- }, [selectedAffiliations]);
32
- useEffect(() => {
33
- if (selectedCreditRoles && formRef.current) {
34
- formRef.current.setFieldValue('creditRoles', selectedCreditRoles);
35
- }
36
- }, [selectedCreditRoles]);
37
- if (actionsRef) {
38
- actionsRef.current = {
39
- reset: () => formRef.current?.resetForm(),
40
- submitForm: () => formRef.current?.submitForm(),
41
- };
27
+ const NAME_PAIR_REQUIRED_MESSAGE = 'Please enter Given Name or Family Name';
28
+ function isAuthorFieldChanged(formik, key) {
29
+ const v = normalizeAuthor(formik.values);
30
+ const i = normalizeAuthor(formik.initialValues);
31
+ const va = getIn(v, key);
32
+ const vb = getIn(i, key);
33
+ if (key === 'affiliationIDs' || key === 'creditRoles' || key === 'degrees') {
34
+ return (va.length !== vb.length ||
35
+ va.some((item, i) => item !== vb[i]));
42
36
  }
37
+ return va !== vb;
38
+ }
39
+ export { authorShowsErrorIndicator as authorDetailsTabShowsErrorIndicator } from '../hooks/useAuthorShowsErrorIndicator';
40
+ const AuthorErrorEffect = ({ newEntity, requiredContinueActive, onChange }) => {
41
+ useAuthorShowsErrorIndicator(newEntity, requiredContinueActive, onChange);
42
+ return null;
43
+ };
44
+ export const AuthorDetailsForm = ({ values, onChange, onSave, actionsRef, isEmailRequired, selectedAffiliations, selectedCreditRoles, authorFormRef, newEntity, onAuthorDetailsTabErrorChange, unsavedContinueActive = false, requiredContinueActive = false, }) => {
43
45
  const validateAuthor = (values) => {
44
46
  const errors = {};
47
+ const given = values.given?.trim() ?? '';
48
+ const family = values.family?.trim() ?? '';
49
+ if (!given && !family) {
50
+ errors.given = NAME_PAIR_REQUIRED_MESSAGE;
51
+ errors.family = NAME_PAIR_REQUIRED_MESSAGE;
52
+ }
45
53
  const email = values.email?.trim();
46
54
  if (isEmailRequired && !email) {
47
55
  errors.email = 'Email address is required';
@@ -56,57 +64,93 @@ export const AuthorDetailsForm = ({ values, onChange, onSave, actionsRef, isEmai
56
64
  }
57
65
  return errors;
58
66
  };
59
- return (React.createElement(Formik, { initialValues: values, onSubmit: (submitted) => onSave(normalizeAuthor(submitted)), enableReinitialize: true, validateOnChange: true, innerRef: formRef, validate: validateAuthor }, (formik) => {
60
- return (React.createElement(ChangeHandlingForm, { onChange: (next) => onChange(normalizeAuthor(next)), id: "author-details-form", formRef: authorFormRef, noValidate: true },
61
- React.createElement(FormRow, null,
62
- React.createElement(Field, { name: 'prefix' }, (props) => (React.createElement(React.Fragment, null,
63
- React.createElement(Label, { htmlFor: "prefix" }, "Prefix"),
64
- React.createElement(TextField, { id: 'prefix', ...props.field }))))),
65
- React.createElement(FormRow, null,
66
- React.createElement(Field, { name: 'given' }, (props) => (React.createElement(React.Fragment, null,
67
- React.createElement(Label, { htmlFor: "given-name" }, "Given name"),
68
- React.createElement(TextField, { id: 'given-name', ...props.field }))))),
69
- React.createElement(FormRow, null,
70
- React.createElement(Field, { name: 'family' }, (props) => (React.createElement(React.Fragment, null,
71
- React.createElement(Label, { htmlFor: "family-name" }, "Family name"),
72
- React.createElement(TextField, { id: 'family-name', ...props.field }))))),
73
- React.createElement(FormRow, null,
74
- React.createElement(Field, { name: 'suffix' }, (props) => (React.createElement(React.Fragment, null,
75
- React.createElement(Label, { htmlFor: "suffix" }, "Suffix"),
76
- React.createElement(TextField, { id: 'suffix', ...props.field }))))),
67
+ const formik = useFormik({
68
+ initialValues: values,
69
+ onSubmit: (submitted) => onSave(normalizeAuthor(submitted)),
70
+ enableReinitialize: true,
71
+ validateOnChange: true,
72
+ validate: validateAuthor,
73
+ });
74
+ useEffect(() => {
75
+ if (selectedAffiliations) {
76
+ formik.setFieldValue('affiliationIDs', selectedAffiliations);
77
+ }
78
+ }, [selectedAffiliations]);
79
+ useEffect(() => {
80
+ if (selectedCreditRoles) {
81
+ formik.setFieldValue('creditRoles', selectedCreditRoles);
82
+ }
83
+ }, [selectedCreditRoles]);
84
+ if (actionsRef) {
85
+ actionsRef.current = {
86
+ reset: () => formik.resetForm(),
87
+ submitForm: () => formik.submitForm(),
88
+ };
89
+ }
90
+ const showNamePairError = isNamePairError(formik, newEntity, requiredContinueActive);
91
+ const showUnsavedDot = (key) => unsavedContinueActive && isAuthorFieldChanged(formik, key);
92
+ return (React.createElement(FormikProvider, { value: formik },
93
+ React.createElement(AuthorErrorEffect, { newEntity: newEntity, requiredContinueActive: requiredContinueActive, onChange: onAuthorDetailsTabErrorChange }),
94
+ React.createElement(ChangeHandlingForm, { onChange: (next) => onChange(normalizeAuthor(next)), id: "author-details-form", formRef: authorFormRef, noValidate: true },
95
+ React.createElement(StyledFormGroup, null,
96
+ React.createElement(FormRow, null,
97
+ React.createElement(Field, { name: 'prefix' }, (props) => (React.createElement(React.Fragment, null,
98
+ React.createElement(UnsavedLabel, { htmlFor: "prefix", showDot: showUnsavedDot('prefix') }, "Prefix"),
99
+ React.createElement(TextField, { id: 'prefix', ...props.field, placeholder: "E.g. Doctor" }))))),
100
+ React.createElement(StyledFormRow, null,
101
+ React.createElement(Field, { name: 'given' }, (props) => (React.createElement(React.Fragment, null,
102
+ React.createElement(UnsavedLabel, { htmlFor: "given-name", showDot: showUnsavedDot('given') },
103
+ "Given name",
104
+ React.createElement(RequiredIndicator, null, "*")),
105
+ React.createElement(TextFieldWithError, { id: 'given-name', ...props.field, error: showNamePairError }),
106
+ showNamePairError && (React.createElement(InputErrorText, null, NAME_PAIR_REQUIRED_MESSAGE))))))),
107
+ React.createElement(StyledFormGroup, null,
108
+ React.createElement(StyledFormRow, null,
109
+ React.createElement(Field, { name: 'family' }, (props) => (React.createElement(React.Fragment, null,
110
+ React.createElement(UnsavedLabel, { htmlFor: "family-name", showDot: showUnsavedDot('family') },
111
+ "Family name",
112
+ React.createElement(RequiredIndicator, null, "*")),
113
+ React.createElement(TextFieldWithError, { id: 'family-name', ...props.field, error: showNamePairError }),
114
+ showNamePairError && (React.createElement(InputErrorText, null, NAME_PAIR_REQUIRED_MESSAGE)))))),
115
+ React.createElement(FormRow, null,
116
+ React.createElement(Field, { name: 'suffix' }, (props) => (React.createElement(React.Fragment, null,
117
+ React.createElement(UnsavedLabel, { htmlFor: "suffix", showDot: showUnsavedDot('suffix') }, "Suffix"),
118
+ React.createElement(TextField, { id: 'suffix', ...props.field, placeholder: "E.g. Junior" })))))),
77
119
  React.createElement(FormRow, null,
78
- React.createElement(Field, { name: 'role' }, (props) => (React.createElement(React.Fragment, null,
79
- React.createElement(Label, { htmlFor: "role" }, "Job Title"),
80
- React.createElement(TextField, { id: 'role', ...props.field }))))),
120
+ React.createElement(CheckboxContainer, { "data-cy": "corresponding-author-container" },
121
+ React.createElement(CheckboxLabel, null,
122
+ React.createElement(Field, { name: 'isCorresponding' }, (props) => (React.createElement(CheckboxField, { id: 'isCorresponding', checked: props.field.value, ...props.field }))),
123
+ React.createElement(UnsavedLabelRow, null,
124
+ showUnsavedDot('isCorresponding') ? (React.createElement(FieldUnsavedDot, { "aria-hidden": true })) : null,
125
+ React.createElement(LabelText, null, "Corresponding Author"))))),
81
126
  React.createElement(FormRow, null,
82
127
  React.createElement(Field, { name: 'email', type: 'email' }, (props) => {
83
- const hasError = getIn(formik.touched, 'email') &&
128
+ const hasError = (getIn(formik.touched, 'email') || requiredContinueActive) &&
84
129
  getIn(formik.errors, 'email');
85
130
  return (React.createElement(React.Fragment, null,
86
- React.createElement(Label, { htmlFor: "email" }, isEmailRequired ? 'Email address*' : 'Email address'),
131
+ React.createElement(UnsavedLabel, { htmlFor: "email", showDot: showUnsavedDot('email') }, isEmailRequired ? (React.createElement(LabelText, null,
132
+ "Email address",
133
+ React.createElement(RequiredIndicator, null, "*"))) : ('Email address')),
87
134
  React.createElement(TextFieldWithError, { id: 'email', type: "email", required: isEmailRequired, ...props.field, error: hasError }),
88
135
  hasError && (React.createElement(InputErrorText, null, getIn(formik.errors, 'email')))));
89
136
  })),
90
137
  React.createElement(FormRow, null,
91
- React.createElement(CheckboxContainer, { "data-cy": "corresponding-author-container" },
92
- React.createElement(CheckboxLabel, null,
93
- React.createElement(Field, { name: 'isCorresponding' }, (props) => (React.createElement(CheckboxField, { id: 'isCorresponding', checked: props.field.value, ...props.field }))),
94
- React.createElement(LabelText, null, "Corresponding Author")))),
138
+ React.createElement(Field, { name: 'role' }, (props) => (React.createElement(React.Fragment, null,
139
+ React.createElement(UnsavedLabel, { htmlFor: "role", showDot: showUnsavedDot('role') }, "Job Title"),
140
+ React.createElement(TextField, { id: 'role', ...props.field }))))),
95
141
  React.createElement(FormRow, null,
96
142
  React.createElement(Field, { name: 'ORCID', type: 'text' }, (props) => {
97
- const hasError = getIn(formik.touched, 'ORCID') &&
98
- getIn(formik.errors, 'ORCID');
143
+ const hasError = getIn(formik.touched, 'ORCID') && getIn(formik.errors, 'ORCID');
99
144
  return (React.createElement(React.Fragment, null,
100
- React.createElement(Label, { htmlFor: "orcid" }, "ORCID"),
101
- React.createElement(TextFieldWithError, { id: 'orcid', disabled: values.isAuthenticated, 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 }),
145
+ React.createElement(UnsavedLabel, { htmlFor: "orcid", showDot: showUnsavedDot('ORCID') }, "ORCID"),
146
+ React.createElement(TextFieldWithError, { id: 'orcid', type: "url", placeholder: 'https://orcid.org/...', disabled: values.isAuthenticated, ...props.field, pattern: ORCID_INPUT_PATTERN, title: "Please enter a valid ORCID URL: https://orcid.org/xxxx-xxxx-xxxx-xxxx", error: hasError }),
102
147
  hasError && (React.createElement(InputErrorText, null, getIn(formik.errors, 'ORCID')))));
103
148
  })),
104
149
  React.createElement(FormRow, null,
105
- React.createElement(Label, { htmlFor: 'degrees' }, "Degrees"),
106
- React.createElement(MultiValueInput, { id: "degrees", inputType: "text", placeholder: "Enter degree and press enter", initialValues: values.degrees, onChange: (newValues) => {
107
- formik.setFieldValue('degrees', newValues);
108
- } }))));
109
- }));
150
+ React.createElement(UnsavedLabel, { htmlFor: "degrees", showDot: showUnsavedDot('degrees') }, "Qualification"),
151
+ React.createElement(MultiValueInput, { id: 'degrees', inputType: "text", placeholder: "E.g. Bsc Computer Science", initialValues: Array.isArray(formik.values.degrees) ? formik.values.degrees : [], onChange: (values) => {
152
+ formik.setFieldValue('degrees', values);
153
+ } })))));
110
154
  };
111
155
  export const Fieldset = styled.fieldset `
112
156
  padding: 0;
@@ -123,3 +167,14 @@ export const CheckboxContainer = styled.div `
123
167
  align-items: center;
124
168
  gap: 32px;
125
169
  `;
170
+ export const StyledFormGroup = styled(FormGroup) `
171
+ [name='prefix'] {
172
+ width: 180px;
173
+ }
174
+ [name='suffix'] {
175
+ width: 180px;
176
+ }
177
+ `;
178
+ export const StyledFormRow = styled(FormRow) `
179
+ flex: 1;
180
+ `;
@@ -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, AddRoleIcon, AuthorPlaceholderIcon, CloseButton, ModalBody, ModalContainer, ModalHeader, ModalSidebar, ModalSidebarHeader, ModalSidebarTitle, outlineStyle, ScrollableModalContent, SidebarContent, StyledModal, InspectorTabs, InspectorTabPanel, InspectorTabList, InspectorTab, InspectorTabPanels, } from '@manuscripts/style-guide';
16
+ import { AddIcon, AuthorPlaceholderIcon, CloseButton, FormSubtitle, ModalBody, ModalContainer, ModalHeader, ModalSidebar, ModalSidebarHeader, ModalSidebarTitle, outlineStyle, ScrollableModalContent, SidebarContent, StyledModal, InspectorTabs, InspectorTabPanel, 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';
@@ -24,21 +24,26 @@ import { normalizeAuthor } from '../../lib/normalize';
24
24
  import { ConfirmationDialog, DialogType } from '../dialog/ConfirmationDialog';
25
25
  import FormFooter from '../form/FormFooter';
26
26
  import { FormPlaceholder } from '../form/FormPlaceholder';
27
- import { ModalFormActions } from '../form/ModalFormActions';
28
- import { DrawerGroup } from '../modal-drawer/GenericDrawerGroup';
27
+ import { ModalFormActions, ModalFormSaveButton } from '../form/ModalFormActions';
28
+ import { ModalTabs } from '../authors-affiliations/ModalTabs';
29
29
  import { AffiliationsPanel } from '../affiliations/AffiliationsPanel';
30
30
  import { AuthorDetailsForm } from './AuthorDetailsForm';
31
31
  import { AuthorList } from './AuthorList';
32
- import { CreditDrawer } from './CreditDrawer';
32
+ import { CreditContributionsCheckboxes } from './CreditDrawer';
33
33
  import { useManageAffiliations } from './useManageAffiliations';
34
34
  import { useManageCredit } from './useManageCredit';
35
+ const MODAL_ON_CLOSE_NOTIFY_DELAY_MS = 220;
35
36
  export const authorsReducer = arrayReducer((a, b) => a.id === b.id);
36
37
  export const AuthorsModal = ({ authors: $authors, affiliations: $affiliations, author, onSaveAuthor, onDeleteAuthor, addNewAuthor = false, onOpenAffiliationsModal, onClose, }) => {
37
38
  const [isOpen, setOpen] = useState(true);
38
39
  const prevIsOpenRef = useRef(true);
39
40
  useEffect(() => {
40
41
  if (prevIsOpenRef.current && !isOpen) {
41
- onClose?.();
42
+ prevIsOpenRef.current = isOpen;
43
+ const id = window.setTimeout(() => {
44
+ onClose?.();
45
+ }, MODAL_ON_CLOSE_NOTIFY_DELAY_MS);
46
+ return () => window.clearTimeout(id);
42
47
  }
43
48
  prevIsOpenRef.current = isOpen;
44
49
  }, [isOpen, onClose]);
@@ -53,7 +58,10 @@ export const AuthorsModal = ({ authors: $authors, affiliations: $affiliations, a
53
58
  const [nextAuthor, setNextAuthor] = useState(null);
54
59
  const [isSwitchingAuthor, setIsSwitchingAuthor] = useState(false);
55
60
  const [isCreatingNewAuthor, setIsCreatingNewAuthor] = useState(false);
56
- const [showCreditDrawer, setShowCreditDrawer] = useState(false);
61
+ const [authorHasError, setAuthorDetailsTabHasError] = useState(false);
62
+ const [authorDetailsUnsavedContinue, setAuthorDetailsUnsavedContinue] = useState(false);
63
+ const [authorDetailsRequiredContinue, setAuthorDetailsRequiredContinue] = useState(false);
64
+ const [authorTabIndex, setAuthorTabIndex] = useState(0);
57
65
  const valuesRef = useRef(undefined);
58
66
  const actionsRef = useRef(undefined);
59
67
  const authorFormRef = useRef(null);
@@ -70,6 +78,19 @@ export const AuthorsModal = ({ authors: $authors, affiliations: $affiliations, a
70
78
  const relevantAffiliations = affiliations.filter((item) => currentAuthor?.affiliationIDs?.includes(item.id));
71
79
  setSelectedAffiliations(relevantAffiliations);
72
80
  }, []);
81
+ useEffect(() => {
82
+ if (!unSavedChanges) {
83
+ setAuthorDetailsUnsavedContinue(false);
84
+ setAuthorDetailsRequiredContinue(false);
85
+ }
86
+ }, [unSavedChanges]);
87
+ useEffect(() => {
88
+ setAuthorDetailsUnsavedContinue(false);
89
+ setAuthorDetailsRequiredContinue(false);
90
+ if (selection?.id) {
91
+ setAuthorTabIndex(0);
92
+ }
93
+ }, [selection?.id]);
73
94
  const selectAuthor = (author) => {
74
95
  if (author.id === selection?.id) {
75
96
  return;
@@ -120,31 +141,6 @@ export const AuthorsModal = ({ authors: $authors, affiliations: $affiliations, a
120
141
  setSelection(undefined);
121
142
  }
122
143
  };
123
- const save = () => {
124
- if (!authorFormRef.current?.checkValidity()) {
125
- setShowConfirmationDialog(false);
126
- setTimeout(() => {
127
- authorFormRef.current?.reportValidity();
128
- }, 830);
129
- }
130
- else {
131
- if (valuesRef.current && selection) {
132
- saveAuthor(valuesRef.current);
133
- }
134
- if (nextAuthor) {
135
- setSelection(nextAuthor);
136
- setNextAuthor(null);
137
- setNewAuthor(false);
138
- updateAffiliationSelection(nextAuthor);
139
- setIsCreatingNewAuthor(false);
140
- }
141
- else if (isCreatingNewAuthor) {
142
- createNewAuthor();
143
- setIsCreatingNewAuthor(false);
144
- }
145
- setShowConfirmationDialog(false);
146
- }
147
- };
148
144
  const cancel = () => {
149
145
  resetAuthor();
150
146
  if (nextAuthor) {
@@ -293,7 +289,11 @@ export const AuthorsModal = ({ authors: $authors, affiliations: $affiliations, a
293
289
  }
294
290
  setEmailRequired(isCorresponding);
295
291
  };
296
- const { removeCreditRole, selectCreditRole, selectedCreditRoles, setSelectedCreditRoles, vocabTermItems, } = useManageCredit(selection);
292
+ const { selectCreditRole, selectedCreditRoles, setSelectedCreditRoles, vocabTermItems, } = useManageCredit(selection);
293
+ const newEntity = newAuthor ||
294
+ (isCreatingNewAuthor &&
295
+ !showConfirmationDialog &&
296
+ !showRequiredFieldConfirmationDialog);
297
297
  return (React.createElement(StyledModal, { isOpen: isOpen, onRequestClose: () => close(), shouldCloseOnOverlayClick: true },
298
298
  React.createElement(ModalContainer, { "data-cy": "authors-modal" },
299
299
  React.createElement(ModalHeader, null,
@@ -301,35 +301,50 @@ export const AuthorsModal = ({ authors: $authors, affiliations: $affiliations, a
301
301
  React.createElement(StyledModalBody, null,
302
302
  React.createElement(ModalSidebar, { "data-cy": "authors-sidebar" },
303
303
  React.createElement(StyledModalSidebarHeader, null,
304
- React.createElement(ModalSidebarTitle, null, "Authors")),
304
+ React.createElement(ModalSidebarTitle, null, "Manage Authors")),
305
305
  React.createElement(StyledSidebarContent, null,
306
306
  React.createElement(AddAuthorButton, { "data-cy": "add-author-button", onClick: addAuthor, "data-active": isCreatingNewAuthor || newAuthor },
307
307
  React.createElement(AddIcon, { width: 18, height: 18 }),
308
308
  React.createElement(ActionTitle, null, "New Author")),
309
309
  React.createElement(AuthorList, { author: selection, authors: authors, onSelect: selectAuthor, onDelete: () => setShowDeleteDialog((prev) => !prev), moveAuthor: moveAuthor, lastSavedAuthor: lastSavedAuthor }))),
310
- React.createElement(ScrollableModalContent, { "data-cy": "author-modal-content" }, selection ? (React.createElement(React.Fragment, null,
311
- React.createElement(AuthorTabs, null,
312
- React.createElement(ModalFormActions, { form: 'author-details-form', onSubmitForm: () => actionsRef.current?.submitForm?.(), type: "author", onDelete: deleteAuthor, showingDeleteDialog: showingDeleteDialog, showDeleteDialog: () => setShowDeleteDialog((prev) => !prev), newEntity: newAuthor ||
313
- (isCreatingNewAuthor &&
314
- !showConfirmationDialog &&
315
- !showRequiredFieldConfirmationDialog), isDisableSave: isDisableSave }),
316
- React.createElement(InspectorTabList, null,
317
- React.createElement(InspectorTab, null, "Details"),
318
- onOpenAffiliationsModal && (React.createElement(InspectorTab, null, "Affiliations")),
319
- React.createElement(InspectorTab, null, "Contributions (CRediT)")),
310
+ React.createElement(StyledScrollableModalContent, { "data-cy": "author-modal-content" }, selection ? (React.createElement(React.Fragment, null,
311
+ React.createElement(AuthorTabs, { selectedIndex: authorTabIndex, onChange: setAuthorTabIndex },
312
+ React.createElement(ModalFormActions, { type: "author", onDelete: deleteAuthor, showingDeleteDialog: showingDeleteDialog, showDeleteDialog: () => setShowDeleteDialog((prev) => !prev) }),
313
+ React.createElement(ModalTabs, { tabLabels: [
314
+ 'Author Details',
315
+ ...(onOpenAffiliationsModal ? ['Affiliations'] : []),
316
+ 'Contributions',
317
+ ], tabErrorIndicators: [
318
+ authorHasError,
319
+ ...(onOpenAffiliationsModal ? [false] : []),
320
+ false,
321
+ ], tabWarningIndicators: [
322
+ authorDetailsUnsavedContinue && !authorHasError,
323
+ ...(onOpenAffiliationsModal ? [false] : []),
324
+ false,
325
+ ] }),
320
326
  React.createElement(InspectorTabPanels, null,
321
327
  React.createElement(AuthorTabPanel, null,
322
- React.createElement(AuthorDetailsForm, { values: normalizeAuthor(selection), onChange: changeAuthor, onSave: saveAuthor, actionsRef: actionsRef, isEmailRequired: isEmailRequired, selectedAffiliations: selectedAffiliations.map((a) => a.id), authorFormRef: authorFormRef, selectedCreditRoles: selectedCreditRoles })),
328
+ React.createElement(AuthorDetailsForm, { values: normalizeAuthor(selection), onChange: changeAuthor, onSave: saveAuthor, actionsRef: actionsRef, isEmailRequired: isEmailRequired, selectedAffiliations: selectedAffiliations.map((a) => a.id), authorFormRef: authorFormRef, selectedCreditRoles: selectedCreditRoles, newEntity: newEntity, onAuthorDetailsTabErrorChange: setAuthorDetailsTabHasError, unsavedContinueActive: authorDetailsUnsavedContinue, requiredContinueActive: authorDetailsRequiredContinue })),
323
329
  onOpenAffiliationsModal && (React.createElement(AuthorTabPanel, null,
324
330
  React.createElement(AffiliationsPanel, { items: affiliations, selectedItems: selectedAffiliations, onSelect: selectAffiliation, onOpenAffiliationsModal: onOpenAffiliationsModal }))),
325
331
  React.createElement(AuthorTabPanel, null,
326
- React.createElement(DrawerGroup, { Drawer: CreditDrawer, removeItem: removeCreditRole, selectedItems: selectedCreditRoles.map((r) => ({
332
+ React.createElement(FormSubtitle, null, "Contributions (CRediT)"),
333
+ React.createElement(ContributionsDescriptionSubtitle, null, "Select the roles this author contributed to according to the CRediT taxonomy"),
334
+ React.createElement(CreditContributionsCheckboxes, { items: vocabTermItems, selectedItems: selectedCreditRoles.map((r) => ({
327
335
  id: r.vocabTerm,
328
- ...r,
329
- })), onSelect: selectCreditRole, items: vocabTermItems, showDrawer: showCreditDrawer, setShowDrawer: setShowCreditDrawer, title: "Contributions (CRediT)", buttonText: "Assign Roles", cy: "credit-taxnonomy", labelField: "vocabTerm", Icon: React.createElement(AddRoleIcon, { width: 16, height: 16 }) })))),
330
- React.createElement(ConfirmationDialog, { isOpen: showRequiredFieldConfirmationDialog, onPrimary: () => setShowRequiredFieldConfirmationDialog(false), onSecondary: cancel, type: DialogType.REQUIRED, entityType: "author" }),
331
- React.createElement(ConfirmationDialog, { isOpen: showConfirmationDialog, onPrimary: save, onSecondary: cancel, type: DialogType.SAVE, entityType: "author" }))) : (React.createElement(FormPlaceholder, { type: "author", title: "Author Details", message: "Select an author from the list to display their details here.", placeholderIcon: React.createElement(AuthorPlaceholderIcon, null) })))),
332
- React.createElement(FormFooter, { onCancel: close }))));
336
+ })), onSelect: selectCreditRole })))),
337
+ React.createElement(ConfirmationDialog, { isOpen: showRequiredFieldConfirmationDialog, onPrimary: () => {
338
+ setShowRequiredFieldConfirmationDialog(false);
339
+ setNextAuthor(null);
340
+ setAuthorDetailsRequiredContinue(true);
341
+ }, onSecondary: cancel, type: DialogType.REQUIRED, entityType: "author" }),
342
+ React.createElement(ConfirmationDialog, { isOpen: showConfirmationDialog, onPrimary: () => {
343
+ setShowConfirmationDialog(false);
344
+ setNextAuthor(null);
345
+ setAuthorDetailsUnsavedContinue(true);
346
+ }, onSecondary: cancel, type: DialogType.SAVE, entityType: "author" }))) : (React.createElement(FormPlaceholder, { type: "author", title: "Author Details", message: "Select an author from the list to display their details here.", placeholderIcon: React.createElement(AuthorPlaceholderIcon, null) })))),
347
+ React.createElement(FormFooter, { onCancel: close, primaryAction: selection ? (React.createElement(ModalFormSaveButton, { form: "author-details-form", newEntity: newEntity, isDisableSave: isDisableSave, onSubmitForm: () => actionsRef.current?.submitForm?.() })) : undefined }))));
333
348
  };
334
349
  function createEmptyAuthor(priority) {
335
350
  return {
@@ -348,6 +363,7 @@ function createEmptyAuthor(priority) {
348
363
  correspIDs: [],
349
364
  footnoteIDs: [],
350
365
  prefix: '',
366
+ creditRoles: [],
351
367
  };
352
368
  }
353
369
  const AddAuthorButton = styled.button `
@@ -381,6 +397,10 @@ const AuthorTabPanel = styled(InspectorTabPanel).attrs({
381
397
  }) `
382
398
  margin-top: ${(props) => props.theme.grid.unit * 4}px;
383
399
  `;
400
+ const ContributionsDescriptionSubtitle = styled(FormSubtitle) `
401
+ font-size: ${(props) => props.theme.font.size.small};
402
+ line-height: ${(props) => props.theme.font.lineHeight.normal};
403
+ `;
384
404
  const StyledSidebarContent = styled(SidebarContent) `
385
405
  padding: 8px;
386
406
  `;
@@ -389,5 +409,8 @@ const StyledModalBody = styled(ModalBody) `
389
409
  height: calc(90vh - 40px);
390
410
  `;
391
411
  const StyledModalSidebarHeader = styled(ModalSidebarHeader) `
392
- margin-bottom: 16px;
412
+ margin-bottom: 12px;
413
+ `;
414
+ const StyledScrollableModalContent = styled(ScrollableModalContent) `
415
+ padding: 45px 16px 16px;
393
416
  `;
@@ -1,9 +1,9 @@
1
- import { AuthorPlaceholderIcon } from '@manuscripts/style-guide';
1
+ import { InfoCircleIcon } from '@manuscripts/style-guide';
2
2
  import React from 'react';
3
3
  import { GenericPanel, ListItem, ListItems, useListSelectedIds, } from '../authors-affiliations/GenericPanel';
4
- export const AuthorsPanel = ({ items, selectedItems = [], onSelect, onOpenAuthorsModal, }) => {
4
+ export const AuthorsPanel = ({ items, selectedItems = [], onSelect, openAuthorsModal, }) => {
5
5
  const selectedIds = useListSelectedIds(selectedItems);
6
- return (React.createElement(GenericPanel, { title: "Authors", createLabel: "Add New Author", onCreate: onOpenAuthorsModal, createDataCy: "add-authors-link", emptyDataCy: "authors-panel-empty", isEmpty: items.length === 0, emptyIcon: React.createElement(AuthorPlaceholderIcon, null), emptyMessage: React.createElement(React.Fragment, null,
6
+ return (React.createElement(GenericPanel, { title: "Authors", createLabel: "Add New Author", onCreate: openAuthorsModal, createDataCy: "add-authors-link", emptyDataCy: "authors-panel-empty", isEmpty: items.length === 0, emptyIcon: React.createElement(InfoCircleIcon, null), emptyMessage: React.createElement(React.Fragment, null,
7
7
  "There are no authors attributed yet!",
8
8
  React.createElement("br", null),
9
9
  "Click \u2018Add New Author\u2019") },
@@ -0,0 +1,81 @@
1
+ import { CloseButton, ModalContainer, ModalHeader, StyledModal, } from '@manuscripts/style-guide';
2
+ import { generateNodeID, schema } from '@manuscripts/transform';
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { normalizeAuthor } from '../../lib/normalize';
5
+ import { MODAL_ON_CLOSE_NOTIFY_DELAY_MS, FormTitle, StyledModalBody, StyledScrollableModalContent, } from '../form/CreateModalStyles';
6
+ import FormFooter from '../form/FormFooter';
7
+ import { ModalFormSaveButton } from '../form/ModalFormActions';
8
+ import { AuthorDetailsForm } from './AuthorDetailsForm';
9
+ import { useManageAffiliations } from './useManageAffiliations';
10
+ import { useManageCredit } from './useManageCredit';
11
+ export const CreateAuthorModal = ({ authorsCount, affiliations: $affiliations, onSave, onClose, }) => {
12
+ const [isOpen, setIsOpen] = useState(true);
13
+ const [isDisableSave, setDisableSave] = useState(true);
14
+ const [isEmailRequired, setEmailRequired] = useState(false);
15
+ const [hasError, setHasError] = useState(false);
16
+ const actionsRef = useRef(undefined);
17
+ const authorFormRef = useRef(null);
18
+ const selection = useMemo(() => createEmptyAuthor(authorsCount), []);
19
+ const prevIsOpenRef = useRef(true);
20
+ useEffect(() => {
21
+ if (prevIsOpenRef.current && !isOpen) {
22
+ prevIsOpenRef.current = isOpen;
23
+ const id = window.setTimeout(() => {
24
+ onClose();
25
+ }, MODAL_ON_CLOSE_NOTIFY_DELAY_MS);
26
+ return () => window.clearTimeout(id);
27
+ }
28
+ prevIsOpenRef.current = isOpen;
29
+ }, [isOpen, onClose]);
30
+ const { selectedAffiliations } = useManageAffiliations(selection, $affiliations);
31
+ const { selectedCreditRoles } = useManageCredit(selection);
32
+ const handleSave = (values) => {
33
+ onSave({ ...selection, ...values });
34
+ setIsOpen(false);
35
+ };
36
+ const handleChange = (values) => {
37
+ const { given, family, email, isCorresponding } = values;
38
+ const isNameFilled = given?.length || family?.length;
39
+ if (isNameFilled) {
40
+ if (isCorresponding) {
41
+ setDisableSave(!email?.length);
42
+ }
43
+ else {
44
+ setDisableSave(false);
45
+ }
46
+ }
47
+ else {
48
+ setDisableSave(true);
49
+ }
50
+ setEmailRequired(!!isCorresponding);
51
+ };
52
+ return (React.createElement(StyledModal, { isOpen: isOpen, onRequestClose: () => setIsOpen(false), shouldCloseOnOverlayClick: true },
53
+ React.createElement(ModalContainer, { "data-cy": "create-author-modal" },
54
+ React.createElement(ModalHeader, null,
55
+ React.createElement(CloseButton, { onClick: () => setIsOpen(false), "data-cy": "modal-close-button" })),
56
+ React.createElement(StyledModalBody, null,
57
+ React.createElement(StyledScrollableModalContent, null,
58
+ React.createElement(FormTitle, null, "Create New Author"),
59
+ React.createElement(AuthorDetailsForm, { values: normalizeAuthor(selection), onChange: handleChange, onSave: handleSave, actionsRef: actionsRef, isEmailRequired: isEmailRequired, selectedAffiliations: selectedAffiliations.map((a) => a.id), authorFormRef: authorFormRef, selectedCreditRoles: selectedCreditRoles, newEntity: true, onAuthorDetailsTabErrorChange: setHasError }))),
60
+ React.createElement(FormFooter, { onCancel: () => setIsOpen(false), primaryAction: React.createElement(ModalFormSaveButton, { form: "author-details-form", newEntity: true, isDisableSave: isDisableSave || hasError, createLabel: "Create New Author", onSubmitForm: () => actionsRef.current?.submitForm?.() }) }))));
61
+ };
62
+ function createEmptyAuthor(priority) {
63
+ return {
64
+ id: generateNodeID(schema.nodes.contributor),
65
+ role: '',
66
+ affiliationIDs: [],
67
+ degrees: [],
68
+ given: '',
69
+ family: '',
70
+ email: '',
71
+ suffix: '',
72
+ isCorresponding: false,
73
+ ORCID: '',
74
+ priority,
75
+ isJointContributor: false,
76
+ correspIDs: [],
77
+ footnoteIDs: [],
78
+ prefix: '',
79
+ creditRoles: [],
80
+ };
81
+ }
@@ -1,20 +1,22 @@
1
- import { CheckboxField, CheckboxLabel, Drawer, DrawerItemsList, LabelText, } from '@manuscripts/style-guide';
1
+ import { CheckboxField, CheckboxLabel, LabelText, } from '@manuscripts/style-guide';
2
2
  import React from 'react';
3
3
  import styled from 'styled-components';
4
4
  import { CheckboxContainer } from './AuthorDetailsForm';
5
- export const CreditDrawer = ({ items, selectedItems = [], onSelect, ...drawerProps }) => {
6
- return (React.createElement(Drawer, { ...drawerProps },
7
- React.createElement(TwoColumnContainer, null, items.map((item, i) => (React.createElement(TwoColumnCheckbox, { key: item.id },
8
- React.createElement(CheckboxLabel, null,
9
- React.createElement(CheckboxField, { id: 'credit-role-' + i, name: item.id, checked: selectedItems?.map((a) => a.id).includes(item.id), onChange: () => {
10
- onSelect(item.id);
11
- } }),
12
- React.createElement(LabelText, null, item.vocabTerm))))))));
5
+ export const CreditContributionsCheckboxes = ({ items, selectedItems = [], onSelect }) => {
6
+ const selectedIds = selectedItems.map((a) => a.id);
7
+ return (React.createElement(TwoColumnContainer, { "data-cy": "credit-taxnonomy", role: "group", "aria-label": "Contributions (CRediT)" }, items.map((item) => (React.createElement(TwoColumnCheckbox, { key: item.id },
8
+ React.createElement(CheckboxLabel, null,
9
+ React.createElement(CheckboxField, { name: item.id, checked: selectedIds.includes(item.id), onChange: () => {
10
+ onSelect(item.id);
11
+ } }),
12
+ React.createElement(LabelText, null, item.vocabTerm)))))));
13
13
  };
14
- const TwoColumnContainer = styled(DrawerItemsList) `
14
+ const TwoColumnContainer = styled.ul `
15
+ list-style: none;
16
+ padding: 0 ${(props) => props.theme.grid.unit * 4}px;
17
+ margin: 0;
15
18
  display: flex;
16
19
  flex-flow: row wrap;
17
- padding: 0 ${(props) => props.theme.grid.unit * 4}px;
18
20
  position: relative;
19
21
  `;
20
22
  const TwoColumnCheckbox = styled(CheckboxContainer) `
@@ -13,32 +13,16 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { schema, } from '@manuscripts/transform';
16
+ import { schema } from '@manuscripts/transform';
17
17
  import React, { useState } from 'react';
18
+ import { upsertAuthor, upsertAffiliation, } from '../../lib/authors-and-affiliations';
18
19
  import { getEditorProps } from '../../plugins/editor-props';
19
20
  import ReactSubView from '../../views/ReactSubView';
20
- import { deleteNode, findChildByType, findChildrenAttrsByType, updateNodeAttrs, } from '../../lib/view';
21
+ import { deleteNode, findChildrenAttrsByType, updateNodeAttrs, } from '../../lib/view';
21
22
  import { AffiliationsModal, } from '../affiliations/AffiliationsModal';
23
+ import { CreateAffiliationModal } from '../affiliations/CreateAffiliationModal';
22
24
  import { AuthorsModal } from '../authors/AuthorsModal';
23
- function insertNode(parentType, childType) {
24
- return (view, attrs) => {
25
- const parent = findChildByType(view, parentType);
26
- if (parent) {
27
- view.dispatch(view.state.tr.insert(parent.pos + 1, childType.create(attrs)));
28
- }
29
- };
30
- }
31
- function upsertNode(nodeType, insertFn) {
32
- return (view, attrs) => {
33
- if (!updateNodeAttrs(view, nodeType, attrs)) {
34
- insertFn(view, attrs);
35
- }
36
- };
37
- }
38
- const insertAuthorNode = insertNode(schema.nodes.contributors, schema.nodes.contributor);
39
- const insertAffiliationNode = insertNode(schema.nodes.affiliations, schema.nodes.affiliation);
40
- const upsertAuthor = upsertNode(schema.nodes.contributor, insertAuthorNode);
41
- const upsertAffiliation = upsertNode(schema.nodes.affiliation, insertAffiliationNode);
25
+ import { CreateAuthorModal } from '../authors/CreateAuthorModal';
42
26
  export const AuthorsAndAffiliationsModals = ({ initialModal, view, author, affiliation, addNewAuthor, addNewAffiliation, }) => {
43
27
  const [showOverlay, setShowOverlay] = useState(false);
44
28
  const [authors, setAuthors] = useState(() => findChildrenAttrsByType(view, schema.nodes.contributor));
@@ -69,11 +53,11 @@ export const AuthorsAndAffiliationsModals = ({ initialModal, view, author, affil
69
53
  if (initialModal === 'authors') {
70
54
  return (React.createElement(React.Fragment, null,
71
55
  React.createElement(AuthorsModal, { ...authorsProps, onOpenAffiliationsModal: handleOpenOverlay }),
72
- showOverlay && (React.createElement(AffiliationsModal, { ...affiliationsProps, addNewAffiliation: true, onClose: handleOverlayClose }))));
56
+ showOverlay && (React.createElement(CreateAffiliationModal, { affiliationsCount: affiliations.length, onSave: (a) => upsertAffiliation(view, a), onClose: handleOverlayClose }))));
73
57
  }
74
58
  return (React.createElement(React.Fragment, null,
75
- React.createElement(AffiliationsModal, { ...affiliationsProps, onOpenAuthorsModal: handleOpenOverlay }),
76
- showOverlay && (React.createElement(AuthorsModal, { ...authorsProps, addNewAuthor: true, onClose: handleOverlayClose }))));
59
+ React.createElement(AffiliationsModal, { ...affiliationsProps, openAuthorsModal: handleOpenOverlay }),
60
+ showOverlay && (React.createElement(CreateAuthorModal, { authorsCount: authors.length, affiliations: affiliations, onSave: (a) => upsertAuthor(view, a), onClose: handleOverlayClose }))));
77
61
  };
78
62
  export const openAuthorsAndAffiliationsModals = (pos, view, initialModal) => {
79
63
  if (!view) {
@@ -57,8 +57,8 @@ const PanelEmpty = styled.div `
57
57
  ${(props) => props.theme.grid.unit * 4}px;
58
58
  `;
59
59
  const PanelEmptyIcon = styled.div `
60
- width: 120px;
61
- height: 120px;
60
+ width: 80px;
61
+ height: 80px;
62
62
  flex-shrink: 0;
63
63
  display: flex;
64
64
  align-items: center;