newspack-components 4.3.0 → 4.4.0-epic-editor-refactor.1
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 +11 -0
- package/DEVELOPMENT.md +868 -0
- package/copy-styles.js +3 -2
- package/dist/cjs/accordion/index.js +17 -12
- package/dist/cjs/action-card/index.js +176 -139
- package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
- package/dist/cjs/autocomplete-with-latest-posts/index.js +172 -110
- package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
- package/dist/cjs/badge/index.js +7 -7
- package/dist/cjs/box-contrast/index.js +19 -15
- package/dist/cjs/button/index.js +41 -26
- package/dist/cjs/button-props/button-props.js +5 -5
- package/dist/cjs/button-props/index.js +2 -2
- package/dist/cjs/card/core-card.js +110 -52
- package/dist/cjs/card/index.js +74 -67
- package/dist/cjs/card/style-core.scss +93 -25
- package/dist/cjs/card/style.scss +2 -2
- package/dist/cjs/card-feature/index.js +141 -0
- package/dist/cjs/card-feature/style.scss +75 -0
- package/dist/cjs/card-form/index.js +144 -0
- package/dist/cjs/card-form/style.scss +20 -0
- package/dist/cjs/card-settings-group/index.js +36 -16
- package/dist/cjs/card-sortable-list/index.js +176 -140
- package/dist/cjs/category-autocomplete/index.js +175 -155
- package/dist/cjs/color-picker/index.js +41 -23
- package/dist/cjs/color-picker/index.test.js +82 -0
- package/dist/cjs/color-picker/style.scss +11 -13
- package/dist/cjs/confirm-dialog/index.js +86 -22
- package/dist/cjs/consts.js +1 -1
- package/dist/cjs/custom-select-control/index.js +39 -28
- package/dist/cjs/dataviews/index.js +38 -0
- package/dist/cjs/dataviews/style.scss +9 -0
- package/dist/cjs/divider/index.js +26 -21
- package/dist/cjs/footer/index.js +36 -30
- package/dist/cjs/form-token-field/index.js +51 -40
- package/dist/cjs/global-notices/index.js +5 -5
- package/dist/cjs/grid/index.js +25 -19
- package/dist/cjs/grid/style.scss +6 -1
- package/dist/cjs/handoff/index.js +204 -156
- package/dist/cjs/handoff-message/index.js +12 -7
- package/dist/cjs/hooks/index.js +10 -8
- package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
- package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
- package/dist/cjs/hooks/useObjectState.js +21 -12
- package/dist/cjs/hooks/useObjectState.test.js +55 -33
- package/dist/cjs/hooks/useOnClickOutside.js +6 -6
- package/dist/cjs/hooks/usePrompt.js +10 -13
- package/dist/cjs/image-upload/index.js +114 -123
- package/dist/cjs/image-upload/index.test.js +11 -13
- package/dist/cjs/image-upload/style.scss +12 -40
- package/dist/cjs/index.js +136 -104
- package/dist/cjs/info-button/index.js +43 -32
- package/dist/cjs/integration-icons/active-campaign.js +22 -0
- package/dist/cjs/integration-icons/constant-contact.js +28 -0
- package/dist/cjs/integration-icons/fundraise-up.js +25 -0
- package/dist/cjs/integration-icons/index.js +48 -0
- package/dist/cjs/integration-icons/mailchimp.js +19 -0
- package/dist/cjs/integration-icons/salesforce.js +17 -0
- package/dist/cjs/integration-icons/wisepops.js +20 -0
- package/dist/cjs/modal/index.js +22 -19
- package/dist/cjs/modal/style.scss +4 -0
- package/dist/cjs/newspack-icon/index.js +45 -38
- package/dist/cjs/notice/index.js +64 -55
- package/dist/cjs/plugin-installer/index.js +259 -253
- package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
- package/dist/cjs/plugin-settings/index.js +217 -200
- package/dist/cjs/plugin-toggle/index.js +192 -199
- package/dist/cjs/popover/index.js +41 -28
- package/dist/cjs/position-control/index.js +24 -23
- package/dist/cjs/progress-bar/index.js +71 -59
- package/dist/cjs/progress-bar/index.test.js +62 -68
- package/dist/cjs/proxied-imports/router.js +3 -3
- package/dist/cjs/radio-control/index.js +40 -27
- package/dist/cjs/section-header/index.js +103 -58
- package/dist/cjs/section-header/style.scss +31 -11
- package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
- package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
- package/dist/cjs/select-control/index.js +52 -40
- package/dist/cjs/settings/MinMaxSetting.js +30 -20
- package/dist/cjs/settings/SettingsCard.js +20 -17
- package/dist/cjs/settings/SettingsSection.js +22 -21
- package/dist/cjs/settings/index.js +5 -5
- package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
- package/dist/cjs/steps-list/index.js +44 -34
- package/dist/cjs/steps-list-item/index.js +47 -39
- package/dist/cjs/style-card/index.js +70 -63
- package/dist/cjs/tabbed-navigation/index.js +38 -36
- package/dist/cjs/text-control/index.js +27 -25
- package/dist/cjs/utils/color.js +4 -4
- package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
- package/dist/cjs/utils/index.js +31 -19
- package/dist/cjs/utils/style.scss +8 -0
- package/dist/cjs/waiting/index.js +48 -37
- package/dist/cjs/web-preview/index.js +217 -211
- package/dist/cjs/with-wizard/index.js +362 -364
- package/dist/cjs/with-wizard/style.scss +4 -54
- package/dist/cjs/with-wizard-screen/index.js +54 -58
- package/dist/cjs/with-wizard-screen/style.scss +41 -3
- package/dist/cjs/wizard/components/WizardError.js +20 -18
- package/dist/cjs/wizard/components/WizardSnackbar.js +26 -23
- package/dist/cjs/wizard/index.js +176 -114
- package/dist/cjs/wizard/store/index.js +171 -123
- package/dist/cjs/wizard/store/utils.js +13 -8
- package/dist/esm/accordion/index.js +14 -9
- package/dist/esm/action-card/index.js +168 -123
- package/dist/esm/autocomplete-tokenfield/index.js +248 -222
- package/dist/esm/autocomplete-with-latest-posts/index.js +169 -103
- package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
- package/dist/esm/badge/index.js +5 -5
- package/dist/esm/box-contrast/index.js +15 -11
- package/dist/esm/button/index.js +39 -21
- package/dist/esm/button-props/button-props.js +1 -1
- package/dist/esm/card/core-card.js +109 -47
- package/dist/esm/card/index.js +69 -54
- package/dist/esm/card/style-core.scss +93 -25
- package/dist/esm/card/style.scss +2 -2
- package/dist/esm/card-feature/index.js +131 -0
- package/dist/esm/card-feature/style.scss +75 -0
- package/dist/esm/card-form/index.js +134 -0
- package/dist/esm/card-form/style.scss +20 -0
- package/dist/esm/card-settings-group/index.js +33 -14
- package/dist/esm/card-sortable-list/index.js +170 -126
- package/dist/esm/category-autocomplete/index.js +168 -140
- package/dist/esm/color-picker/index.js +39 -21
- package/dist/esm/color-picker/index.test.js +78 -0
- package/dist/esm/color-picker/style.scss +11 -13
- package/dist/esm/confirm-dialog/index.js +85 -18
- package/dist/esm/consts.js +1 -1
- package/dist/esm/custom-select-control/index.js +34 -15
- package/dist/esm/dataviews/index.js +34 -0
- package/dist/esm/dataviews/style.scss +9 -0
- package/dist/esm/divider/index.js +24 -16
- package/dist/esm/footer/index.js +34 -28
- package/dist/esm/form-token-field/index.js +46 -27
- package/dist/esm/global-notices/index.js +3 -3
- package/dist/esm/grid/index.js +23 -14
- package/dist/esm/grid/style.scss +6 -1
- package/dist/esm/handoff/index.js +199 -143
- package/dist/esm/handoff-message/index.js +10 -6
- package/dist/esm/hooks/index.js +8 -6
- package/dist/esm/hooks/use-confirm-dialog.js +73 -0
- package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
- package/dist/esm/hooks/useObjectState.js +19 -9
- package/dist/esm/hooks/useObjectState.test.js +54 -32
- package/dist/esm/hooks/useOnClickOutside.js +4 -4
- package/dist/esm/hooks/usePrompt.js +9 -11
- package/dist/esm/image-upload/index.js +110 -111
- package/dist/esm/image-upload/index.test.js +11 -13
- package/dist/esm/image-upload/style.scss +12 -40
- package/dist/esm/index.js +7 -1
- package/dist/esm/info-button/index.js +38 -19
- package/dist/esm/integration-icons/active-campaign.js +16 -0
- package/dist/esm/integration-icons/constant-contact.js +22 -0
- package/dist/esm/integration-icons/fundraise-up.js +19 -0
- package/dist/esm/integration-icons/index.js +6 -0
- package/dist/esm/integration-icons/mailchimp.js +13 -0
- package/dist/esm/integration-icons/salesforce.js +11 -0
- package/dist/esm/integration-icons/wisepops.js +14 -0
- package/dist/esm/modal/index.js +20 -13
- package/dist/esm/modal/style.scss +4 -0
- package/dist/esm/newspack-icon/index.js +40 -25
- package/dist/esm/notice/index.js +59 -42
- package/dist/esm/plugin-installer/index.js +255 -243
- package/dist/esm/plugin-settings/SettingsSection.js +61 -50
- package/dist/esm/plugin-settings/index.js +212 -189
- package/dist/esm/plugin-toggle/index.js +187 -190
- package/dist/esm/popover/index.js +35 -16
- package/dist/esm/position-control/index.js +22 -18
- package/dist/esm/progress-bar/index.js +66 -49
- package/dist/esm/progress-bar/index.test.js +61 -67
- package/dist/esm/radio-control/index.js +34 -15
- package/dist/esm/section-header/index.js +102 -57
- package/dist/esm/section-header/style.scss +31 -11
- package/dist/esm/select-control/ButtonGroupControl.js +23 -18
- package/dist/esm/select-control/GroupedSelectControl.js +38 -30
- package/dist/esm/select-control/index.js +47 -27
- package/dist/esm/settings/MinMaxSetting.js +27 -16
- package/dist/esm/settings/SettingsCard.js +18 -13
- package/dist/esm/settings/SettingsSection.js +20 -19
- package/dist/esm/settings/index.js +3 -3
- package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
- package/dist/esm/steps-list/index.js +39 -21
- package/dist/esm/steps-list-item/index.js +42 -26
- package/dist/esm/style-card/index.js +65 -50
- package/dist/esm/tabbed-navigation/index.js +35 -33
- package/dist/esm/text-control/index.js +25 -19
- package/dist/esm/utils/color.js +4 -4
- package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
- package/dist/esm/utils/index.js +29 -16
- package/dist/esm/utils/style.scss +8 -0
- package/dist/esm/waiting/index.js +43 -24
- package/dist/esm/web-preview/index.js +213 -201
- package/dist/esm/with-wizard/index.js +360 -358
- package/dist/esm/with-wizard/style.scss +4 -54
- package/dist/esm/with-wizard-screen/index.js +48 -46
- package/dist/esm/with-wizard-screen/style.scss +41 -3
- package/dist/esm/wizard/components/WizardError.js +18 -16
- package/dist/esm/wizard/components/WizardSnackbar.js +23 -17
- package/dist/esm/wizard/index.js +171 -103
- package/dist/esm/wizard/store/index.js +169 -116
- package/dist/esm/wizard/store/utils.js +13 -6
- package/package.json +30 -16
- package/release.config.js +1 -0
- package/shared/hooks/use-coauthors.js +195 -27
- package/shared/hooks/use-coauthors.test.js +232 -6
- package/shared/scss/_mixins.scss +13 -5
- package/src/accordion/index.js +8 -7
- package/src/action-card/index.js +15 -1
- package/src/badge/index.tsx +3 -1
- package/src/card/core-card.js +95 -18
- package/src/card/index.js +1 -0
- package/src/card/style-core.scss +93 -25
- package/src/card/style.scss +2 -2
- package/src/card-feature/README.md +188 -0
- package/src/card-feature/index.tsx +186 -0
- package/src/card-feature/style.scss +75 -0
- package/src/card-form/README.md +113 -0
- package/src/card-form/index.tsx +128 -0
- package/src/card-form/style.scss +20 -0
- package/src/card-settings-group/index.tsx +28 -3
- package/src/card-sortable-list/index.tsx +27 -4
- package/src/color-picker/index.js +15 -8
- package/src/color-picker/index.test.js +52 -0
- package/src/color-picker/style.scss +11 -13
- package/src/confirm-dialog/README.md +128 -0
- package/src/confirm-dialog/index.tsx +74 -6
- package/src/dataviews/index.tsx +34 -0
- package/src/dataviews/style.scss +9 -0
- package/src/grid/style.scss +6 -1
- package/src/handoff/README.md +93 -0
- package/src/handoff/index.js +46 -8
- package/src/hooks/index.ts +2 -1
- package/src/hooks/use-confirm-dialog.tsx +80 -0
- package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
- package/src/image-upload/index.js +10 -15
- package/src/image-upload/style.scss +12 -40
- package/src/index.js +7 -1
- package/src/integration-icons/active-campaign.js +14 -0
- package/src/integration-icons/constant-contact.js +22 -0
- package/src/integration-icons/fundraise-up.js +18 -0
- package/src/integration-icons/index.js +6 -0
- package/src/integration-icons/mailchimp.js +10 -0
- package/src/integration-icons/salesforce.js +10 -0
- package/src/integration-icons/wisepops.js +14 -0
- package/src/modal/style.scss +4 -0
- package/src/plugin-installer/index.js +30 -19
- package/src/section-header/index.js +56 -24
- package/src/section-header/style.scss +31 -11
- package/src/select-control/index.js +4 -0
- package/src/utils/editor-toolbar-back-button.tsx +48 -0
- package/src/utils/index.js +6 -0
- package/src/utils/style.scss +8 -0
- package/src/with-wizard/style.scss +4 -54
- package/src/with-wizard-screen/style.scss +41 -3
- package/src/wizard/index.js +90 -53
- package/src/wizard/store/index.js +2 -2
- package/dist/cjs/button-card/index.js +0 -72
- package/dist/cjs/button-card/style.scss +0 -180
- package/dist/esm/button-card/index.js +0 -64
- package/dist/esm/button-card/style.scss +0 -180
- package/src/button-card/index.js +0 -52
- package/src/button-card/style.scss +0 -180
|
@@ -6,11 +6,11 @@ import { useState, useEffect, useMemo } from '@wordpress/element';
|
|
|
6
6
|
import { store as coreStore } from '@wordpress/core-data';
|
|
7
7
|
import apiFetch from '@wordpress/api-fetch';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
9
|
+
// Detection key for the optional Redux store published by CAP <4.0. When present and exposing
|
|
10
|
+
// `getAuthors`, prefer the synchronous path; otherwise fall back to term-ID resolution via REST.
|
|
12
11
|
const CAP_STORE = 'cap/authors';
|
|
13
12
|
const COAUTHORS_ENDPOINT = '/coauthors/v1/coauthors';
|
|
13
|
+
const COAUTHORS_BY_TERM_IDS_ENDPOINT = '/coauthors/v1/authors-by-term-ids';
|
|
14
14
|
|
|
15
15
|
// Module-level cache for guest author avatar URLs, keyed by user_nicename.
|
|
16
16
|
// Prevents duplicate REST requests when components re-mount or when
|
|
@@ -23,6 +23,66 @@ const guestAvatarCache = {};
|
|
|
23
23
|
// any fetch has completed.
|
|
24
24
|
const inflightRequests = {};
|
|
25
25
|
|
|
26
|
+
// Term-ID to author object cache (new CAP). A cached `false` means the server omitted the ID.
|
|
27
|
+
const coauthorDetailsCache = {};
|
|
28
|
+
|
|
29
|
+
// In-flight term-ID fetches keyed by sorted CSV, so concurrent mounts share one REST call.
|
|
30
|
+
const inflightTermResolves = {};
|
|
31
|
+
|
|
32
|
+
// Stable empty-state reference so setState is a no-op when already empty.
|
|
33
|
+
const EMPTY_AUTHORS = Object.freeze( [] );
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetch coauthor details for an array of taxonomy term IDs (new CAP).
|
|
37
|
+
* Deduplicates concurrent requests and caches results by term ID.
|
|
38
|
+
*
|
|
39
|
+
* @param {number[]} termIds Array of coauthor taxonomy term IDs.
|
|
40
|
+
* @return {Promise} Resolves once results are in `coauthorDetailsCache`.
|
|
41
|
+
*/
|
|
42
|
+
function fetchCoauthorsByTermIds( termIds ) {
|
|
43
|
+
if ( ! termIds || termIds.length === 0 ) {
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
}
|
|
46
|
+
const uncachedIds = termIds.filter( id => ! ( id in coauthorDetailsCache ) );
|
|
47
|
+
if ( uncachedIds.length === 0 ) {
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
const key = [ ...uncachedIds ].sort( ( a, b ) => a - b ).join( ',' );
|
|
51
|
+
if ( inflightTermResolves[ key ] ) {
|
|
52
|
+
return inflightTermResolves[ key ];
|
|
53
|
+
}
|
|
54
|
+
inflightTermResolves[ key ] = apiFetch( {
|
|
55
|
+
path: `${ COAUTHORS_BY_TERM_IDS_ENDPOINT }?ids=${ uncachedIds.join( ',' ) }`,
|
|
56
|
+
} )
|
|
57
|
+
.then( results => {
|
|
58
|
+
if ( Array.isArray( results ) ) {
|
|
59
|
+
results.forEach( author => {
|
|
60
|
+
if ( author?.termId ) {
|
|
61
|
+
coauthorDetailsCache[ author.termId ] = author;
|
|
62
|
+
}
|
|
63
|
+
} );
|
|
64
|
+
}
|
|
65
|
+
// Mark IDs that the server omitted as permanently unresolved (e.g. deleted author).
|
|
66
|
+
// Only done on a successful response — transient errors leave cache untouched to allow retries.
|
|
67
|
+
uncachedIds.forEach( id => {
|
|
68
|
+
if ( ! ( id in coauthorDetailsCache ) ) {
|
|
69
|
+
coauthorDetailsCache[ id ] = false;
|
|
70
|
+
}
|
|
71
|
+
} );
|
|
72
|
+
} )
|
|
73
|
+
.catch( () => {
|
|
74
|
+
// Do not poison the cache on transient errors (network, 5xx) or permission errors (403).
|
|
75
|
+
// Concurrent calls are already deduped via `inflightTermResolves`, and leaving the cache
|
|
76
|
+
// untouched allows a later call to `fetchCoauthorsByTermIds` to retry after this
|
|
77
|
+
// in-flight request clears. Failure is signalled to consumers via `hasCoauthorTermIds`
|
|
78
|
+
// remaining true while `authors` stays empty.
|
|
79
|
+
} )
|
|
80
|
+
.finally( () => {
|
|
81
|
+
delete inflightTermResolves[ key ];
|
|
82
|
+
} );
|
|
83
|
+
return inflightTermResolves[ key ];
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
/**
|
|
27
87
|
* Fetch a single coauthor's avatar URLs, deduplicating concurrent requests.
|
|
28
88
|
*
|
|
@@ -59,6 +119,14 @@ export function resetGuestAvatarCacheForTests() {
|
|
|
59
119
|
Object.keys( inflightRequests ).forEach( key => delete inflightRequests[ key ] );
|
|
60
120
|
}
|
|
61
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Reset the module-level coauthor details cache. Exposed for testing only.
|
|
124
|
+
*/
|
|
125
|
+
export function resetCoauthorDetailsCacheForTests() {
|
|
126
|
+
Object.keys( coauthorDetailsCache ).forEach( key => delete coauthorDetailsCache[ key ] );
|
|
127
|
+
Object.keys( inflightTermResolves ).forEach( key => delete inflightTermResolves[ key ] );
|
|
128
|
+
}
|
|
129
|
+
|
|
62
130
|
/**
|
|
63
131
|
* Extract user_nicename from an author archive URL.
|
|
64
132
|
*
|
|
@@ -83,58 +151,144 @@ function extractNicenameFromLink( link ) {
|
|
|
83
151
|
/**
|
|
84
152
|
* Hook to get CoAuthors Plus authors from the CAP store or REST API.
|
|
85
153
|
*
|
|
86
|
-
* For the currently-edited post,
|
|
87
|
-
*
|
|
88
|
-
*
|
|
154
|
+
* For the currently-edited post, uses one of two sources depending on which CAP version is active:
|
|
155
|
+
* - Legacy CAP (Redux store published by CAP <4.0): reads from `cap/authors` for real-time updates.
|
|
156
|
+
* - New CAP (no Redux store, CAP 4.0+): reads taxonomy term IDs from
|
|
157
|
+
* `getEditedPostAttribute('coauthors')` and resolves them to rich author data via the
|
|
158
|
+
* `authors-by-term-ids` REST endpoint.
|
|
159
|
+
* For Query Loop posts, it uses REST data from `newspack_author_info` (unaffected by CAP version).
|
|
89
160
|
*
|
|
90
161
|
* @param {number} postId Post ID to get authors for.
|
|
91
162
|
* @param {string} postType Post type (default: 'post').
|
|
92
163
|
* @param {boolean} skip Skip fetching (default: false).
|
|
93
|
-
* @return {Object}
|
|
164
|
+
* @return {Object} `{ authors, isCapAvailable, isLoading, hasCoauthorTermIds }`.
|
|
165
|
+
* `isLoading` is true during the new-CAP term-ID resolution window (including the
|
|
166
|
+
* pre-flight render before the effect fires); legacy CAP is synchronous.
|
|
167
|
+
* `hasCoauthorTermIds` is true when the new-CAP path is in use AND term IDs are
|
|
168
|
+
* assigned — consumers should treat `authors.length === 0` while this flag is true
|
|
169
|
+
* as "resolution unavailable" (e.g. permission-denied REST) rather than "no coauthors",
|
|
170
|
+
* to avoid silently falling back to default state.
|
|
94
171
|
*/
|
|
95
172
|
export function useCoAuthors( postId, postType = 'post', skip = false ) {
|
|
96
173
|
// Return raw store references from useSelect to avoid creating new objects
|
|
97
174
|
// on every render (which triggers useSelect memoization warnings).
|
|
98
175
|
// The .map() transformations happen in useMemo below.
|
|
99
|
-
const {
|
|
176
|
+
const { legacyCapAuthors, coauthorTermIdsKey, restAuthors, isCapAvailable } = useSelect(
|
|
100
177
|
select => {
|
|
101
178
|
if ( skip ) {
|
|
102
|
-
return {
|
|
179
|
+
return { legacyCapAuthors: null, coauthorTermIdsKey: null, restAuthors: null, isCapAvailable: false };
|
|
103
180
|
}
|
|
104
181
|
|
|
105
182
|
const capStore = select( CAP_STORE );
|
|
106
|
-
const
|
|
183
|
+
const isLegacyCapStoreAvailable = Boolean( capStore && typeof capStore.getAuthors === 'function' );
|
|
107
184
|
|
|
108
185
|
const editorStore = select( 'core/editor' );
|
|
109
186
|
const currentPostId = editorStore?.getCurrentPostId?.();
|
|
110
187
|
const isQueryLoopContext = postId && currentPostId && postId !== currentPostId;
|
|
111
188
|
|
|
112
|
-
//
|
|
113
|
-
|
|
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.
|
|
189
|
+
// Query Loop: read from newspack_author_info on the post entity.
|
|
190
|
+
// Unaffected by CAP version.
|
|
121
191
|
if ( isQueryLoopContext && postId ) {
|
|
122
192
|
const { getEntityRecord } = select( coreStore );
|
|
123
193
|
const post = getEntityRecord( 'postType', postType, postId );
|
|
124
194
|
const restData = post?.newspack_author_info || null;
|
|
125
|
-
return {
|
|
195
|
+
return {
|
|
196
|
+
legacyCapAuthors: null,
|
|
197
|
+
coauthorTermIdsKey: null,
|
|
198
|
+
restAuthors: restData,
|
|
199
|
+
isCapAvailable: restData ? true : isLegacyCapStoreAvailable,
|
|
200
|
+
};
|
|
126
201
|
}
|
|
127
202
|
|
|
128
|
-
|
|
203
|
+
// Legacy CAP: read from cap/authors store for real-time updates.
|
|
204
|
+
if ( isLegacyCapStoreAvailable ) {
|
|
205
|
+
const rawCapAuthors = postId ? capStore.getAuthors( postId ) : null;
|
|
206
|
+
return { legacyCapAuthors: rawCapAuthors, coauthorTermIdsKey: null, restAuthors: null, isCapAvailable: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// New CAP: read taxonomy term IDs from the post entity. Serialize to a stable string
|
|
210
|
+
// inside the selector so a fresh array reference from `getEditedPostAttribute` doesn't
|
|
211
|
+
// trigger spurious re-renders. `null` = attribute not loaded; `''` = loaded but empty;
|
|
212
|
+
// CSV (e.g. `'471,488'`) = term IDs assigned.
|
|
213
|
+
const termIds = editorStore?.getEditedPostAttribute?.( 'coauthors' );
|
|
214
|
+
if ( Array.isArray( termIds ) ) {
|
|
215
|
+
const validIds = termIds.map( Number ).filter( Number.isInteger );
|
|
216
|
+
return {
|
|
217
|
+
legacyCapAuthors: null,
|
|
218
|
+
coauthorTermIdsKey: validIds.length === 0 ? '' : validIds.join( ',' ),
|
|
219
|
+
restAuthors: null,
|
|
220
|
+
isCapAvailable: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// No CAP detected (plugin inactive).
|
|
225
|
+
return { legacyCapAuthors: null, coauthorTermIdsKey: null, restAuthors: null, isCapAvailable: false };
|
|
129
226
|
},
|
|
130
227
|
[ postId, postType, skip ]
|
|
131
228
|
);
|
|
132
229
|
|
|
230
|
+
// Resolve coauthor term IDs to author data via REST (new CAP path only).
|
|
231
|
+
// Uses a module-level cache + in-flight dedup so concurrent mounts share a single fetch.
|
|
232
|
+
const [ resolvedTermAuthors, setResolvedTermAuthors ] = useState( EMPTY_AUTHORS );
|
|
233
|
+
const [ asyncIsLoading, setAsyncIsLoading ] = useState( false );
|
|
234
|
+
// Tracks the last `coauthorTermIdsKey` the effect has processed. Used to derive a synchronous
|
|
235
|
+
// pre-flight loading state during the render between key change and effect firing — without
|
|
236
|
+
// it, consumers briefly see "term IDs assigned but isLoading false" and may pick wrong defaults.
|
|
237
|
+
const [ processedKey, setProcessedKey ] = useState( null );
|
|
238
|
+
|
|
239
|
+
const hasCoauthorTermIds = coauthorTermIdsKey !== null && coauthorTermIdsKey !== '';
|
|
240
|
+
const isPreFlight = hasCoauthorTermIds && processedKey !== coauthorTermIdsKey;
|
|
241
|
+
const isLoading = isPreFlight || asyncIsLoading;
|
|
242
|
+
|
|
243
|
+
useEffect( () => {
|
|
244
|
+
if ( skip || coauthorTermIdsKey === null ) {
|
|
245
|
+
// Use the module-level frozen empty array so the identity is stable
|
|
246
|
+
// and setState is a no-op when already empty.
|
|
247
|
+
setResolvedTermAuthors( EMPTY_AUTHORS );
|
|
248
|
+
setAsyncIsLoading( false );
|
|
249
|
+
setProcessedKey( coauthorTermIdsKey );
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const ids = coauthorTermIdsKey === '' ? [] : coauthorTermIdsKey.split( ',' ).map( Number ).filter( Number.isInteger );
|
|
253
|
+
if ( ids.length === 0 ) {
|
|
254
|
+
setResolvedTermAuthors( EMPTY_AUTHORS );
|
|
255
|
+
setAsyncIsLoading( false );
|
|
256
|
+
setProcessedKey( coauthorTermIdsKey );
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// If everything is already cached, resolve synchronously without flipping loading state.
|
|
261
|
+
const allCached = ids.every( id => id in coauthorDetailsCache );
|
|
262
|
+
if ( allCached ) {
|
|
263
|
+
const resolved = ids.map( id => coauthorDetailsCache[ id ] ).filter( author => author && typeof author === 'object' );
|
|
264
|
+
setResolvedTermAuthors( resolved.length === 0 ? EMPTY_AUTHORS : resolved );
|
|
265
|
+
setAsyncIsLoading( false );
|
|
266
|
+
setProcessedKey( coauthorTermIdsKey );
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let cancelled = false;
|
|
271
|
+
setAsyncIsLoading( true );
|
|
272
|
+
fetchCoauthorsByTermIds( ids ).then( () => {
|
|
273
|
+
if ( cancelled ) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const resolved = ids.map( id => coauthorDetailsCache[ id ] ).filter( author => author && typeof author === 'object' );
|
|
277
|
+
setResolvedTermAuthors( resolved.length === 0 ? EMPTY_AUTHORS : resolved );
|
|
278
|
+
setAsyncIsLoading( false );
|
|
279
|
+
setProcessedKey( coauthorTermIdsKey );
|
|
280
|
+
} );
|
|
281
|
+
|
|
282
|
+
return () => {
|
|
283
|
+
cancelled = true;
|
|
284
|
+
};
|
|
285
|
+
}, [ coauthorTermIdsKey, skip ] );
|
|
286
|
+
|
|
133
287
|
// Map raw store data to our normalized author format.
|
|
134
288
|
const authors = useMemo( () => {
|
|
135
|
-
// CAP store authors: { id, label, display, value, userType }
|
|
136
|
-
if (
|
|
137
|
-
return
|
|
289
|
+
// Legacy CAP store authors: { id, label, display, value, userType }
|
|
290
|
+
if ( legacyCapAuthors && legacyCapAuthors.length > 0 ) {
|
|
291
|
+
return legacyCapAuthors.map( author => ( {
|
|
138
292
|
id: author.id,
|
|
139
293
|
display_name: author.display || author.value || author.label,
|
|
140
294
|
user_nicename: author.value,
|
|
@@ -142,7 +296,21 @@ export function useCoAuthors( postId, postType = 'post', skip = false ) {
|
|
|
142
296
|
} ) );
|
|
143
297
|
}
|
|
144
298
|
|
|
145
|
-
//
|
|
299
|
+
// New CAP authors resolved from term IDs.
|
|
300
|
+
// REST shape: { id (string), termId, displayName, userNicename, userType, ... }
|
|
301
|
+
// Coerce `id` to Number at the source so consumers and the in-file dedupe (`other.id === author.id`)
|
|
302
|
+
// see a consistent shape across legacy CAP (numeric id) and new CAP (string id).
|
|
303
|
+
if ( resolvedTermAuthors && resolvedTermAuthors.length > 0 ) {
|
|
304
|
+
return resolvedTermAuthors.map( author => ( {
|
|
305
|
+
id: Number( author.id ),
|
|
306
|
+
termId: author.termId,
|
|
307
|
+
display_name: author.displayName,
|
|
308
|
+
user_nicename: author.userNicename,
|
|
309
|
+
isGuest: author.userType === 'guest-author',
|
|
310
|
+
} ) );
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// REST API authors from newspack_author_info (Query Loop).
|
|
146
314
|
if ( restAuthors && Array.isArray( restAuthors ) && restAuthors.length > 0 ) {
|
|
147
315
|
return restAuthors.map( author => ( {
|
|
148
316
|
id: author.id,
|
|
@@ -155,7 +323,7 @@ export function useCoAuthors( postId, postType = 'post', skip = false ) {
|
|
|
155
323
|
}
|
|
156
324
|
|
|
157
325
|
return [];
|
|
158
|
-
}, [
|
|
326
|
+
}, [ legacyCapAuthors, resolvedTermAuthors, restAuthors ] );
|
|
159
327
|
|
|
160
328
|
// Fetch avatar URLs from the CAP REST API for authors that need it.
|
|
161
329
|
// The CAP store strips avatar data via formatAuthorData(), so we need
|
|
@@ -216,5 +384,5 @@ export function useCoAuthors( postId, postType = 'post', skip = false ) {
|
|
|
216
384
|
return { ...author, avatar_urls: urls };
|
|
217
385
|
} );
|
|
218
386
|
|
|
219
|
-
return { authors: authorsWithAvatars, isCapAvailable };
|
|
387
|
+
return { authors: authorsWithAvatars, isCapAvailable, isLoading, hasCoauthorTermIds };
|
|
220
388
|
}
|
|
@@ -12,7 +12,7 @@ import apiFetch from '@wordpress/api-fetch';
|
|
|
12
12
|
/**
|
|
13
13
|
* Internal dependencies
|
|
14
14
|
*/
|
|
15
|
-
import { useCoAuthors, resetGuestAvatarCacheForTests } from './use-coauthors';
|
|
15
|
+
import { useCoAuthors, resetGuestAvatarCacheForTests, resetCoauthorDetailsCacheForTests } from './use-coauthors';
|
|
16
16
|
|
|
17
17
|
jest.mock( '@wordpress/data', () => ( {
|
|
18
18
|
useSelect: jest.fn(),
|
|
@@ -27,13 +27,16 @@ jest.mock( '@wordpress/api-fetch', () => jest.fn() );
|
|
|
27
27
|
/**
|
|
28
28
|
* Helper to create a mock select function for multiple stores.
|
|
29
29
|
*
|
|
30
|
-
* @param {Object}
|
|
31
|
-
* @param {Object}
|
|
32
|
-
* @param {number}
|
|
33
|
-
* @param {Object}
|
|
30
|
+
* @param {Object} options Mock options.
|
|
31
|
+
* @param {Object} options.capStore Legacy CAP store mock (null if unavailable — i.e., new CAP).
|
|
32
|
+
* @param {number} options.currentPostId Currently-edited post ID.
|
|
33
|
+
* @param {Object} options.entityRecords Map of postId -> post entity record (used for Query Loop).
|
|
34
|
+
* @param {number[]} options.coauthorTermIds `coauthors` post attribute (new CAP).
|
|
35
|
+
* `undefined` means attribute not set (legacy CAP / plugin missing);
|
|
36
|
+
* `[]` means the attribute is present but empty.
|
|
34
37
|
* @return {Function} Mock select function.
|
|
35
38
|
*/
|
|
36
|
-
const createMockSelect = ( { capStore = null, currentPostId = 123, entityRecords = {} } = {} ) => {
|
|
39
|
+
const createMockSelect = ( { capStore = null, currentPostId = 123, entityRecords = {}, coauthorTermIds } = {} ) => {
|
|
37
40
|
return storeName => {
|
|
38
41
|
if ( storeName === 'cap/authors' ) {
|
|
39
42
|
return capStore;
|
|
@@ -41,6 +44,7 @@ const createMockSelect = ( { capStore = null, currentPostId = 123, entityRecords
|
|
|
41
44
|
if ( storeName === 'core/editor' ) {
|
|
42
45
|
return {
|
|
43
46
|
getCurrentPostId: () => currentPostId,
|
|
47
|
+
getEditedPostAttribute: attr => ( attr === 'coauthors' ? coauthorTermIds : undefined ),
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
50
|
if ( storeName === 'core' ) {
|
|
@@ -56,6 +60,7 @@ describe( 'useCoAuthors', () => {
|
|
|
56
60
|
beforeEach( () => {
|
|
57
61
|
jest.clearAllMocks();
|
|
58
62
|
resetGuestAvatarCacheForTests();
|
|
63
|
+
resetCoauthorDetailsCacheForTests();
|
|
59
64
|
// Default: apiFetch resolves with empty object (no avatar_urls).
|
|
60
65
|
apiFetch.mockResolvedValue( {} );
|
|
61
66
|
} );
|
|
@@ -181,6 +186,227 @@ describe( 'useCoAuthors', () => {
|
|
|
181
186
|
} );
|
|
182
187
|
} );
|
|
183
188
|
|
|
189
|
+
describe( 'currently-edited post (new CAP, term ID resolution)', () => {
|
|
190
|
+
const NEW_CAP_RESPONSE = [
|
|
191
|
+
{ id: '1', termId: 471, displayName: 'Jane Doe', userNicename: 'jane-doe', userType: 'wpuser', login: 'jane', email: 'jane@x.com' },
|
|
192
|
+
{ id: '2', termId: 488, displayName: 'John Smith', userNicename: 'john-smith', userType: 'wpuser', login: 'john', email: 'john@x.com' },
|
|
193
|
+
{ id: '1591', termId: 483, displayName: 'External', userNicename: 'external', userType: 'guest-author', login: 'external', email: '' },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
it( 'should resolve term IDs via REST when legacy CAP store is absent', async () => {
|
|
197
|
+
apiFetch.mockResolvedValueOnce( NEW_CAP_RESPONSE );
|
|
198
|
+
|
|
199
|
+
useSelect.mockImplementation( callback =>
|
|
200
|
+
callback(
|
|
201
|
+
createMockSelect( {
|
|
202
|
+
capStore: null, // legacy CAP not registered
|
|
203
|
+
currentPostId: 123,
|
|
204
|
+
coauthorTermIds: [ 471, 488, 483 ],
|
|
205
|
+
} )
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const { result } = renderHook( () => useCoAuthors( 123 ) );
|
|
210
|
+
|
|
211
|
+
await waitFor( () => {
|
|
212
|
+
expect( result.current.authors ).toHaveLength( 3 );
|
|
213
|
+
} );
|
|
214
|
+
|
|
215
|
+
expect( apiFetch ).toHaveBeenCalledWith( {
|
|
216
|
+
path: '/coauthors/v1/authors-by-term-ids?ids=471,488,483',
|
|
217
|
+
} );
|
|
218
|
+
expect( result.current.authors[ 0 ] ).toMatchObject( {
|
|
219
|
+
id: 1,
|
|
220
|
+
termId: 471,
|
|
221
|
+
display_name: 'Jane Doe',
|
|
222
|
+
user_nicename: 'jane-doe',
|
|
223
|
+
isGuest: false,
|
|
224
|
+
} );
|
|
225
|
+
expect( result.current.authors[ 2 ].isGuest ).toBe( true );
|
|
226
|
+
expect( result.current.isCapAvailable ).toBe( true );
|
|
227
|
+
expect( result.current.hasCoauthorTermIds ).toBe( true );
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
it( 'should return empty authors when coauthors attribute is an empty array', async () => {
|
|
231
|
+
useSelect.mockImplementation( callback =>
|
|
232
|
+
callback(
|
|
233
|
+
createMockSelect( {
|
|
234
|
+
capStore: null,
|
|
235
|
+
currentPostId: 123,
|
|
236
|
+
coauthorTermIds: [],
|
|
237
|
+
} )
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const { result } = renderHook( () => useCoAuthors( 123 ) );
|
|
242
|
+
|
|
243
|
+
await act( () => Promise.resolve() );
|
|
244
|
+
|
|
245
|
+
expect( result.current.authors ).toEqual( [] );
|
|
246
|
+
expect( result.current.isCapAvailable ).toBe( true );
|
|
247
|
+
expect( apiFetch ).not.toHaveBeenCalled();
|
|
248
|
+
} );
|
|
249
|
+
|
|
250
|
+
it( 'should mark isCapAvailable false when neither legacy store nor coauthors attribute are present', () => {
|
|
251
|
+
useSelect.mockImplementation( callback =>
|
|
252
|
+
callback(
|
|
253
|
+
createMockSelect( {
|
|
254
|
+
capStore: null,
|
|
255
|
+
currentPostId: 123,
|
|
256
|
+
// coauthorTermIds undefined
|
|
257
|
+
} )
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const { result } = renderHook( () => useCoAuthors( 123 ) );
|
|
262
|
+
|
|
263
|
+
expect( result.current.isCapAvailable ).toBe( false );
|
|
264
|
+
expect( result.current.authors ).toEqual( [] );
|
|
265
|
+
} );
|
|
266
|
+
|
|
267
|
+
it( 'should prefer legacy CAP store when both legacy store and coauthors attribute are present', async () => {
|
|
268
|
+
// This simulates an in-flight upgrade scenario where old JS is still loaded.
|
|
269
|
+
// Legacy path should win to keep behavior consistent.
|
|
270
|
+
useSelect.mockImplementation( callback =>
|
|
271
|
+
callback(
|
|
272
|
+
createMockSelect( {
|
|
273
|
+
capStore: {
|
|
274
|
+
getAuthors: () => [ { id: 99, display: 'Legacy', value: 'legacy', userType: 'wpuser' } ],
|
|
275
|
+
},
|
|
276
|
+
currentPostId: 123,
|
|
277
|
+
coauthorTermIds: [ 471 ],
|
|
278
|
+
} )
|
|
279
|
+
)
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const { result } = renderHook( () => useCoAuthors( 123 ) );
|
|
283
|
+
|
|
284
|
+
await act( () => Promise.resolve() );
|
|
285
|
+
|
|
286
|
+
expect( apiFetch ).not.toHaveBeenCalled();
|
|
287
|
+
expect( result.current.authors ).toEqual( [ { id: 99, display_name: 'Legacy', user_nicename: 'legacy', isGuest: false } ] );
|
|
288
|
+
} );
|
|
289
|
+
|
|
290
|
+
it( 'should cache results so subsequent mounts skip the REST call', async () => {
|
|
291
|
+
apiFetch.mockResolvedValue( NEW_CAP_RESPONSE );
|
|
292
|
+
|
|
293
|
+
useSelect.mockImplementation( callback =>
|
|
294
|
+
callback(
|
|
295
|
+
createMockSelect( {
|
|
296
|
+
capStore: null,
|
|
297
|
+
currentPostId: 123,
|
|
298
|
+
coauthorTermIds: [ 471, 488, 483 ],
|
|
299
|
+
} )
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const { result: result1 } = renderHook( () => useCoAuthors( 123 ) );
|
|
304
|
+
|
|
305
|
+
await waitFor( () => {
|
|
306
|
+
expect( result1.current.authors ).toHaveLength( 3 );
|
|
307
|
+
} );
|
|
308
|
+
|
|
309
|
+
const callsAfterFirstMount = apiFetch.mock.calls.length;
|
|
310
|
+
|
|
311
|
+
// A later mount re-uses the cache and makes no additional calls.
|
|
312
|
+
const { result: result2 } = renderHook( () => useCoAuthors( 123 ) );
|
|
313
|
+
|
|
314
|
+
await waitFor( () => {
|
|
315
|
+
expect( result2.current.authors ).toHaveLength( 3 );
|
|
316
|
+
} );
|
|
317
|
+
|
|
318
|
+
expect( apiFetch ).toHaveBeenCalledTimes( callsAfterFirstMount );
|
|
319
|
+
} );
|
|
320
|
+
|
|
321
|
+
it( 'should silently drop term IDs that do not resolve', async () => {
|
|
322
|
+
// REST returns only 2 of 3 requested IDs (the third was deleted).
|
|
323
|
+
apiFetch.mockResolvedValueOnce( [ NEW_CAP_RESPONSE[ 0 ], NEW_CAP_RESPONSE[ 1 ] ] );
|
|
324
|
+
|
|
325
|
+
useSelect.mockImplementation( callback =>
|
|
326
|
+
callback(
|
|
327
|
+
createMockSelect( {
|
|
328
|
+
capStore: null,
|
|
329
|
+
currentPostId: 123,
|
|
330
|
+
coauthorTermIds: [ 471, 488, 99999 ],
|
|
331
|
+
} )
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const { result } = renderHook( () => useCoAuthors( 123 ) );
|
|
336
|
+
|
|
337
|
+
await waitFor( () => {
|
|
338
|
+
expect( result.current.authors ).toHaveLength( 2 );
|
|
339
|
+
} );
|
|
340
|
+
|
|
341
|
+
expect( result.current.authors.map( a => a.termId ) ).toEqual( [ 471, 488 ] );
|
|
342
|
+
} );
|
|
343
|
+
|
|
344
|
+
it( 'should handle REST errors without blocking the component', async () => {
|
|
345
|
+
apiFetch.mockRejectedValueOnce( new Error( 'Server error' ) );
|
|
346
|
+
|
|
347
|
+
useSelect.mockImplementation( callback =>
|
|
348
|
+
callback(
|
|
349
|
+
createMockSelect( {
|
|
350
|
+
capStore: null,
|
|
351
|
+
currentPostId: 123,
|
|
352
|
+
coauthorTermIds: [ 471 ],
|
|
353
|
+
} )
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const { result } = renderHook( () => useCoAuthors( 123 ) );
|
|
358
|
+
|
|
359
|
+
// Allow the failed promise to settle.
|
|
360
|
+
await act( () => Promise.resolve().then( () => Promise.resolve() ) );
|
|
361
|
+
|
|
362
|
+
expect( result.current.authors ).toEqual( [] );
|
|
363
|
+
expect( result.current.isCapAvailable ).toBe( true );
|
|
364
|
+
// Term IDs are assigned but resolution returned empty — surface this so consumers
|
|
365
|
+
// don't silently fall back to `post_author` and credit the wrong person.
|
|
366
|
+
expect( result.current.hasCoauthorTermIds ).toBe( true );
|
|
367
|
+
} );
|
|
368
|
+
|
|
369
|
+
it( 'should allow retry after a transient REST error (no negative caching on catch)', async () => {
|
|
370
|
+
const RETRY_RESPONSE = [
|
|
371
|
+
{ id: '1', termId: 471, displayName: 'Jane', userNicename: 'jane', userType: 'wpuser', login: 'jane', email: '' },
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
useSelect.mockImplementation( callback =>
|
|
375
|
+
callback(
|
|
376
|
+
createMockSelect( {
|
|
377
|
+
capStore: null,
|
|
378
|
+
currentPostId: 123,
|
|
379
|
+
coauthorTermIds: [ 471 ],
|
|
380
|
+
} )
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// First attempt fails with a transient error; cache should NOT be poisoned.
|
|
385
|
+
apiFetch.mockRejectedValueOnce( new Error( 'Transient 500' ) );
|
|
386
|
+
|
|
387
|
+
const { result: result1, unmount: unmount1 } = renderHook( () => useCoAuthors( 123 ) );
|
|
388
|
+
|
|
389
|
+
await act( () => Promise.resolve().then( () => Promise.resolve() ) );
|
|
390
|
+
|
|
391
|
+
expect( result1.current.authors ).toEqual( [] );
|
|
392
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 1 );
|
|
393
|
+
|
|
394
|
+
unmount1();
|
|
395
|
+
|
|
396
|
+
// Second attempt succeeds — we should actually re-fetch (not a cached failure).
|
|
397
|
+
apiFetch.mockResolvedValueOnce( RETRY_RESPONSE );
|
|
398
|
+
|
|
399
|
+
const { result: result2 } = renderHook( () => useCoAuthors( 123 ) );
|
|
400
|
+
|
|
401
|
+
await waitFor( () => {
|
|
402
|
+
expect( result2.current.authors ).toHaveLength( 1 );
|
|
403
|
+
} );
|
|
404
|
+
|
|
405
|
+
expect( apiFetch ).toHaveBeenCalledTimes( 2 );
|
|
406
|
+
expect( result2.current.authors[ 0 ].display_name ).toBe( 'Jane' );
|
|
407
|
+
} );
|
|
408
|
+
} );
|
|
409
|
+
|
|
184
410
|
describe( 'Query Loop context (uses REST API)', () => {
|
|
185
411
|
it( 'should use REST API when postId differs from currently-edited post', () => {
|
|
186
412
|
const restAuthors = [
|
package/shared/scss/_mixins.scss
CHANGED
|
@@ -15,14 +15,21 @@
|
|
|
15
15
|
// Used by: author-profile-social, author-social-link blocks
|
|
16
16
|
// Hex fallbacks from newspack-block-theme theme.json defaults.
|
|
17
17
|
@mixin socialButton {
|
|
18
|
-
background: var(--wp--preset--color--base-2, #f0f0f0);
|
|
19
18
|
border-radius: 50%; // Circular
|
|
20
|
-
color: var(--wp--preset--color--contrast, #1e1e1e);
|
|
21
19
|
display: grid;
|
|
22
20
|
place-items: center;
|
|
23
21
|
text-decoration: none;
|
|
24
|
-
transition: background 125ms ease-in-out, color 125ms ease-in-out;
|
|
25
22
|
|
|
23
|
+
// Use :not(.wp-element-button) to beat wp-elements parent link colour rules,
|
|
24
|
+
// which use :where() and therefore have lower specificity.
|
|
25
|
+
&:not(.wp-element-button) {
|
|
26
|
+
background: var(--icon-background, var(--wp--preset--color--base-2, #f0f0f0));
|
|
27
|
+
color: var(--icon-color, var(--wp--preset--color--contrast, #1e1e1e));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@media not (prefers-reduced-motion) {
|
|
31
|
+
transition: transform 100ms ease;
|
|
32
|
+
}
|
|
26
33
|
// Sizing - makes circular by using equal width/height
|
|
27
34
|
// --icon-size controls the circle diameter (e.g., 36px)
|
|
28
35
|
// Icon inside is 2/3 of circle size (e.g., 24px icon in 36px circle)
|
|
@@ -32,9 +39,10 @@
|
|
|
32
39
|
padding: 0;
|
|
33
40
|
|
|
34
41
|
&:hover {
|
|
35
|
-
background: var(--wp--preset--color--base-
|
|
36
|
-
color: var(--wp--preset--color--contrast, #1e1e1e);
|
|
42
|
+
background: var(--icon-background, var(--wp--preset--color--base-2, #f0f0f0));
|
|
43
|
+
color: var(--icon-color, var(--wp--preset--color--contrast, #1e1e1e));
|
|
37
44
|
text-decoration: none;
|
|
45
|
+
transform: scale(1.1);
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
&:focus {
|
package/src/accordion/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
|
2
|
-
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
2
|
* WordPress dependencies
|
|
6
3
|
*/
|
|
@@ -17,11 +14,15 @@ import classNames from 'classnames';
|
|
|
17
14
|
*/
|
|
18
15
|
import './style.scss';
|
|
19
16
|
|
|
20
|
-
const Accordion = ( { children, title } ) => {
|
|
21
|
-
const [ isOpen, setIsOpen ] = useState(
|
|
17
|
+
const Accordion = ( { children, title, defaultOpen = false } ) => {
|
|
18
|
+
const [ isOpen, setIsOpen ] = useState( defaultOpen );
|
|
22
19
|
return (
|
|
23
|
-
<details
|
|
24
|
-
|
|
20
|
+
<details
|
|
21
|
+
className={ classNames( 'newspack-accordion', { 'newspack-accordion--is-open': isOpen } ) }
|
|
22
|
+
open={ isOpen }
|
|
23
|
+
onToggle={ e => setIsOpen( e.currentTarget.open ) }
|
|
24
|
+
>
|
|
25
|
+
<summary>
|
|
25
26
|
{ title }
|
|
26
27
|
<Icon className="newspack-accordion__icon" icon={ chevronRight } size={ 24 } />
|
|
27
28
|
</summary>
|
package/src/action-card/index.js
CHANGED
|
@@ -39,6 +39,9 @@ const ActionCard = ( {
|
|
|
39
39
|
heading = 2,
|
|
40
40
|
description,
|
|
41
41
|
handoff,
|
|
42
|
+
handoffUrl,
|
|
43
|
+
bannerText,
|
|
44
|
+
bannerButtonText,
|
|
42
45
|
editLink,
|
|
43
46
|
href,
|
|
44
47
|
notification,
|
|
@@ -187,7 +190,18 @@ const ActionCard = ( {
|
|
|
187
190
|
{ actionContent && actionContent }
|
|
188
191
|
{ actionText &&
|
|
189
192
|
( handoff ? (
|
|
190
|
-
<Handoff
|
|
193
|
+
<Handoff
|
|
194
|
+
plugin={ handoff }
|
|
195
|
+
editLink={ editLink }
|
|
196
|
+
bannerText={ bannerText }
|
|
197
|
+
bannerButtonText={ bannerButtonText }
|
|
198
|
+
compact
|
|
199
|
+
isLink
|
|
200
|
+
>
|
|
201
|
+
{ actionText }
|
|
202
|
+
</Handoff>
|
|
203
|
+
) : handoffUrl ? (
|
|
204
|
+
<Handoff url={ handoffUrl } bannerText={ bannerText } bannerButtonText={ bannerButtonText } compact isLink>
|
|
191
205
|
{ actionText }
|
|
192
206
|
</Handoff>
|
|
193
207
|
) : onClick || hasInternalLink ? (
|
package/src/badge/index.tsx
CHANGED
|
@@ -8,9 +8,11 @@ import './style.scss';
|
|
|
8
8
|
*/
|
|
9
9
|
import classnames from 'classnames';
|
|
10
10
|
|
|
11
|
+
export type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
|
12
|
+
|
|
11
13
|
type BadgeProps = {
|
|
12
14
|
text: string;
|
|
13
|
-
level?:
|
|
15
|
+
level?: BadgeLevel;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
/**
|