newspack-components 4.2.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.
- package/dist/cjs/action-card/index.js +21 -12
- package/dist/cjs/action-card/style.scss +25 -2
- package/dist/cjs/autocomplete-tokenfield/index.js +2 -2
- package/dist/cjs/autocomplete-with-latest-posts/index.js +289 -0
- package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
- package/dist/cjs/badge/index.js +32 -0
- package/dist/cjs/badge/style.scss +36 -0
- package/dist/cjs/button/index.js +2 -0
- package/dist/cjs/card/core-card.js +141 -0
- package/dist/cjs/card/index.js +33 -0
- package/dist/cjs/card/style-core.scss +227 -0
- package/dist/cjs/card/style.scss +2 -2
- package/dist/cjs/card-settings-group/index.js +49 -0
- package/dist/cjs/card-settings-group/style.scss +33 -0
- package/dist/cjs/card-sortable-list/index.js +390 -0
- package/dist/cjs/card-sortable-list/style.scss +48 -0
- package/dist/cjs/confirm-dialog/index.js +59 -0
- package/dist/cjs/divider/index.js +42 -0
- package/dist/cjs/divider/style.scss +51 -0
- package/dist/cjs/form-token-field/style.scss +0 -5
- package/dist/cjs/grid/index.js +2 -1
- package/dist/cjs/grid/style.scss +63 -9
- package/dist/cjs/index.js +42 -0
- package/dist/cjs/modal/index.js +11 -4
- package/dist/cjs/modal/style.scss +43 -13
- package/dist/cjs/newspack-icon/style.scss +5 -3
- package/dist/cjs/section-header/index.js +76 -17
- package/dist/cjs/section-header/style.scss +109 -4
- package/dist/cjs/style.scss +2 -0
- package/dist/cjs/waiting/index.js +3 -1
- package/dist/cjs/waiting/style.scss +4 -0
- package/dist/cjs/web-preview/index.js +98 -39
- package/dist/cjs/web-preview/style.scss +20 -53
- package/dist/cjs/with-wizard/index.js +1 -1
- package/dist/cjs/with-wizard/style.scss +24 -6
- package/dist/cjs/with-wizard-screen/style.scss +39 -10
- package/dist/cjs/wizard/components/WizardSnackbar.js +70 -0
- package/dist/cjs/wizard/components/style.scss +48 -0
- package/dist/cjs/wizard/index.js +97 -16
- package/dist/cjs/wizard/store/index.js +46 -0
- package/dist/esm/action-card/index.js +21 -12
- package/dist/esm/action-card/style.scss +25 -2
- package/dist/esm/autocomplete-tokenfield/index.js +2 -2
- package/dist/esm/autocomplete-with-latest-posts/index.js +281 -0
- package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
- package/dist/esm/badge/index.js +24 -0
- package/dist/esm/badge/style.scss +36 -0
- package/dist/esm/button/index.js +2 -0
- package/dist/esm/card/core-card.js +133 -0
- package/dist/esm/card/index.js +33 -0
- package/dist/esm/card/style-core.scss +227 -0
- package/dist/esm/card/style.scss +2 -2
- package/dist/esm/card-settings-group/index.js +42 -0
- package/dist/esm/card-settings-group/style.scss +33 -0
- package/dist/esm/card-sortable-list/index.js +381 -0
- package/dist/esm/card-sortable-list/style.scss +48 -0
- package/dist/esm/confirm-dialog/index.js +49 -0
- package/dist/esm/divider/index.js +34 -0
- package/dist/esm/divider/style.scss +51 -0
- package/dist/esm/form-token-field/style.scss +0 -5
- package/dist/esm/grid/index.js +2 -1
- package/dist/esm/grid/style.scss +63 -9
- package/dist/esm/index.js +6 -0
- package/dist/esm/modal/index.js +11 -4
- package/dist/esm/modal/style.scss +43 -13
- package/dist/esm/newspack-icon/style.scss +5 -3
- package/dist/esm/section-header/index.js +75 -18
- package/dist/esm/section-header/style.scss +109 -4
- package/dist/esm/style.scss +2 -0
- package/dist/esm/waiting/index.js +3 -1
- package/dist/esm/waiting/style.scss +4 -0
- package/dist/esm/web-preview/index.js +101 -42
- package/dist/esm/web-preview/style.scss +20 -53
- package/dist/esm/with-wizard/index.js +1 -1
- package/dist/esm/with-wizard/style.scss +24 -6
- package/dist/esm/with-wizard-screen/style.scss +39 -10
- package/dist/esm/wizard/components/WizardSnackbar.js +64 -0
- package/dist/esm/wizard/components/style.scss +48 -0
- package/dist/esm/wizard/index.js +101 -20
- package/dist/esm/wizard/store/index.js +46 -0
- package/package.json +5 -6
- package/shared/author-context.js +26 -0
- package/shared/hooks/use-coauthors.js +220 -0
- package/shared/hooks/use-coauthors.test.js +624 -0
- package/shared/hooks/use-custom-byline.js +47 -0
- package/shared/hooks/use-custom-byline.test.js +133 -0
- package/shared/scss/_mixins.scss +46 -0
- package/src/action-card/action-card.d.ts +7 -3
- package/src/action-card/index.js +24 -11
- package/src/action-card/style.scss +25 -2
- package/src/autocomplete-tokenfield/index.js +2 -2
- package/src/autocomplete-with-latest-posts/index.js +290 -0
- package/src/autocomplete-with-latest-posts/style.scss +150 -0
- package/src/badge/index.tsx +24 -0
- package/src/badge/style.scss +36 -0
- package/src/button/index.tsx +3 -2
- package/src/card/core-card.js +147 -0
- package/src/card/index.js +39 -1
- package/src/card/style-core.scss +227 -0
- package/src/card/style.scss +2 -2
- package/src/card-settings-group/README.md +43 -0
- package/src/card-settings-group/index.tsx +53 -0
- package/src/card-settings-group/style.scss +33 -0
- package/src/card-sortable-list/README.md +51 -0
- package/src/card-sortable-list/index.tsx +412 -0
- package/src/card-sortable-list/style.scss +48 -0
- package/src/confirm-dialog/index.tsx +64 -0
- package/src/divider/README.md +25 -0
- package/src/divider/index.js +31 -0
- package/src/divider/style.scss +51 -0
- package/src/form-token-field/style.scss +0 -5
- package/src/grid/index.tsx +2 -1
- package/src/grid/style.scss +63 -9
- package/src/index.js +6 -0
- package/src/modal/index.js +12 -3
- package/src/modal/style.scss +43 -13
- package/src/newspack-icon/style.scss +5 -3
- package/src/section-header/index.js +72 -13
- package/src/section-header/style.scss +109 -4
- package/src/style.scss +2 -0
- package/src/waiting/index.js +2 -1
- package/src/waiting/style.scss +4 -0
- package/src/web-preview/index.js +90 -35
- package/src/web-preview/style.scss +20 -53
- package/src/with-wizard/index.js +1 -1
- package/src/with-wizard/style.scss +24 -6
- package/src/with-wizard-screen/style.scss +39 -10
- package/src/wizard/components/WizardSnackbar.js +58 -0
- package/src/wizard/components/style.scss +48 -0
- package/src/wizard/index.js +108 -14
- 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
|
+
}
|
package/dist/esm/wizard/index.js
CHANGED
|
@@ -6,17 +6,19 @@ import classnames from 'classnames';
|
|
|
6
6
|
/**
|
|
7
7
|
* WordPress dependencies.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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__*/
|
|
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__*/
|
|
94
|
-
className: "
|
|
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__*/
|
|
113
|
-
|
|
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: [
|
|
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
|
-
|
|
192
|
+
render: routerProps => /*#__PURE__*/_jsxs("div", {
|
|
131
193
|
className: classnames('newspack-wizard__content', className),
|
|
132
|
-
children: ['function' === typeof renderAboveSections ? renderAboveSections() : null, /*#__PURE__*/_jsx(
|
|
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.
|
|
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": "^
|
|
27
|
-
"@wordpress/components": "^
|
|
26
|
+
"@wordpress/base-styles": "^6.0.0",
|
|
27
|
+
"@wordpress/components": "^32.0.0",
|
|
28
28
|
"@wordpress/element": "^6.0.0",
|
|
29
|
-
"@wordpress/i18n": "^
|
|
30
|
-
"@wordpress/icons": "^
|
|
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
|
+
}
|