newspack-components 4.1.0 → 4.3.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 (135) hide show
  1. package/colors/colors.module.scss +1 -1
  2. package/dist/cjs/action-card/index.js +21 -12
  3. package/dist/cjs/action-card/style.scss +32 -14
  4. package/dist/cjs/autocomplete-tokenfield/index.js +2 -2
  5. package/dist/cjs/autocomplete-with-latest-posts/index.js +289 -0
  6. package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
  7. package/dist/cjs/badge/index.js +32 -0
  8. package/dist/cjs/badge/style.scss +36 -0
  9. package/dist/cjs/button/index.js +2 -0
  10. package/dist/cjs/card/core-card.js +141 -0
  11. package/dist/cjs/card/index.js +33 -0
  12. package/dist/cjs/card/style-core.scss +227 -0
  13. package/dist/cjs/card/style.scss +2 -2
  14. package/dist/cjs/card-settings-group/index.js +49 -0
  15. package/dist/cjs/card-settings-group/style.scss +33 -0
  16. package/dist/cjs/card-sortable-list/index.js +390 -0
  17. package/dist/cjs/card-sortable-list/style.scss +48 -0
  18. package/dist/cjs/confirm-dialog/index.js +59 -0
  19. package/dist/cjs/divider/index.js +42 -0
  20. package/dist/cjs/divider/style.scss +51 -0
  21. package/dist/cjs/form-token-field/style.scss +0 -5
  22. package/dist/cjs/grid/index.js +2 -1
  23. package/dist/cjs/grid/style.scss +63 -9
  24. package/dist/cjs/index.js +42 -0
  25. package/dist/cjs/modal/index.js +11 -4
  26. package/dist/cjs/modal/style.scss +43 -13
  27. package/dist/cjs/newspack-icon/style.scss +5 -3
  28. package/dist/cjs/popover/style.scss +1 -1
  29. package/dist/cjs/section-header/index.js +76 -17
  30. package/dist/cjs/section-header/style.scss +109 -4
  31. package/dist/cjs/style.scss +2 -0
  32. package/dist/cjs/waiting/index.js +3 -1
  33. package/dist/cjs/waiting/style.scss +4 -0
  34. package/dist/cjs/web-preview/index.js +98 -39
  35. package/dist/cjs/web-preview/style.scss +20 -53
  36. package/dist/cjs/with-wizard/index.js +1 -1
  37. package/dist/cjs/with-wizard/style.scss +24 -6
  38. package/dist/cjs/with-wizard-screen/style.scss +39 -10
  39. package/dist/cjs/wizard/components/WizardSnackbar.js +70 -0
  40. package/dist/cjs/wizard/components/style.scss +48 -0
  41. package/dist/cjs/wizard/index.js +97 -16
  42. package/dist/cjs/wizard/store/index.js +46 -0
  43. package/dist/esm/action-card/index.js +21 -12
  44. package/dist/esm/action-card/style.scss +32 -14
  45. package/dist/esm/autocomplete-tokenfield/index.js +2 -2
  46. package/dist/esm/autocomplete-with-latest-posts/index.js +281 -0
  47. package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
  48. package/dist/esm/badge/index.js +24 -0
  49. package/dist/esm/badge/style.scss +36 -0
  50. package/dist/esm/button/index.js +2 -0
  51. package/dist/esm/card/core-card.js +133 -0
  52. package/dist/esm/card/index.js +33 -0
  53. package/dist/esm/card/style-core.scss +227 -0
  54. package/dist/esm/card/style.scss +2 -2
  55. package/dist/esm/card-settings-group/index.js +42 -0
  56. package/dist/esm/card-settings-group/style.scss +33 -0
  57. package/dist/esm/card-sortable-list/index.js +381 -0
  58. package/dist/esm/card-sortable-list/style.scss +48 -0
  59. package/dist/esm/confirm-dialog/index.js +49 -0
  60. package/dist/esm/divider/index.js +34 -0
  61. package/dist/esm/divider/style.scss +51 -0
  62. package/dist/esm/form-token-field/style.scss +0 -5
  63. package/dist/esm/grid/index.js +2 -1
  64. package/dist/esm/grid/style.scss +63 -9
  65. package/dist/esm/index.js +6 -0
  66. package/dist/esm/modal/index.js +11 -4
  67. package/dist/esm/modal/style.scss +43 -13
  68. package/dist/esm/newspack-icon/style.scss +5 -3
  69. package/dist/esm/popover/style.scss +1 -1
  70. package/dist/esm/section-header/index.js +75 -18
  71. package/dist/esm/section-header/style.scss +109 -4
  72. package/dist/esm/style.scss +2 -0
  73. package/dist/esm/waiting/index.js +3 -1
  74. package/dist/esm/waiting/style.scss +4 -0
  75. package/dist/esm/web-preview/index.js +101 -42
  76. package/dist/esm/web-preview/style.scss +20 -53
  77. package/dist/esm/with-wizard/index.js +1 -1
  78. package/dist/esm/with-wizard/style.scss +24 -6
  79. package/dist/esm/with-wizard-screen/style.scss +39 -10
  80. package/dist/esm/wizard/components/WizardSnackbar.js +64 -0
  81. package/dist/esm/wizard/components/style.scss +48 -0
  82. package/dist/esm/wizard/index.js +101 -20
  83. package/dist/esm/wizard/store/index.js +46 -0
  84. package/package.json +5 -6
  85. package/shared/author-context.js +26 -0
  86. package/shared/hooks/use-coauthors.js +220 -0
  87. package/shared/hooks/use-coauthors.test.js +624 -0
  88. package/shared/hooks/use-custom-byline.js +47 -0
  89. package/shared/hooks/use-custom-byline.test.js +133 -0
  90. package/shared/scss/_mixins.scss +46 -0
  91. package/src/action-card/action-card.d.ts +7 -3
  92. package/src/action-card/index.js +24 -11
  93. package/src/action-card/style.scss +32 -14
  94. package/src/autocomplete-tokenfield/index.js +2 -2
  95. package/src/autocomplete-with-latest-posts/index.js +290 -0
  96. package/src/autocomplete-with-latest-posts/style.scss +150 -0
  97. package/src/badge/index.tsx +24 -0
  98. package/src/badge/style.scss +36 -0
  99. package/src/button/index.tsx +3 -2
  100. package/src/card/core-card.js +147 -0
  101. package/src/card/index.js +39 -1
  102. package/src/card/style-core.scss +227 -0
  103. package/src/card/style.scss +2 -2
  104. package/src/card-settings-group/README.md +43 -0
  105. package/src/card-settings-group/index.tsx +53 -0
  106. package/src/card-settings-group/style.scss +33 -0
  107. package/src/card-sortable-list/README.md +51 -0
  108. package/src/card-sortable-list/index.tsx +412 -0
  109. package/src/card-sortable-list/style.scss +48 -0
  110. package/src/confirm-dialog/index.tsx +64 -0
  111. package/src/divider/README.md +25 -0
  112. package/src/divider/index.js +31 -0
  113. package/src/divider/style.scss +51 -0
  114. package/src/form-token-field/style.scss +0 -5
  115. package/src/grid/index.tsx +2 -1
  116. package/src/grid/style.scss +63 -9
  117. package/src/index.js +6 -0
  118. package/src/modal/index.js +12 -3
  119. package/src/modal/style.scss +43 -13
  120. package/src/newspack-icon/style.scss +5 -3
  121. package/src/popover/style.scss +1 -1
  122. package/src/section-header/index.js +72 -13
  123. package/src/section-header/style.scss +109 -4
  124. package/src/style.scss +2 -0
  125. package/src/waiting/index.js +2 -1
  126. package/src/waiting/style.scss +4 -0
  127. package/src/web-preview/index.js +90 -35
  128. package/src/web-preview/style.scss +20 -53
  129. package/src/with-wizard/index.js +1 -1
  130. package/src/with-wizard/style.scss +24 -6
  131. package/src/with-wizard-screen/style.scss +39 -10
  132. package/src/wizard/components/WizardSnackbar.js +58 -0
  133. package/src/wizard/components/style.scss +48 -0
  134. package/src/wizard/index.js +108 -14
  135. package/src/wizard/store/index.js +26 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Snackbar.
3
+ */
4
+
5
+ /**
6
+ * WordPress dependencies.
7
+ */
8
+ import { Snackbar as BaseComponent } from '@wordpress/components';
9
+ import { useDispatch } from '@wordpress/data';
10
+
11
+ /**
12
+ * Internal dependencies.
13
+ */
14
+ import { WIZARD_STORE_NAMESPACE } from '../store';
15
+ import './style.scss';
16
+
17
+ /**
18
+ * External dependencies.
19
+ */
20
+ import classnames from 'classnames';
21
+
22
+ /**
23
+ * WizardSnackbar component.
24
+ *
25
+ * @param {Object} props - The component props.
26
+ * @param {Object[]} props.actions - The actions to display in the snackbar.
27
+ * @param {JSX.Element} props.children - The component children.
28
+ * @param {Object} props.props - The component props. See: https://wordpress.github.io/gutenberg/?path=/docs/components-snackbar--docs
29
+ * @param {string} props.position - The snackbar position.
30
+ * @param {string} props.type - The snackbar type: 'info', 'success', 'warning', or 'error'.
31
+ * @return {JSX.Element} The component.
32
+ */
33
+ import { jsx as _jsx } from "react/jsx-runtime";
34
+ const WizardSnackbar = ({
35
+ children,
36
+ position = 'bottom-left',
37
+ type = 'info',
38
+ actions = [],
39
+ ...props
40
+ }) => {
41
+ const className = classnames('newspack-wizard__snackbar', props.className, `newspack-wizard__snackbar--${position}`, `newspack-wizard__snackbar--${type}`);
42
+ const {
43
+ removeNotice,
44
+ resetNotices
45
+ } = useDispatch(WIZARD_STORE_NAMESPACE);
46
+ const onRemove = () => {
47
+ if (props.onRemove) {
48
+ props.onRemove();
49
+ }
50
+ if (props.id) {
51
+ removeNotice(props.id);
52
+ } else {
53
+ resetNotices();
54
+ }
55
+ };
56
+ return /*#__PURE__*/_jsx(BaseComponent, {
57
+ className: className,
58
+ ...props,
59
+ onRemove: onRemove,
60
+ actions: actions,
61
+ children: children
62
+ });
63
+ };
64
+ export default WizardSnackbar;
@@ -0,0 +1,48 @@
1
+ .newspack-wizard {
2
+ &__snackbar {
3
+ border-bottom: 2px solid transparent;
4
+ bottom: 16px;
5
+ left: 16px;
6
+ max-width: calc(100vw - 32px);
7
+ position: fixed;
8
+ z-index: 99999;
9
+ &--success {
10
+ border-color: var(--newspack-ui-color-success-60);
11
+ }
12
+ &--error {
13
+ border-color: var(--newspack-ui-color-error-60);
14
+ }
15
+ &--warning {
16
+ border-color: var(--newspack-ui-color-warning-40);
17
+ }
18
+ @media screen and ( min-width: 783px ) {
19
+ left: calc(36px + 16px); /* 36px for admin menu bar */
20
+ }
21
+ @media screen and ( min-width: 960px ) {
22
+ left: calc(160px + 16px); /* 160px for admin menu bar */
23
+ }
24
+ &.newspack-wizard__snackbar--bottom-right,
25
+ &.newspack-wizard__snackbar--top-right {
26
+ left: auto;
27
+ right: calc(16px);
28
+ }
29
+ &.newspack-wizard__snackbar--top-left,
30
+ &.newspack-wizard__snackbar--top-right {
31
+ bottom: auto;
32
+ top: calc(16px + 46px); /* 46px for admin bar */
33
+ @media screen and ( min-width: 783px ) {
34
+ top: calc(16px + 32px); /* 32px for admin bar */
35
+ }
36
+ }
37
+ a,
38
+ .components-button.is-link {
39
+ color: var(--newspack-ui-color-neutral-0);
40
+ cursor: pointer;
41
+ margin-left: 16px;
42
+ &:hover,
43
+ &:focus {
44
+ color: var(--newspack-ui-color-neutral-10);
45
+ }
46
+ }
47
+ }
48
+ }
@@ -6,17 +6,19 @@ import classnames from 'classnames';
6
6
  /**
7
7
  * WordPress dependencies.
8
8
  */
9
- import { useSelect } from '@wordpress/data';
10
- import { useState, forwardRef } from '@wordpress/element';
9
+ import { DropdownMenu, MenuItem } from '@wordpress/components';
10
+ import { useDispatch, useSelect } from '@wordpress/data';
11
+ import { useEffect, useState, forwardRef } from '@wordpress/element';
11
12
  import { __ } from '@wordpress/i18n';
12
- import { category } from '@wordpress/icons';
13
+ import { category, moreVertical } from '@wordpress/icons';
13
14
 
14
15
  /**
15
16
  * Internal dependencies
16
17
  */
17
- import { Footer, Notice, Button, NewspackIcon, TabbedNavigation, PluginInstaller, HandoffMessage } from '../';
18
+ import { Footer, Notice, Button, NewspackIcon, TabbedNavigation, PluginInstaller, SectionHeader, HandoffMessage } from '../';
18
19
  import Router from '../proxied-imports/router';
19
20
  import registerStore, { WIZARD_STORE_NAMESPACE } from './store';
21
+ import WizardSnackbar from './components/WizardSnackbar';
20
22
  import WizardError from './components/WizardError';
21
23
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
24
  registerStore();
@@ -24,9 +26,24 @@ const {
24
26
  HashRouter,
25
27
  Redirect,
26
28
  Route,
27
- Switch
29
+ Switch,
30
+ useLocation
28
31
  } = Router;
29
32
 
33
+ /**
34
+ * Reset the header data when a new section is rendered.
35
+ */
36
+ const ResetHeaderData = () => {
37
+ const location = useLocation();
38
+ const {
39
+ resetHeaderData
40
+ } = useDispatch(WIZARD_STORE_NAMESPACE);
41
+ useEffect(() => {
42
+ resetHeaderData();
43
+ }, [location.pathname, resetHeaderData]);
44
+ return null;
45
+ };
46
+
30
47
  /**
31
48
  * @typedef {Object} WizardProps
32
49
  * @property {string} headerText The header text.
@@ -52,15 +69,31 @@ const Wizard = ({
52
69
  sections = [],
53
70
  headerText,
54
71
  apiSlug,
72
+ sharedProps = {},
55
73
  subHeaderText,
56
74
  hasSimpleFooter,
57
75
  className,
58
76
  renderAboveSections,
59
77
  requiredPlugins = [],
60
- isInitialFetchTriggered = true
78
+ isInitialFetchTriggered = true,
79
+ fixedHeader = false
61
80
  }, ref) => {
62
81
  const isLoading = useSelect(select => select(WIZARD_STORE_NAMESPACE).isLoading());
63
82
  const isQuietLoading = useSelect(select => select(WIZARD_STORE_NAMESPACE).isQuietLoading());
83
+ const headerData = useSelect(select => select(WIZARD_STORE_NAMESPACE).getHeaderData());
84
+ const notices = useSelect(select => select(WIZARD_STORE_NAMESPACE).getNotices());
85
+ const {
86
+ actions,
87
+ backNav,
88
+ badges,
89
+ sectionDescription,
90
+ sectionName,
91
+ sectionTitle,
92
+ sectionPrimaryAction,
93
+ sectionSecondaryAction
94
+ } = headerData;
95
+ const mainActions = actions?.filter(action => action.type === 'primary' || action.type === 'secondary');
96
+ const moreActions = actions?.filter(action => action.type === 'more');
64
97
 
65
98
  // Trigger initial data fetch. Some sections might not use the wizard data,
66
99
  // but for consistency, fetching is triggered regardless of the section.
@@ -82,17 +115,18 @@ const Wizard = ({
82
115
  const urlWithoutHash = window.location.href.split('#')[0];
83
116
  return /*#__PURE__*/_jsxs("div", {
84
117
  ref: ref,
85
- children: [/*#__PURE__*/_jsx("div", {
118
+ children: [/*#__PURE__*/_jsxs("div", {
86
119
  className: classnames(isLoading ? 'newspack-wizard__is-loading' : 'newspack-wizard__is-loaded', {
87
- 'newspack-wizard__is-loading-quiet': isQuietLoading
120
+ 'newspack-wizard__is-loading-quiet': isQuietLoading,
121
+ 'newspack-wizard__fixed-header': fixedHeader
88
122
  }),
89
- children: /*#__PURE__*/_jsxs(HashRouter, {
123
+ children: [/*#__PURE__*/_jsxs(HashRouter, {
90
124
  hashType: "slash",
91
125
  children: [newspack_aux_data.is_debug_mode && /*#__PURE__*/_jsx(Notice, {
92
126
  debugMode: true
93
- }), /*#__PURE__*/_jsx("div", {
94
- className: "bg-white",
95
- children: /*#__PURE__*/_jsx("div", {
127
+ }), /*#__PURE__*/_jsxs("div", {
128
+ className: "newspack-wizard__header",
129
+ children: [/*#__PURE__*/_jsx("div", {
96
130
  className: "newspack-wizard__header__inner",
97
131
  children: /*#__PURE__*/_jsxs("div", {
98
132
  className: "newspack-wizard__title",
@@ -109,34 +143,81 @@ const Wizard = ({
109
143
  }) : /*#__PURE__*/_jsx(NewspackIcon, {
110
144
  size: 36
111
145
  }), /*#__PURE__*/_jsxs("div", {
112
- children: [headerText && /*#__PURE__*/_jsx("h2", {
113
- children: headerText
146
+ children: [headerText && /*#__PURE__*/_jsxs("h2", {
147
+ className: "newspack-wizard__header__title",
148
+ children: [headerText, sectionName && /*#__PURE__*/_jsxs("span", {
149
+ className: "newspack-wizard__header__section",
150
+ children: [/*#__PURE__*/_jsx("span", {
151
+ className: "newspack-wizard__header__section__separator",
152
+ children: " / "
153
+ }), " ", sectionName]
154
+ })]
114
155
  }), subHeaderText && /*#__PURE__*/_jsx("span", {
115
156
  children: subHeaderText
116
157
  })]
117
158
  })]
118
159
  })
119
- })
160
+ }), actions?.length > 0 && /*#__PURE__*/_jsxs("div", {
161
+ className: "newspack-wizard__header__actions",
162
+ children: [mainActions.map((action, index) => /*#__PURE__*/_jsx(Button, {
163
+ icon: action.icon,
164
+ variant: action.type,
165
+ onClick: action.action,
166
+ disabled: action.disabled || false,
167
+ isDestructive: action.destructive || false,
168
+ children: action.label
169
+ }, index)), moreActions.length > 0 && /*#__PURE__*/_jsx(DropdownMenu, {
170
+ icon: moreVertical,
171
+ label: __('More', 'newspack-plugin'),
172
+ children: () => moreActions.map((action, index) => /*#__PURE__*/_jsx(MenuItem, {
173
+ icon: action.icon,
174
+ onClick: action.action,
175
+ disabled: action.disabled || false,
176
+ isDestructive: action.destructive || false,
177
+ children: action.label
178
+ }, index))
179
+ })]
180
+ })]
120
181
  }), displayedSections.length > 1 && /*#__PURE__*/_jsx(TabbedNavigation, {
121
182
  items: displayedSections,
122
183
  children: /*#__PURE__*/_jsx(WizardError, {})
123
- }), /*#__PURE__*/_jsx(HandoffMessage, {}), /*#__PURE__*/_jsxs(Switch, {
124
- children: [displayedSections.map((section, index) => {
184
+ }), /*#__PURE__*/_jsx(HandoffMessage, {}), sections.length > 1 && /*#__PURE__*/_jsx(ResetHeaderData, {}), /*#__PURE__*/_jsxs(Switch, {
185
+ children: [sections.map((section, index) => {
125
186
  var _section$exact;
126
187
  const SectionComponent = section.render;
188
+ const sectionProps = section.props || {};
127
189
  return /*#__PURE__*/_jsx(Route, {
128
190
  exact: (_section$exact = section.exact) !== null && _section$exact !== void 0 ? _section$exact : false,
129
191
  path: section.path,
130
- children: /*#__PURE__*/_jsxs("div", {
192
+ render: routerProps => /*#__PURE__*/_jsxs("div", {
131
193
  className: classnames('newspack-wizard__content', className),
132
- children: ['function' === typeof renderAboveSections ? renderAboveSections() : null, /*#__PURE__*/_jsx(SectionComponent, {})]
194
+ children: ['function' === typeof renderAboveSections ? renderAboveSections() : null, (sectionTitle || section.title) && /*#__PURE__*/_jsx(SectionHeader, {
195
+ className: "newspack-wizard__section-header",
196
+ backNav: backNav || section.backNav,
197
+ title: sectionTitle || section.title,
198
+ description: sectionDescription || section.description,
199
+ badges: badges || section.badges,
200
+ primaryAction: sectionPrimaryAction || section.primaryAction,
201
+ secondaryAction: sectionSecondaryAction || section.secondaryAction,
202
+ heading: 1,
203
+ noMargin: true
204
+ }), /*#__PURE__*/_jsx(SectionComponent, {
205
+ ...routerProps,
206
+ ...sectionProps,
207
+ ...sharedProps
208
+ })]
133
209
  })
134
210
  }, index);
135
211
  }), /*#__PURE__*/_jsx(Redirect, {
136
212
  to: displayedSections[0].path
137
213
  })]
138
214
  })]
139
- })
215
+ }), notices?.length > 0 && notices.map((notice, index) => /*#__PURE__*/_jsx(WizardSnackbar, {
216
+ type: notice.type,
217
+ id: notice.id,
218
+ actions: notice.actions,
219
+ children: notice.message
220
+ }, notice.id || index))]
140
221
  }), !isLoading && /*#__PURE__*/_jsx(Footer, {
141
222
  simple: hasSimpleFooter
142
223
  })]
@@ -13,9 +13,18 @@ import { createReduxStore, register, dispatch, select } from '@wordpress/data';
13
13
  import { createAction } from './utils.js';
14
14
  export const WIZARD_STORE_NAMESPACE = 'newspack/wizards';
15
15
  const DEFAULT_STATE = {
16
+ headerData: {
17
+ actions: [],
18
+ backNav: '',
19
+ badges: [],
20
+ sectionDescription: '',
21
+ sectionName: '',
22
+ sectionTitle: ''
23
+ },
16
24
  isLoading: false,
17
25
  isQuietLoading: false,
18
26
  apiData: {},
27
+ notices: [],
19
28
  error: null
20
29
  };
21
30
 
@@ -30,6 +39,21 @@ const reducer = (state = DEFAULT_STATE, {
30
39
  payload = {}
31
40
  }) => {
32
41
  switch (type) {
42
+ case 'SET_HEADER_DATA':
43
+ return {
44
+ ...state,
45
+ headerData: {
46
+ ...state.headerData,
47
+ ...payload
48
+ }
49
+ };
50
+ case 'RESET_HEADER_DATA':
51
+ return {
52
+ ...state,
53
+ headerData: {
54
+ ...DEFAULT_STATE.headerData
55
+ }
56
+ };
33
57
  case 'START_LOADING_DATA':
34
58
  if (payload.isQuietLoading) {
35
59
  return {
@@ -51,22 +75,42 @@ const reducer = (state = DEFAULT_STATE, {
51
75
  return set(clone(state), ['apiData', payload.slug], payload.data);
52
76
  case 'UPDATE_WIZARD_SETTINGS':
53
77
  return set(clone(state), ['apiData', payload.slug, ...payload.path], payload.value);
78
+ case 'ADD_NOTICE':
79
+ return {
80
+ ...state,
81
+ notices: [...state.notices, payload]
82
+ };
83
+ case 'REMOVE_NOTICE':
84
+ return {
85
+ ...state,
86
+ notices: state.notices.filter(notice => notice.id !== payload)
87
+ };
54
88
  case 'SET_ERROR':
55
89
  return {
56
90
  ...state,
57
91
  error: payload
58
92
  };
93
+ case 'RESET_NOTICES':
94
+ return {
95
+ ...state,
96
+ notices: DEFAULT_STATE.notices
97
+ };
59
98
  default:
60
99
  return state;
61
100
  }
62
101
  };
63
102
  const actions = {
64
103
  // Regular actions.
104
+ setHeaderData: createAction('SET_HEADER_DATA'),
105
+ resetHeaderData: createAction('RESET_HEADER_DATA'),
65
106
  startLoadingData: createAction('START_LOADING_DATA'),
66
107
  finishLoadingData: createAction('FINISH_LOADING_DATA'),
67
108
  fetchFromAPI: createAction('FETCH_FROM_API'),
68
109
  setAPIDataForWizard: createAction('SET_API_DATA'),
69
110
  updateWizardSettings: createAction('UPDATE_WIZARD_SETTINGS'),
111
+ addNotice: createAction('ADD_NOTICE'),
112
+ removeNotice: createAction('REMOVE_NOTICE'),
113
+ resetNotices: createAction('RESET_NOTICES'),
70
114
  setError: createAction('SET_ERROR'),
71
115
  // Async actions. These will not show up in Redux devtools.
72
116
  *saveWizardSettings({
@@ -109,6 +153,7 @@ const actions = {
109
153
  }
110
154
  };
111
155
  const selectors = {
156
+ getHeaderData: state => state.headerData,
112
157
  isLoading: state => state.isLoading,
113
158
  isQuietLoading: state => state.isQuietLoading,
114
159
  getWizardAPIData: (state, slug) => state.apiData[slug] || {},
@@ -116,6 +161,7 @@ const selectors = {
116
161
  var _state$apiData$slug;
117
162
  return (_state$apiData$slug = state.apiData[slug]) !== null && _state$apiData$slug !== void 0 ? _state$apiData$slug : {};
118
163
  },
164
+ getNotices: state => state.notices,
119
165
  getError: state => state.error
120
166
  };
121
167
  const store = createReduxStore(WIZARD_STORE_NAMESPACE, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newspack-components",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "Newspack design system components",
5
5
  "author": "Automattic",
6
6
  "license": "GPL-2.0-or-later",
@@ -23,16 +23,15 @@
23
23
  "access": "public"
24
24
  },
25
25
  "dependencies": {
26
- "@wordpress/base-styles": "^5.0.0",
27
- "@wordpress/components": "^28.0.0",
26
+ "@wordpress/base-styles": "^6.0.0",
27
+ "@wordpress/components": "^32.0.0",
28
28
  "@wordpress/element": "^6.0.0",
29
- "@wordpress/i18n": "^5.0.0",
30
- "@wordpress/icons": "^10.0.0",
29
+ "@wordpress/i18n": "^6.0.0",
30
+ "@wordpress/icons": "^11.0.0",
31
31
  "classnames": "^2.3.1",
32
32
  "lodash": "^4.17.21",
33
33
  "moment-range": "^3.1.1",
34
34
  "qs": "^6.10.1",
35
- "react-daterange-picker": "^2.0.1",
36
35
  "react-router-dom": "^5.3.4"
37
36
  },
38
37
  "devDependencies": {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { createContext } from '@wordpress/element';
5
+
6
+ /**
7
+ * Window global key used by newspack-blocks to expose the shared AuthorContext.
8
+ *
9
+ * @type {string}
10
+ */
11
+ export const AUTHOR_CONTEXT_KEY = 'NewspackAuthorContext';
12
+
13
+ /**
14
+ * Fallback context that always returns null.
15
+ * Used when the shared AuthorContext from newspack-blocks is not available.
16
+ */
17
+ const FallbackAuthorContext = createContext( null );
18
+
19
+ /**
20
+ * Get the shared AuthorContext from newspack-blocks if available, otherwise use fallback.
21
+ * Resolved at render time (not module load) so it works regardless of script load order.
22
+ *
23
+ * @return {Object} React context object.
24
+ */
25
+ export const getSharedAuthorContext = () =>
26
+ typeof window !== 'undefined' && window[ AUTHOR_CONTEXT_KEY ] ? window[ AUTHOR_CONTEXT_KEY ] : FallbackAuthorContext;
@@ -0,0 +1,220 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useSelect } from '@wordpress/data';
5
+ import { useState, useEffect, useMemo } from '@wordpress/element';
6
+ import { store as coreStore } from '@wordpress/core-data';
7
+ import apiFetch from '@wordpress/api-fetch';
8
+
9
+ /**
10
+ * CoAuthors Plus store name.
11
+ */
12
+ const CAP_STORE = 'cap/authors';
13
+ const COAUTHORS_ENDPOINT = '/coauthors/v1/coauthors';
14
+
15
+ // Module-level cache for guest author avatar URLs, keyed by user_nicename.
16
+ // Prevents duplicate REST requests when components re-mount or when
17
+ // multiple avatar blocks in a Query Loop share the same guest authors.
18
+ const guestAvatarCache = {};
19
+
20
+ // In-flight request promises, keyed by user_nicename.
21
+ // Prevents duplicate concurrent requests when multiple block instances
22
+ // mount simultaneously (e.g. Query Loop) and all check the cache before
23
+ // any fetch has completed.
24
+ const inflightRequests = {};
25
+
26
+ /**
27
+ * Fetch a single coauthor's avatar URLs, deduplicating concurrent requests.
28
+ *
29
+ * @param {string} nicename Author nicename (slug).
30
+ * @return {Promise} Resolves when the fetch completes (result is stored in guestAvatarCache).
31
+ */
32
+ function fetchCoauthorAvatar( nicename ) {
33
+ if ( nicename in guestAvatarCache ) {
34
+ return Promise.resolve();
35
+ }
36
+ if ( inflightRequests[ nicename ] ) {
37
+ return inflightRequests[ nicename ];
38
+ }
39
+ inflightRequests[ nicename ] = apiFetch( {
40
+ path: `${ COAUTHORS_ENDPOINT }/${ encodeURIComponent( nicename ) }`,
41
+ } )
42
+ .then( result => {
43
+ guestAvatarCache[ nicename ] = result?.avatar_urls || false;
44
+ } )
45
+ .catch( () => {
46
+ guestAvatarCache[ nicename ] = false;
47
+ } )
48
+ .finally( () => {
49
+ delete inflightRequests[ nicename ];
50
+ } );
51
+ return inflightRequests[ nicename ];
52
+ }
53
+
54
+ /**
55
+ * Reset the module-level guest avatar cache. Exposed for testing only.
56
+ */
57
+ export function resetGuestAvatarCacheForTests() {
58
+ Object.keys( guestAvatarCache ).forEach( key => delete guestAvatarCache[ key ] );
59
+ Object.keys( inflightRequests ).forEach( key => delete inflightRequests[ key ] );
60
+ }
61
+
62
+ /**
63
+ * Extract user_nicename from an author archive URL.
64
+ *
65
+ * @param {string} link Author archive URL (e.g. /author/jane/).
66
+ * @return {string|undefined} Extracted nicename, or undefined.
67
+ */
68
+ function extractNicenameFromLink( link ) {
69
+ if ( ! link ) {
70
+ return undefined;
71
+ }
72
+ const cleaned = link.split( '?' )[ 0 ].split( '#' )[ 0 ].replace( /\/$/, '' );
73
+ const segments = cleaned.split( '/' );
74
+ const slug = segments.pop();
75
+ // Require a parent path segment so root URLs and plain permalinks are rejected.
76
+ const parent = segments[ segments.length - 1 ];
77
+ if ( ! slug || ! parent ) {
78
+ return undefined;
79
+ }
80
+ return slug;
81
+ }
82
+
83
+ /**
84
+ * Hook to get CoAuthors Plus authors from the CAP store or REST API.
85
+ *
86
+ * For the currently-edited post, it uses CAP's JS store for real-time updates.
87
+ * For Query Loop posts, it uses REST API data since CAP's store only works for the
88
+ * currently-edited post.
89
+ *
90
+ * @param {number} postId Post ID to get authors for.
91
+ * @param {string} postType Post type (default: 'post').
92
+ * @param {boolean} skip Skip fetching (default: false).
93
+ * @return {Object} Authors array and availability state.
94
+ */
95
+ export function useCoAuthors( postId, postType = 'post', skip = false ) {
96
+ // Return raw store references from useSelect to avoid creating new objects
97
+ // on every render (which triggers useSelect memoization warnings).
98
+ // The .map() transformations happen in useMemo below.
99
+ const { capAuthors, restAuthors, isCapAvailable } = useSelect(
100
+ select => {
101
+ if ( skip ) {
102
+ return { capAuthors: null, restAuthors: null, isCapAvailable: false };
103
+ }
104
+
105
+ const capStore = select( CAP_STORE );
106
+ const isCapStoreAvailable = Boolean( capStore && typeof capStore.getAuthors === 'function' );
107
+
108
+ const editorStore = select( 'core/editor' );
109
+ const currentPostId = editorStore?.getCurrentPostId?.();
110
+ const isQueryLoopContext = postId && currentPostId && postId !== currentPostId;
111
+
112
+ // For the currently-edited post, use CAP's store for real-time updates.
113
+ if ( isCapStoreAvailable && ! isQueryLoopContext ) {
114
+ const rawCapAuthors = postId ? capStore.getAuthors( postId ) : null;
115
+ return { capAuthors: rawCapAuthors, restAuthors: null, isCapAvailable: true };
116
+ }
117
+
118
+ // For Query Loop context, try to get coauthors from REST API.
119
+ // If REST author data exists, treat CAP as available (matching original behavior)
120
+ // since the server-side plugin provided the data even if the JS store isn't loaded.
121
+ if ( isQueryLoopContext && postId ) {
122
+ const { getEntityRecord } = select( coreStore );
123
+ const post = getEntityRecord( 'postType', postType, postId );
124
+ const restData = post?.newspack_author_info || null;
125
+ return { capAuthors: null, restAuthors: restData, isCapAvailable: restData ? true : isCapStoreAvailable };
126
+ }
127
+
128
+ return { capAuthors: null, restAuthors: null, isCapAvailable: isCapStoreAvailable };
129
+ },
130
+ [ postId, postType, skip ]
131
+ );
132
+
133
+ // Map raw store data to our normalized author format.
134
+ const authors = useMemo( () => {
135
+ // CAP store authors: { id, label, display, value, userType }
136
+ if ( capAuthors && capAuthors.length > 0 ) {
137
+ return capAuthors.map( author => ( {
138
+ id: author.id,
139
+ display_name: author.display || author.value || author.label,
140
+ user_nicename: author.value,
141
+ isGuest: author.userType === 'guest-author',
142
+ } ) );
143
+ }
144
+
145
+ // REST API authors from newspack_author_info.
146
+ if ( restAuthors && Array.isArray( restAuthors ) && restAuthors.length > 0 ) {
147
+ return restAuthors.map( author => ( {
148
+ id: author.id,
149
+ display_name: author.display_name,
150
+ author_link: author.author_link,
151
+ user_nicename: author.user_nicename || extractNicenameFromLink( author.author_link ),
152
+ ...( typeof author.is_guest === 'boolean' ? { isGuest: author.is_guest } : {} ),
153
+ ...( author.avatar_urls ? { avatar_urls: author.avatar_urls } : {} ),
154
+ } ) );
155
+ }
156
+
157
+ return [];
158
+ }, [ capAuthors, restAuthors ] );
159
+
160
+ // Fetch avatar URLs from the CAP REST API for authors that need it.
161
+ // The CAP store strips avatar data via formatAuthorData(), so we need
162
+ // to fetch the raw REST response to get the avatar URL (especially for
163
+ // guest authors whose avatars come from featured images).
164
+ //
165
+ // Fetches per-author (by nicename) instead of per-post so that avatars
166
+ // resolve immediately when a guest author is added — before the post is saved.
167
+ //
168
+ // For currently-edited post authors, isGuest is explicitly true/false.
169
+ // For Query Loop authors, isGuest is undefined (we can't distinguish from
170
+ // REST data alone), so we fetch for all of them and let the consumer
171
+ // (hooks.js) prefer WP user data when available.
172
+ const [ avatarMap, setAvatarMap ] = useState( {} );
173
+
174
+ // Build a stable key from author nicenames to control effect re-runs.
175
+ // isGuest !== false: includes guest authors (true) and Query Loop authors (undefined),
176
+ // but excludes known WP users from the currently-edited post (false).
177
+ const authorsNeedingAvatars = authors.filter( author => author.isGuest !== false && author.user_nicename && ! author.avatar_urls );
178
+ const avatarNicenames = authorsNeedingAvatars.map( author => author.user_nicename ).join( ',' );
179
+
180
+ useEffect( () => {
181
+ if ( skip || ! isCapAvailable || ! avatarNicenames ) {
182
+ return;
183
+ }
184
+
185
+ let cancelled = false;
186
+ Promise.all( authorsNeedingAvatars.map( a => fetchCoauthorAvatar( a.user_nicename ) ) ).then( () => {
187
+ if ( cancelled ) {
188
+ return;
189
+ }
190
+ const map = {};
191
+ authorsNeedingAvatars.forEach( a => {
192
+ if ( guestAvatarCache[ a.user_nicename ] ) {
193
+ map[ a.id ] = guestAvatarCache[ a.user_nicename ];
194
+ }
195
+ } );
196
+ setAvatarMap( map );
197
+ } );
198
+
199
+ return () => {
200
+ cancelled = true;
201
+ };
202
+ }, [ skip, isCapAvailable, avatarNicenames ] );
203
+
204
+ // Merge avatar URLs into guest authors.
205
+ // Check both the async avatarMap (populated by the effect) and the
206
+ // synchronous guestAvatarCache (populated by previous fetches) so that
207
+ // cached avatars render immediately without waiting for an effect cycle.
208
+ const authorsWithAvatars = authors.map( author => {
209
+ if ( author.isGuest === false || ! author.user_nicename ) {
210
+ return author;
211
+ }
212
+ const urls = avatarMap[ author.id ] || guestAvatarCache[ author.user_nicename ];
213
+ if ( ! urls ) {
214
+ return author;
215
+ }
216
+ return { ...author, avatar_urls: urls };
217
+ } );
218
+
219
+ return { authors: authorsWithAvatars, isCapAvailable };
220
+ }