newspack-components 4.5.0 → 4.6.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/CHANGELOG.md +40 -0
- package/DEVELOPMENT.md +1 -1
- package/dist/cjs/accordion/index.js +43 -26
- package/dist/cjs/accordion/index.test.js +80 -0
- package/dist/cjs/accordion/style.scss +25 -21
- package/dist/cjs/action-card/style.scss +0 -22
- package/dist/cjs/breadcrumbs/index.js +80 -0
- package/dist/cjs/breadcrumbs/index.test.js +109 -0
- package/dist/cjs/breadcrumbs/style.scss +41 -0
- package/dist/cjs/card/core-card.js +8 -4
- package/dist/cjs/card/index.js +1 -1
- package/dist/cjs/card/style-core.scss +14 -0
- package/dist/cjs/card/style.scss +0 -9
- package/dist/cjs/card-feature/index.js +25 -13
- package/dist/cjs/card-settings-group/index.js +3 -0
- package/dist/cjs/confirm-dialog/index.js +34 -8
- package/dist/cjs/confirm-dialog/index.test.js +173 -0
- package/dist/cjs/divider/style.scss +4 -2
- package/dist/cjs/grid/style.scss +5 -0
- package/dist/cjs/handoff/index.js +8 -5
- package/dist/cjs/index.js +34 -1
- package/dist/cjs/integration-icon/index.js +40 -0
- package/dist/cjs/integration-icon/style.scss +42 -0
- package/dist/cjs/integration-icons/index.js +21 -2
- package/dist/cjs/modal/style.scss +4 -0
- package/dist/cjs/page/index.js +121 -0
- package/dist/cjs/page/index.test.js +69 -0
- package/dist/cjs/page/style.scss +86 -0
- package/dist/cjs/route-match.js +59 -0
- package/dist/cjs/route-match.test.js +77 -0
- package/dist/cjs/section-header/index.js +8 -1
- package/dist/cjs/section-header/style.scss +11 -11
- package/dist/cjs/select-control/GroupedSelectControl.js +1 -1
- package/dist/cjs/select-control/style.scss +4 -0
- package/dist/cjs/style.scss +1 -4
- package/dist/cjs/tabbed-navigation/index.js +188 -40
- package/dist/cjs/tabbed-navigation/index.test.js +299 -0
- package/dist/cjs/tabbed-navigation/style.scss +77 -57
- package/dist/cjs/with-wizard/index.js +34 -45
- package/dist/cjs/with-wizard-screen/index.js +82 -41
- package/dist/cjs/with-wizard-screen/index.test.js +50 -0
- package/dist/cjs/with-wizard-screen/style.scss +15 -118
- package/dist/cjs/wizard/breadcrumbs-select.js +70 -0
- package/dist/cjs/wizard/breadcrumbs-select.test.js +200 -0
- package/dist/cjs/wizard/components/WizardError.js +8 -4
- package/dist/cjs/wizard/index.js +162 -141
- package/dist/esm/accordion/index.js +42 -26
- package/dist/esm/accordion/index.test.js +79 -0
- package/dist/esm/accordion/style.scss +25 -21
- package/dist/esm/action-card/style.scss +0 -22
- package/dist/esm/breadcrumbs/index.js +75 -0
- package/dist/esm/breadcrumbs/index.test.js +105 -0
- package/dist/esm/breadcrumbs/style.scss +41 -0
- package/dist/esm/card/core-card.js +8 -4
- package/dist/esm/card/index.js +1 -1
- package/dist/esm/card/style-core.scss +14 -0
- package/dist/esm/card/style.scss +0 -9
- package/dist/esm/card-feature/index.js +25 -13
- package/dist/esm/card-settings-group/index.js +3 -0
- package/dist/esm/confirm-dialog/index.js +34 -8
- package/dist/esm/confirm-dialog/index.test.js +173 -0
- package/dist/esm/divider/style.scss +4 -2
- package/dist/esm/grid/style.scss +5 -0
- package/dist/esm/handoff/index.js +7 -5
- package/dist/esm/index.js +5 -1
- package/dist/esm/integration-icon/index.js +34 -0
- package/dist/esm/integration-icon/style.scss +42 -0
- package/dist/esm/integration-icons/index.js +22 -1
- package/dist/esm/modal/style.scss +4 -0
- package/dist/esm/page/index.js +113 -0
- package/dist/esm/page/index.test.js +65 -0
- package/dist/esm/page/style.scss +86 -0
- package/dist/esm/route-match.js +51 -0
- package/dist/esm/route-match.test.js +74 -0
- package/dist/esm/section-header/index.js +8 -1
- package/dist/esm/section-header/style.scss +11 -11
- package/dist/esm/select-control/GroupedSelectControl.js +1 -1
- package/dist/esm/select-control/style.scss +4 -0
- package/dist/esm/style.scss +1 -4
- package/dist/esm/tabbed-navigation/index.js +192 -38
- package/dist/esm/tabbed-navigation/index.test.js +296 -0
- package/dist/esm/tabbed-navigation/style.scss +77 -57
- package/dist/esm/with-wizard/index.js +33 -45
- package/dist/esm/with-wizard-screen/index.js +78 -41
- package/dist/esm/with-wizard-screen/index.test.js +46 -0
- package/dist/esm/with-wizard-screen/style.scss +15 -118
- package/dist/esm/wizard/breadcrumbs-select.js +63 -0
- package/dist/esm/wizard/breadcrumbs-select.test.js +197 -0
- package/dist/esm/wizard/components/WizardError.js +7 -5
- package/dist/esm/wizard/index.js +165 -144
- package/package.json +10 -2
- package/src/accordion/index.js +27 -15
- package/src/accordion/index.test.js +62 -0
- package/src/accordion/style.scss +25 -21
- package/src/action-card/style.scss +0 -22
- package/src/breadcrumbs/index.js +63 -0
- package/src/breadcrumbs/index.test.js +59 -0
- package/src/breadcrumbs/style.scss +41 -0
- package/src/card/core-card.js +11 -6
- package/src/card/index.js +0 -1
- package/src/card/style-core.scss +14 -0
- package/src/card/style.scss +0 -9
- package/src/card-feature/README.md +3 -3
- package/src/card-feature/index.tsx +37 -21
- package/src/card-settings-group/index.tsx +4 -0
- package/src/card-sortable-list/index.tsx +1 -1
- package/src/confirm-dialog/index.test.js +142 -0
- package/src/confirm-dialog/index.tsx +34 -7
- package/src/divider/style.scss +4 -2
- package/src/grid/style.scss +5 -0
- package/src/handoff/index.js +4 -3
- package/src/index.js +5 -1
- package/src/integration-icon/index.tsx +35 -0
- package/src/integration-icon/style.scss +42 -0
- package/src/integration-icons/index.js +23 -0
- package/src/modal/style.scss +4 -0
- package/src/page/index.js +89 -0
- package/src/page/index.test.js +43 -0
- package/src/page/style.scss +86 -0
- package/src/route-match.js +47 -0
- package/src/route-match.test.js +50 -0
- package/src/section-header/index.js +7 -3
- package/src/section-header/style.scss +11 -11
- package/src/select-control/GroupedSelectControl.js +1 -1
- package/src/select-control/style.scss +4 -0
- package/src/style.scss +1 -4
- package/src/tabbed-navigation/index.js +181 -34
- package/src/tabbed-navigation/index.test.js +228 -0
- package/src/tabbed-navigation/style.scss +77 -57
- package/src/with-wizard/index.js +18 -39
- package/src/with-wizard-screen/index.js +70 -35
- package/src/with-wizard-screen/index.test.js +36 -0
- package/src/with-wizard-screen/style.scss +15 -118
- package/src/wizard/breadcrumbs-select.js +51 -0
- package/src/wizard/breadcrumbs-select.test.js +124 -0
- package/src/wizard/components/WizardError.js +4 -3
- package/src/wizard/index.js +160 -146
- package/tsconfig.json +7 -0
package/src/with-wizard/index.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
import { Component, createRef, Fragment } from '@wordpress/element';
|
|
5
5
|
import { __ } from '@wordpress/i18n';
|
|
6
6
|
import apiFetch from '@wordpress/api-fetch';
|
|
7
|
-
import {
|
|
7
|
+
import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal dependencies.
|
|
11
11
|
*/
|
|
12
|
-
import { Button,
|
|
12
|
+
import { Button, Modal, Notice, Page, PluginInstaller } from '../';
|
|
13
13
|
import Router from '../proxied-imports/router';
|
|
14
14
|
import Footer from '../footer';
|
|
15
15
|
import './style.scss';
|
|
@@ -99,11 +99,11 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
|
|
|
99
99
|
return (
|
|
100
100
|
<Modal title={ __( 'Unrecoverable error' ) } onRequestClose={ () => ( window.location = fallbackURL ) }>
|
|
101
101
|
<Notice noticeText={ message } isError rawHTML />
|
|
102
|
-
<
|
|
102
|
+
<HStack justify="flex-end" spacing={ 4 } wrap className="newspack-modal__footer">
|
|
103
103
|
<Button isPrimary href={ fallbackURL }>
|
|
104
104
|
{ __( 'Return to Dashboard', 'newspack-plugin' ) }
|
|
105
105
|
</Button>
|
|
106
|
-
</
|
|
106
|
+
</HStack>
|
|
107
107
|
</Modal>
|
|
108
108
|
);
|
|
109
109
|
};
|
|
@@ -203,41 +203,20 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
|
|
|
203
203
|
if ( complete ) {
|
|
204
204
|
return <Redirect from="/plugin-requirements" to="/" />;
|
|
205
205
|
}
|
|
206
|
+
const headerText = requiredPlugins.length > 1 ? __( 'Required plugins', 'newspack-plugin' ) : __( 'Required plugin', 'newspack-plugin' );
|
|
207
|
+
const content = (
|
|
208
|
+
<div className="newspack-wizard newspack-wizard__content">
|
|
209
|
+
<PluginInstaller plugins={ requiredPlugins } onStatus={ status => this.pluginInstallationStatus( status ) } />
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
206
212
|
return (
|
|
207
213
|
<Route
|
|
208
214
|
path="/"
|
|
209
|
-
render={ () =>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<div className="newspack-wizard__title">
|
|
215
|
-
<Button
|
|
216
|
-
isLink
|
|
217
|
-
href={ newspack_urls.dashboard }
|
|
218
|
-
label={ __( 'Return to Dashboard', 'newspack-plugin' ) }
|
|
219
|
-
showTooltip={ true }
|
|
220
|
-
icon={ category }
|
|
221
|
-
iconSize={ 36 }
|
|
222
|
-
>
|
|
223
|
-
<NewspackIcon size={ 36 } />
|
|
224
|
-
</Button>
|
|
225
|
-
<div>
|
|
226
|
-
<h2>
|
|
227
|
-
{ requiredPlugins.length > 1
|
|
228
|
-
? __( 'Required plugins', 'newspack-plugin' )
|
|
229
|
-
: __( 'Required plugin', 'newspack-plugin' ) }
|
|
230
|
-
</h2>
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
) }
|
|
236
|
-
<div className="newspack-wizard newspack-wizard__content">
|
|
237
|
-
<PluginInstaller plugins={ requiredPlugins } onStatus={ status => this.pluginInstallationStatus( status ) } />
|
|
238
|
-
</div>
|
|
239
|
-
</Fragment>
|
|
240
|
-
) }
|
|
215
|
+
render={ () =>
|
|
216
|
+
// While `complete` is still null the requirements are being
|
|
217
|
+
// determined; render content without a header (matches prior behavior).
|
|
218
|
+
complete === null ? content : <Page breadcrumbItems={ [ { label: headerText } ] }>{ content }</Page>
|
|
219
|
+
}
|
|
241
220
|
/>
|
|
242
221
|
);
|
|
243
222
|
};
|
|
@@ -256,7 +235,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
|
|
|
256
235
|
confirmAction = options => {
|
|
257
236
|
const modalOptions = {
|
|
258
237
|
title: null,
|
|
259
|
-
message: __( 'Are you sure?', '
|
|
238
|
+
message: __( 'Are you sure?', 'newspack-plugin' ),
|
|
260
239
|
confirmText: __( 'OK', 'newspack-plugin' ),
|
|
261
240
|
cancelText: __( 'Cancel', 'newspack-plugin' ),
|
|
262
241
|
callback: null,
|
|
@@ -281,7 +260,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
|
|
|
281
260
|
callback && (
|
|
282
261
|
<Modal size="small" hideTitle={ ! title } title={ title } onRequestClose={ () => this.setState( { confirmation: null } ) }>
|
|
283
262
|
<p>{ message }</p>
|
|
284
|
-
<
|
|
263
|
+
<HStack justify="flex-end" spacing={ 4 } wrap className="newspack-modal__footer">
|
|
285
264
|
<Button variant="secondary" onClick={ () => this.setState( { confirmation: null } ) }>
|
|
286
265
|
{ cancelText }
|
|
287
266
|
</Button>
|
|
@@ -294,7 +273,7 @@ export default function withWizard( WrappedComponent, requiredPlugins ) {
|
|
|
294
273
|
>
|
|
295
274
|
{ confirmText }
|
|
296
275
|
</Button>
|
|
297
|
-
</
|
|
276
|
+
</HStack>
|
|
298
277
|
</Modal>
|
|
299
278
|
)
|
|
300
279
|
);
|
|
@@ -1,25 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WordPress dependencies
|
|
2
|
+
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { category } from '@wordpress/icons';
|
|
4
|
+
import { cloneElement } from '@wordpress/element';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Internal dependencies
|
|
9
8
|
*/
|
|
10
|
-
import { Button, Handoff,
|
|
9
|
+
import { Button, Handoff, Notice, HandoffMessage, TabbedNavigation, Page } from '../';
|
|
10
|
+
import { activeBreadcrumbs } from '../wizard/breadcrumbs-select';
|
|
11
11
|
import { buttonProps } from '../button-props';
|
|
12
|
+
import Router from '../proxied-imports/router';
|
|
12
13
|
import './style.scss';
|
|
13
14
|
|
|
15
|
+
const { useLocation } = Router;
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* External dependencies
|
|
16
19
|
*/
|
|
17
20
|
import classnames from 'classnames';
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Derives the active-tab breadcrumb trail from the current route. Only rendered
|
|
24
|
+
* for tabbed wizards, which always mount inside a Router, so calling useLocation
|
|
25
|
+
* here keeps router-free consumers (e.g. standalone multibranded) from crashing.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} props Component props.
|
|
28
|
+
* @param {Array} props.sections Tabbed navigation sections.
|
|
29
|
+
* @param {Function} props.render Renders the page given the breadcrumb trail.
|
|
30
|
+
* @return {JSX.Element} The rendered page.
|
|
31
|
+
*/
|
|
32
|
+
const RouteBreadcrumbs = ( { sections, render } ) => {
|
|
33
|
+
const { pathname } = useLocation();
|
|
34
|
+
return render( activeBreadcrumbs( sections, pathname ) );
|
|
35
|
+
};
|
|
36
|
+
|
|
19
37
|
/**
|
|
20
38
|
* Higher-Order Component to provide plugin management and error handling to Newspack Wizards.
|
|
21
39
|
*/
|
|
22
|
-
export default function withWizardScreen( WrappedComponent, { hidePrimaryButton } = {} ) {
|
|
40
|
+
export default function withWizardScreen( WrappedComponent, { hidePrimaryButton, hideHeader } = {} ) {
|
|
23
41
|
const WrappedWithWizardScreen = props => {
|
|
24
42
|
const {
|
|
25
43
|
className,
|
|
@@ -29,6 +47,8 @@ export default function withWizardScreen( WrappedComponent, { hidePrimaryButton
|
|
|
29
47
|
headerText,
|
|
30
48
|
subHeaderText,
|
|
31
49
|
tabbedNavigation,
|
|
50
|
+
breadcrumbItems,
|
|
51
|
+
headerActions,
|
|
32
52
|
secondaryButtonText,
|
|
33
53
|
secondaryButtonAction,
|
|
34
54
|
renderAboveContent,
|
|
@@ -57,37 +77,15 @@ export default function withWizardScreen( WrappedComponent, { hidePrimaryButton
|
|
|
57
77
|
{ ...overridingProps }
|
|
58
78
|
/>
|
|
59
79
|
);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<Button
|
|
67
|
-
isLink
|
|
68
|
-
href={ newspack_urls.dashboard }
|
|
69
|
-
label={ __( 'Return to Dashboard', 'newspack-plugin' ) }
|
|
70
|
-
showTooltip={ true }
|
|
71
|
-
icon={ category }
|
|
72
|
-
iconSize={ 36 }
|
|
73
|
-
>
|
|
74
|
-
<NewspackIcon size={ 36 } />
|
|
75
|
-
</Button>
|
|
76
|
-
<div>
|
|
77
|
-
{ headerText && <h2>{ headerText }</h2> }
|
|
78
|
-
{ subHeaderText && <span>{ subHeaderText }</span> }
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
|
|
84
|
-
{ tabbedNavigation && (
|
|
85
|
-
<TabbedNavigation
|
|
86
|
-
disableUpcoming={ disableUpcomingInTabbedNavigation }
|
|
87
|
-
items={ tabbedNavigation.filter( item => ! item.isHiddenInNav ) }
|
|
88
|
-
/>
|
|
89
|
-
) }
|
|
80
|
+
const tabbedNavigationRegion = tabbedNavigation && (
|
|
81
|
+
<TabbedNavigation
|
|
82
|
+
disableUpcoming={ disableUpcomingInTabbedNavigation }
|
|
83
|
+
items={ tabbedNavigation.filter( item => ! item.isHiddenInNav ) }
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
90
86
|
|
|
87
|
+
const content = (
|
|
88
|
+
<>
|
|
91
89
|
<HandoffMessage />
|
|
92
90
|
|
|
93
91
|
<div className={ classnames( 'newspack-wizard newspack-wizard__content', className ) }>
|
|
@@ -106,6 +104,43 @@ export default function withWizardScreen( WrappedComponent, { hidePrimaryButton
|
|
|
106
104
|
</div>
|
|
107
105
|
</>
|
|
108
106
|
);
|
|
107
|
+
|
|
108
|
+
const renderPage = crumbs => {
|
|
109
|
+
let pageBreadcrumbs = crumbs ?? [];
|
|
110
|
+
if ( ! pageBreadcrumbs.length && headerText ) {
|
|
111
|
+
pageBreadcrumbs = [ { label: headerText } ];
|
|
112
|
+
}
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
{ newspack_aux_data.is_debug_mode && <Notice debugMode /> }
|
|
116
|
+
{ hideHeader ? (
|
|
117
|
+
// Without the Page shell the tabs still own the content: it
|
|
118
|
+
// renders inside the active tab's panel.
|
|
119
|
+
<>{ tabbedNavigationRegion ? cloneElement( tabbedNavigationRegion, { content } ) : content }</>
|
|
120
|
+
) : (
|
|
121
|
+
<Page
|
|
122
|
+
breadcrumbItems={ pageBreadcrumbs }
|
|
123
|
+
subTitle={ subHeaderText }
|
|
124
|
+
actions={ headerActions }
|
|
125
|
+
tabbedNavigation={ tabbedNavigationRegion }
|
|
126
|
+
>
|
|
127
|
+
{ content }
|
|
128
|
+
</Page>
|
|
129
|
+
) }
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if ( hideHeader ) {
|
|
135
|
+
return renderPage();
|
|
136
|
+
}
|
|
137
|
+
if ( breadcrumbItems ) {
|
|
138
|
+
return renderPage( breadcrumbItems );
|
|
139
|
+
}
|
|
140
|
+
if ( tabbedNavigation ) {
|
|
141
|
+
return <RouteBreadcrumbs sections={ tabbedNavigation } render={ renderPage } />;
|
|
142
|
+
}
|
|
143
|
+
return renderPage();
|
|
109
144
|
};
|
|
110
145
|
return WrappedWithWizardScreen;
|
|
111
146
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { HashRouter } from 'react-router-dom';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Internal dependencies.
|
|
9
|
+
*/
|
|
10
|
+
import withWizardScreen from './';
|
|
11
|
+
|
|
12
|
+
global.newspack_aux_data = { is_debug_mode: false };
|
|
13
|
+
|
|
14
|
+
describe( 'withWizardScreen', () => {
|
|
15
|
+
it( 'falls back to a single headerText breadcrumb when no breadcrumbItems are provided', () => {
|
|
16
|
+
const WrappedComponent = () => <div>Body content</div>;
|
|
17
|
+
const ScreenWithHeader = withWizardScreen( WrappedComponent );
|
|
18
|
+
|
|
19
|
+
render(
|
|
20
|
+
<HashRouter>
|
|
21
|
+
<ScreenWithHeader headerText="Brands" />
|
|
22
|
+
</HashRouter>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect( screen.getByRole( 'heading', { level: 1 } ) ).toHaveTextContent( 'Brands' );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'renders without a Router when no tabbed navigation is provided', () => {
|
|
29
|
+
const WrappedComponent = () => <div>Body content</div>;
|
|
30
|
+
const ScreenWithHeader = withWizardScreen( WrappedComponent );
|
|
31
|
+
|
|
32
|
+
render( <ScreenWithHeader headerText="Brands" /> );
|
|
33
|
+
|
|
34
|
+
expect( screen.getByRole( 'heading', { level: 1 } ) ).toHaveTextContent( 'Brands' );
|
|
35
|
+
} );
|
|
36
|
+
} );
|
|
@@ -45,129 +45,12 @@
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
&__header {
|
|
49
|
-
align-items: center;
|
|
50
|
-
background: white;
|
|
51
|
-
border-bottom: 1px solid wp-colors.$gray-300;
|
|
52
|
-
display: flex;
|
|
53
|
-
.newspack-wizard__fixed-header & {
|
|
54
|
-
position: sticky;
|
|
55
|
-
top: 0;
|
|
56
|
-
width: 100%;
|
|
57
|
-
z-index: 1000;
|
|
58
|
-
.admin-bar & {
|
|
59
|
-
@media only screen and ( min-width: 601px ) {
|
|
60
|
-
top: 46px;
|
|
61
|
-
}
|
|
62
|
-
@media only screen and ( min-width: 782px ) {
|
|
63
|
-
top: 32px;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
&__inner {
|
|
68
|
-
flex: 1 1 100%;
|
|
69
|
-
}
|
|
70
|
-
&__actions {
|
|
71
|
-
align-items: center;
|
|
72
|
-
display: flex;
|
|
73
|
-
gap: 8px;
|
|
74
|
-
justify-content: flex-end;
|
|
75
|
-
padding: 0 24px 0 0;
|
|
76
|
-
> .newspack-wizard__header__actions__main {
|
|
77
|
-
display: none;
|
|
78
|
-
}
|
|
79
|
-
@media only screen and ( min-width: 782px ) {
|
|
80
|
-
flex: 1 1 auto;
|
|
81
|
-
> .newspack-wizard__header__actions__main {
|
|
82
|
-
display: flex;
|
|
83
|
-
}
|
|
84
|
-
&__more {
|
|
85
|
-
&--primary-only,
|
|
86
|
-
.newspack-wizard__header__actions__more__main {
|
|
87
|
-
display: none;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
48
|
&__main {
|
|
95
|
-
overflow
|
|
49
|
+
overflow: hidden;
|
|
96
50
|
padding-bottom: 1px; /* Prevents borders from being cut off */
|
|
97
51
|
}
|
|
98
52
|
|
|
99
53
|
|
|
100
|
-
&__title {
|
|
101
|
-
align-items: center;
|
|
102
|
-
display: flex;
|
|
103
|
-
|
|
104
|
-
h2 {
|
|
105
|
-
margin: 0;
|
|
106
|
-
padding: 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
> div {
|
|
110
|
-
align-items: baseline;
|
|
111
|
-
display: flex;
|
|
112
|
-
padding: 0 24px;
|
|
113
|
-
|
|
114
|
-
> span {
|
|
115
|
-
color: wp-colors.$gray-700;
|
|
116
|
-
display: none;
|
|
117
|
-
margin-left: 8px;
|
|
118
|
-
|
|
119
|
-
@media screen and ( min-width: 744px ) {
|
|
120
|
-
display: block;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
svg {
|
|
126
|
-
fill: white;
|
|
127
|
-
margin: 0 !important;
|
|
128
|
-
|
|
129
|
-
&.newspack-icon {
|
|
130
|
-
flex: 0 0 36px;
|
|
131
|
-
margin-bottom: -1px !important;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
&:not(.newspack-icon) {
|
|
135
|
-
background: var(--wp-admin-theme-color);
|
|
136
|
-
left: 50%;
|
|
137
|
-
margin: -28px 0 0 -28px !important;
|
|
138
|
-
opacity: 0;
|
|
139
|
-
padding: 10px;
|
|
140
|
-
position: absolute;
|
|
141
|
-
top: 50%;
|
|
142
|
-
transition: opacity 125ms ease-in-out;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.components-button.has-icon {
|
|
147
|
-
padding: 0;
|
|
148
|
-
position: relative;
|
|
149
|
-
|
|
150
|
-
&:hover {
|
|
151
|
-
svg {
|
|
152
|
-
opacity: 1;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
&:focus-visible {
|
|
157
|
-
outline: none !important;
|
|
158
|
-
|
|
159
|
-
&::before {
|
|
160
|
-
border: 2px solid white;
|
|
161
|
-
border-radius: 2px;
|
|
162
|
-
content: "";
|
|
163
|
-
display: block;
|
|
164
|
-
inset: 6px 6px 5px;
|
|
165
|
-
position: absolute;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
54
|
&__section-header {
|
|
172
55
|
margin: 0 auto 32px;
|
|
173
56
|
max-width: calc(calc(var(--newspack-wizard-section-space) * 2) + var(--newspack-wizard-section-width));
|
|
@@ -182,6 +65,7 @@
|
|
|
182
65
|
|
|
183
66
|
&__content {
|
|
184
67
|
margin: 0 auto;
|
|
68
|
+
width: 100%;
|
|
185
69
|
max-width: calc(calc(var(--newspack-wizard-section-space) * 2) + var(--newspack-wizard-section-width));
|
|
186
70
|
padding: calc(var(--newspack-wizard-section-space) * 2) var(--newspack-wizard-section-space) 0;
|
|
187
71
|
|
|
@@ -225,3 +109,16 @@
|
|
|
225
109
|
margin: 0;
|
|
226
110
|
}
|
|
227
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wizard header overflow ("More") menu, rendered by the Wizard component into
|
|
115
|
+
* admin-ui's Page actions slot. Hide the toggle when there are no overflow
|
|
116
|
+
* actions, and hide the duplicated primary items inside the menu on wide
|
|
117
|
+
* viewports.
|
|
118
|
+
*/
|
|
119
|
+
@media only screen and ( min-width: 782px ) {
|
|
120
|
+
.newspack-wizard__actions__more--primary-only,
|
|
121
|
+
.newspack-wizard__actions__more .newspack-wizard__actions__more__main {
|
|
122
|
+
display: none;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { matchesRoute } from '../route-match';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Select the active section's explicit breadcrumb trail by current route. Falls
|
|
8
|
+
* back to the first section, then to an empty trail.
|
|
9
|
+
*
|
|
10
|
+
* @param {Array} sections Wizard sections (`{ path, breadcrumbs, exact?, activeTabPaths? }`).
|
|
11
|
+
* @param {string} pathname Current router pathname.
|
|
12
|
+
* @return {Array} Breadcrumb items `{ label, url? }`.
|
|
13
|
+
*/
|
|
14
|
+
export const activeBreadcrumbs = ( sections = [], pathname ) => {
|
|
15
|
+
if ( ! sections?.length ) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const match = sections.find( section => matchesRoute( section, pathname ) ) || sections[ 0 ];
|
|
19
|
+
return match.breadcrumbs || [];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Append a section's render-time current-page breadcrumb(s) to a trail.
|
|
24
|
+
*
|
|
25
|
+
* A section can supply a leaf via `headerData.sectionName` — either a single
|
|
26
|
+
* label (e.g. an integration name, or Add/Edit) or an ordered array of
|
|
27
|
+
* `{ label, url? }` crumbs when the leaf needs its own linked ancestors. Each
|
|
28
|
+
* crumb is appended in turn.
|
|
29
|
+
*
|
|
30
|
+
* Convention: a route authors its current-page leaf in exactly ONE place —
|
|
31
|
+
* the static `breadcrumbs` array for a constant label, or `sectionName` for a
|
|
32
|
+
* render-time label (e.g. Add/Edit) whose static trail then holds ancestors
|
|
33
|
+
* only. Never both. The trailing-label skip below is a defensive guard against
|
|
34
|
+
* a route accidentally doing both; it is not a supported pattern to rely on.
|
|
35
|
+
*
|
|
36
|
+
* @param {Array} breadcrumbItems Base trail `{ label, url? }[]`.
|
|
37
|
+
* @param {string|Array|undefined} sectionName A label, an array of crumbs, or falsy.
|
|
38
|
+
* @return {Array} A new trail with the section crumb(s) appended (deduped).
|
|
39
|
+
*/
|
|
40
|
+
export const appendSectionName = ( breadcrumbItems = [], sectionName ) => {
|
|
41
|
+
if ( ! sectionName ) {
|
|
42
|
+
return breadcrumbItems;
|
|
43
|
+
}
|
|
44
|
+
const extraCrumbs = ( Array.isArray( sectionName ) ? sectionName : [ { label: sectionName } ] ).filter( crumb => crumb?.label );
|
|
45
|
+
return extraCrumbs.reduce( ( trail, crumb ) => {
|
|
46
|
+
if ( trail[ trail.length - 1 ]?.label === crumb.label ) {
|
|
47
|
+
return trail;
|
|
48
|
+
}
|
|
49
|
+
return [ ...trail, crumb ];
|
|
50
|
+
}, breadcrumbItems );
|
|
51
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies.
|
|
3
|
+
*/
|
|
4
|
+
import { activeBreadcrumbs, appendSectionName } from './breadcrumbs-select';
|
|
5
|
+
|
|
6
|
+
const SECTIONS = [
|
|
7
|
+
{ path: '/', breadcrumbs: [ { label: 'Audience Management' }, { label: 'Configuration' } ] },
|
|
8
|
+
{ path: '/donations', breadcrumbs: [ { label: 'Audience Management' }, { label: 'Donations' } ] },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe( 'activeBreadcrumbs', () => {
|
|
12
|
+
it( 'returns the matching section breadcrumbs by exact path', () => {
|
|
13
|
+
expect( activeBreadcrumbs( SECTIONS, '/donations' ) ).toEqual( [ { label: 'Audience Management' }, { label: 'Donations' } ] );
|
|
14
|
+
} );
|
|
15
|
+
|
|
16
|
+
it( 'falls back to the first section when no path matches', () => {
|
|
17
|
+
expect( activeBreadcrumbs( SECTIONS, '/nope' ) ).toEqual( SECTIONS[ 0 ].breadcrumbs );
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'returns [] when sections is empty', () => {
|
|
21
|
+
expect( activeBreadcrumbs( [], '/x' ) ).toEqual( [] );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'returns [] when sections is null or undefined', () => {
|
|
25
|
+
expect( activeBreadcrumbs( null, '/x' ) ).toEqual( [] );
|
|
26
|
+
expect( activeBreadcrumbs( undefined, '/x' ) ).toEqual( [] );
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
it( 'matches a non-exact section by path prefix', () => {
|
|
30
|
+
const sections = [
|
|
31
|
+
{ path: '/', breadcrumbs: [ { label: 'Root' } ] },
|
|
32
|
+
{ path: '/access', exact: false, breadcrumbs: [ { label: 'Root' }, { label: 'Access control' } ] },
|
|
33
|
+
];
|
|
34
|
+
expect( activeBreadcrumbs( sections, '/access/institutions' ) ).toEqual( [ { label: 'Root' }, { label: 'Access control' } ] );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'matches a parameterized route to its own breadcrumbs', () => {
|
|
38
|
+
const sections = [
|
|
39
|
+
{ path: '/content-gates', breadcrumbs: [ { label: 'Access control' } ] },
|
|
40
|
+
{
|
|
41
|
+
path: '/institutions/:id',
|
|
42
|
+
exact: true,
|
|
43
|
+
breadcrumbs: [ { label: 'Access control' }, { label: 'Edit institution' } ],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
expect( activeBreadcrumbs( sections, '/institutions/123' ) ).toEqual( [ { label: 'Access control' }, { label: 'Edit institution' } ] );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
it( 'matches via activeTabPaths wildcard', () => {
|
|
50
|
+
const sections = [
|
|
51
|
+
{ path: '/', breadcrumbs: [ { label: 'Root' } ] },
|
|
52
|
+
{
|
|
53
|
+
path: '/segments',
|
|
54
|
+
activeTabPaths: [ '/segments/*' ],
|
|
55
|
+
breadcrumbs: [ { label: 'Root' }, { label: 'Segments' } ],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
expect( activeBreadcrumbs( sections, '/segments/123' ) ).toEqual( [ { label: 'Root' }, { label: 'Segments' } ] );
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'prefers an exact path match over the first section', () => {
|
|
62
|
+
expect( activeBreadcrumbs( SECTIONS, '/' ) ).toEqual( SECTIONS[ 0 ].breadcrumbs );
|
|
63
|
+
} );
|
|
64
|
+
|
|
65
|
+
it( 'returns [] when the matched section has no breadcrumbs', () => {
|
|
66
|
+
expect( activeBreadcrumbs( [ { path: '/' } ], '/' ) ).toEqual( [] );
|
|
67
|
+
} );
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
describe( 'appendSectionName', () => {
|
|
71
|
+
const base = [ { label: 'Access Control' } ];
|
|
72
|
+
|
|
73
|
+
it( 'returns the base trail unchanged when sectionName is falsy', () => {
|
|
74
|
+
expect( appendSectionName( base, undefined ) ).toBe( base );
|
|
75
|
+
expect( appendSectionName( base, '' ) ).toBe( base );
|
|
76
|
+
expect( appendSectionName( base, null ) ).toBe( base );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
it( 'appends a string sectionName as a leaf crumb', () => {
|
|
80
|
+
expect( appendSectionName( base, 'Content Gifting' ) ).toEqual( [ { label: 'Access Control' }, { label: 'Content Gifting' } ] );
|
|
81
|
+
} );
|
|
82
|
+
|
|
83
|
+
it( 'appends an array of crumbs in order', () => {
|
|
84
|
+
const sectionName = [ { label: 'Mailchimp', url: '/mailchimp' }, { label: 'Logs' } ];
|
|
85
|
+
expect( appendSectionName( base, sectionName ) ).toEqual( [
|
|
86
|
+
{ label: 'Access Control' },
|
|
87
|
+
{ label: 'Mailchimp', url: '/mailchimp' },
|
|
88
|
+
{ label: 'Logs' },
|
|
89
|
+
] );
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
it( 'dedupes a string leaf that repeats the current trailing label', () => {
|
|
93
|
+
const trail = [ { label: 'Access Control' }, { label: 'Metered Countdown' } ];
|
|
94
|
+
expect( appendSectionName( trail, 'Metered Countdown' ) ).toEqual( trail );
|
|
95
|
+
} );
|
|
96
|
+
|
|
97
|
+
it( 'dedupes an array leaf that repeats the current trailing label', () => {
|
|
98
|
+
const trail = [ { label: 'Access Control' }, { label: 'Metered Countdown' } ];
|
|
99
|
+
expect( appendSectionName( trail, [ { label: 'Metered Countdown' } ] ) ).toEqual( trail );
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'skips crumbs without a label', () => {
|
|
103
|
+
expect( appendSectionName( base, [ { url: '/no-label' }, { label: 'Edit' } ] ) ).toEqual( [
|
|
104
|
+
{ label: 'Access Control' },
|
|
105
|
+
{ label: 'Edit' },
|
|
106
|
+
] );
|
|
107
|
+
} );
|
|
108
|
+
|
|
109
|
+
it( 'only dedupes against the immediate predecessor, not earlier crumbs', () => {
|
|
110
|
+
const trail = [ { label: 'Access Control' }, { label: 'Institutions' } ];
|
|
111
|
+
// 'Access Control' repeats an earlier (non-adjacent) crumb, so it is kept.
|
|
112
|
+
expect( appendSectionName( trail, [ { label: 'Access Control' } ] ) ).toEqual( [
|
|
113
|
+
{ label: 'Access Control' },
|
|
114
|
+
{ label: 'Institutions' },
|
|
115
|
+
{ label: 'Access Control' },
|
|
116
|
+
] );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'does not mutate the input trail', () => {
|
|
120
|
+
const trail = [ { label: 'Access Control' } ];
|
|
121
|
+
appendSectionName( trail, 'Content Gifting' );
|
|
122
|
+
expect( trail ).toEqual( [ { label: 'Access Control' } ] );
|
|
123
|
+
} );
|
|
124
|
+
} );
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { __ } from '@wordpress/i18n';
|
|
5
5
|
import { useSelect } from '@wordpress/data';
|
|
6
|
+
import { __experimentalHStack as HStack } from '@wordpress/components'; // eslint-disable-line @wordpress/no-unsafe-wp-apis
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Internal dependencies.
|
|
9
10
|
*/
|
|
10
|
-
import { Button,
|
|
11
|
+
import { Button, Modal, Notice } from '../..';
|
|
11
12
|
import { WIZARD_STORE_NAMESPACE } from '../store';
|
|
12
13
|
|
|
13
14
|
const parseError = ( { data, message, code } ) => {
|
|
@@ -36,11 +37,11 @@ const WizardError = () => {
|
|
|
36
37
|
<Modal title={ __( 'Unrecoverable error' ) } onRequestClose={ fallbackURL ? () => ( window.location = fallbackURL ) : undefined }>
|
|
37
38
|
<Notice noticeText={ message } isError rawHTML />
|
|
38
39
|
{ fallbackURL && (
|
|
39
|
-
<
|
|
40
|
+
<HStack justify="flex-end" spacing={ 4 } wrap className="newspack-modal__footer">
|
|
40
41
|
<Button isPrimary href={ fallbackURL }>
|
|
41
42
|
{ __( 'Return to Dashboard', 'newspack-plugin' ) }
|
|
42
43
|
</Button>
|
|
43
|
-
</
|
|
44
|
+
</HStack>
|
|
44
45
|
) }
|
|
45
46
|
</Modal>
|
|
46
47
|
);
|