newspack-components 4.2.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.
Files changed (315) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/DEVELOPMENT.md +868 -0
  3. package/copy-styles.js +3 -2
  4. package/dist/cjs/accordion/index.js +17 -12
  5. package/dist/cjs/action-card/index.js +186 -140
  6. package/dist/cjs/action-card/style.scss +25 -2
  7. package/dist/cjs/autocomplete-tokenfield/index.js +254 -234
  8. package/dist/cjs/autocomplete-with-latest-posts/index.js +351 -0
  9. package/dist/cjs/autocomplete-with-latest-posts/style.scss +150 -0
  10. package/dist/cjs/autocomplete-with-suggestions/index.js +229 -126
  11. package/dist/cjs/badge/index.js +32 -0
  12. package/dist/cjs/badge/style.scss +36 -0
  13. package/dist/cjs/box-contrast/index.js +19 -15
  14. package/dist/cjs/button/index.js +41 -24
  15. package/dist/cjs/button-props/button-props.js +5 -5
  16. package/dist/cjs/button-props/index.js +2 -2
  17. package/dist/cjs/card/core-card.js +199 -0
  18. package/dist/cjs/card/index.js +75 -35
  19. package/dist/cjs/card/style-core.scss +295 -0
  20. package/dist/cjs/card/style.scss +4 -4
  21. package/dist/cjs/card-feature/index.js +141 -0
  22. package/dist/cjs/card-feature/style.scss +75 -0
  23. package/dist/cjs/card-form/index.js +144 -0
  24. package/dist/cjs/card-form/style.scss +20 -0
  25. package/dist/cjs/card-settings-group/index.js +69 -0
  26. package/dist/cjs/card-settings-group/style.scss +33 -0
  27. package/dist/cjs/card-sortable-list/index.js +426 -0
  28. package/dist/cjs/card-sortable-list/style.scss +48 -0
  29. package/dist/cjs/category-autocomplete/index.js +175 -155
  30. package/dist/cjs/color-picker/index.js +41 -23
  31. package/dist/cjs/color-picker/index.test.js +82 -0
  32. package/dist/cjs/color-picker/style.scss +11 -13
  33. package/dist/cjs/confirm-dialog/index.js +123 -0
  34. package/dist/cjs/consts.js +1 -1
  35. package/dist/cjs/custom-select-control/index.js +39 -28
  36. package/dist/cjs/dataviews/index.js +38 -0
  37. package/dist/cjs/dataviews/style.scss +9 -0
  38. package/dist/cjs/divider/index.js +47 -0
  39. package/dist/cjs/divider/style.scss +51 -0
  40. package/dist/cjs/footer/index.js +36 -30
  41. package/dist/cjs/form-token-field/index.js +51 -40
  42. package/dist/cjs/form-token-field/style.scss +0 -5
  43. package/dist/cjs/global-notices/index.js +5 -5
  44. package/dist/cjs/grid/index.js +25 -18
  45. package/dist/cjs/grid/style.scss +67 -8
  46. package/dist/cjs/handoff/index.js +204 -156
  47. package/dist/cjs/handoff-message/index.js +12 -7
  48. package/dist/cjs/hooks/index.js +10 -8
  49. package/dist/cjs/hooks/use-confirm-dialog.js +79 -0
  50. package/dist/cjs/hooks/use-unsaved-changes-dialog.js +144 -0
  51. package/dist/cjs/hooks/useObjectState.js +21 -12
  52. package/dist/cjs/hooks/useObjectState.test.js +55 -33
  53. package/dist/cjs/hooks/useOnClickOutside.js +6 -6
  54. package/dist/cjs/hooks/usePrompt.js +10 -13
  55. package/dist/cjs/image-upload/index.js +114 -123
  56. package/dist/cjs/image-upload/index.test.js +11 -13
  57. package/dist/cjs/image-upload/style.scss +12 -40
  58. package/dist/cjs/index.js +166 -92
  59. package/dist/cjs/info-button/index.js +43 -32
  60. package/dist/cjs/integration-icons/active-campaign.js +22 -0
  61. package/dist/cjs/integration-icons/constant-contact.js +28 -0
  62. package/dist/cjs/integration-icons/fundraise-up.js +25 -0
  63. package/dist/cjs/integration-icons/index.js +48 -0
  64. package/dist/cjs/integration-icons/mailchimp.js +19 -0
  65. package/dist/cjs/integration-icons/salesforce.js +17 -0
  66. package/dist/cjs/integration-icons/wisepops.js +20 -0
  67. package/dist/cjs/modal/index.js +29 -19
  68. package/dist/cjs/modal/style.scss +47 -13
  69. package/dist/cjs/newspack-icon/index.js +45 -38
  70. package/dist/cjs/newspack-icon/style.scss +5 -3
  71. package/dist/cjs/notice/index.js +64 -55
  72. package/dist/cjs/plugin-installer/index.js +259 -253
  73. package/dist/cjs/plugin-settings/SettingsSection.js +66 -60
  74. package/dist/cjs/plugin-settings/index.js +217 -200
  75. package/dist/cjs/plugin-toggle/index.js +192 -199
  76. package/dist/cjs/popover/index.js +41 -28
  77. package/dist/cjs/position-control/index.js +24 -23
  78. package/dist/cjs/progress-bar/index.js +71 -59
  79. package/dist/cjs/progress-bar/index.test.js +62 -68
  80. package/dist/cjs/proxied-imports/router.js +3 -3
  81. package/dist/cjs/radio-control/index.js +40 -27
  82. package/dist/cjs/section-header/index.js +143 -39
  83. package/dist/cjs/section-header/style.scss +129 -4
  84. package/dist/cjs/select-control/ButtonGroupControl.js +26 -21
  85. package/dist/cjs/select-control/GroupedSelectControl.js +41 -35
  86. package/dist/cjs/select-control/index.js +52 -40
  87. package/dist/cjs/settings/MinMaxSetting.js +30 -20
  88. package/dist/cjs/settings/SettingsCard.js +20 -17
  89. package/dist/cjs/settings/SettingsSection.js +22 -21
  90. package/dist/cjs/settings/index.js +5 -5
  91. package/dist/cjs/sortable-newsletter-list-control/index.js +78 -57
  92. package/dist/cjs/steps-list/index.js +44 -34
  93. package/dist/cjs/steps-list-item/index.js +47 -39
  94. package/dist/cjs/style-card/index.js +70 -63
  95. package/dist/cjs/style.scss +2 -0
  96. package/dist/cjs/tabbed-navigation/index.js +38 -36
  97. package/dist/cjs/text-control/index.js +27 -25
  98. package/dist/cjs/utils/color.js +4 -4
  99. package/dist/cjs/utils/editor-toolbar-back-button.js +61 -0
  100. package/dist/cjs/utils/index.js +31 -19
  101. package/dist/cjs/utils/style.scss +8 -0
  102. package/dist/cjs/waiting/index.js +48 -35
  103. package/dist/cjs/waiting/style.scss +4 -0
  104. package/dist/cjs/web-preview/index.js +219 -154
  105. package/dist/cjs/web-preview/style.scss +20 -53
  106. package/dist/cjs/with-wizard/index.js +362 -364
  107. package/dist/cjs/with-wizard/style.scss +28 -60
  108. package/dist/cjs/with-wizard-screen/index.js +54 -58
  109. package/dist/cjs/with-wizard-screen/style.scss +77 -10
  110. package/dist/cjs/wizard/components/WizardError.js +20 -18
  111. package/dist/cjs/wizard/components/WizardSnackbar.js +73 -0
  112. package/dist/cjs/wizard/components/style.scss +48 -0
  113. package/dist/cjs/wizard/index.js +207 -64
  114. package/dist/cjs/wizard/store/index.js +191 -97
  115. package/dist/cjs/wizard/store/utils.js +13 -8
  116. package/dist/esm/accordion/index.js +14 -9
  117. package/dist/esm/action-card/index.js +178 -124
  118. package/dist/esm/action-card/style.scss +25 -2
  119. package/dist/esm/autocomplete-tokenfield/index.js +248 -222
  120. package/dist/esm/autocomplete-with-latest-posts/index.js +347 -0
  121. package/dist/esm/autocomplete-with-latest-posts/style.scss +150 -0
  122. package/dist/esm/autocomplete-with-suggestions/index.js +226 -119
  123. package/dist/esm/badge/index.js +24 -0
  124. package/dist/esm/badge/style.scss +36 -0
  125. package/dist/esm/box-contrast/index.js +15 -11
  126. package/dist/esm/button/index.js +39 -19
  127. package/dist/esm/button-props/button-props.js +1 -1
  128. package/dist/esm/card/core-card.js +195 -0
  129. package/dist/esm/card/index.js +70 -22
  130. package/dist/esm/card/style-core.scss +295 -0
  131. package/dist/esm/card/style.scss +4 -4
  132. package/dist/esm/card-feature/index.js +131 -0
  133. package/dist/esm/card-feature/style.scss +75 -0
  134. package/dist/esm/card-form/index.js +134 -0
  135. package/dist/esm/card-form/style.scss +20 -0
  136. package/dist/esm/card-settings-group/index.js +61 -0
  137. package/dist/esm/card-settings-group/style.scss +33 -0
  138. package/dist/esm/card-sortable-list/index.js +425 -0
  139. package/dist/esm/card-sortable-list/style.scss +48 -0
  140. package/dist/esm/category-autocomplete/index.js +168 -140
  141. package/dist/esm/color-picker/index.js +39 -21
  142. package/dist/esm/color-picker/index.test.js +78 -0
  143. package/dist/esm/color-picker/style.scss +11 -13
  144. package/dist/esm/confirm-dialog/index.js +116 -0
  145. package/dist/esm/consts.js +1 -1
  146. package/dist/esm/custom-select-control/index.js +34 -15
  147. package/dist/esm/dataviews/index.js +34 -0
  148. package/dist/esm/dataviews/style.scss +9 -0
  149. package/dist/esm/divider/index.js +42 -0
  150. package/dist/esm/divider/style.scss +51 -0
  151. package/dist/esm/footer/index.js +34 -28
  152. package/dist/esm/form-token-field/index.js +46 -27
  153. package/dist/esm/form-token-field/style.scss +0 -5
  154. package/dist/esm/global-notices/index.js +3 -3
  155. package/dist/esm/grid/index.js +23 -13
  156. package/dist/esm/grid/style.scss +67 -8
  157. package/dist/esm/handoff/index.js +199 -143
  158. package/dist/esm/handoff-message/index.js +10 -6
  159. package/dist/esm/hooks/index.js +8 -6
  160. package/dist/esm/hooks/use-confirm-dialog.js +73 -0
  161. package/dist/esm/hooks/use-unsaved-changes-dialog.js +136 -0
  162. package/dist/esm/hooks/useObjectState.js +19 -9
  163. package/dist/esm/hooks/useObjectState.test.js +54 -32
  164. package/dist/esm/hooks/useOnClickOutside.js +4 -4
  165. package/dist/esm/hooks/usePrompt.js +9 -11
  166. package/dist/esm/image-upload/index.js +110 -111
  167. package/dist/esm/image-upload/index.test.js +11 -13
  168. package/dist/esm/image-upload/style.scss +12 -40
  169. package/dist/esm/index.js +13 -1
  170. package/dist/esm/info-button/index.js +38 -19
  171. package/dist/esm/integration-icons/active-campaign.js +16 -0
  172. package/dist/esm/integration-icons/constant-contact.js +22 -0
  173. package/dist/esm/integration-icons/fundraise-up.js +19 -0
  174. package/dist/esm/integration-icons/index.js +6 -0
  175. package/dist/esm/integration-icons/mailchimp.js +13 -0
  176. package/dist/esm/integration-icons/salesforce.js +11 -0
  177. package/dist/esm/integration-icons/wisepops.js +14 -0
  178. package/dist/esm/modal/index.js +27 -13
  179. package/dist/esm/modal/style.scss +47 -13
  180. package/dist/esm/newspack-icon/index.js +40 -25
  181. package/dist/esm/newspack-icon/style.scss +5 -3
  182. package/dist/esm/notice/index.js +59 -42
  183. package/dist/esm/plugin-installer/index.js +255 -243
  184. package/dist/esm/plugin-settings/SettingsSection.js +61 -50
  185. package/dist/esm/plugin-settings/index.js +212 -189
  186. package/dist/esm/plugin-toggle/index.js +187 -190
  187. package/dist/esm/popover/index.js +35 -16
  188. package/dist/esm/position-control/index.js +22 -18
  189. package/dist/esm/progress-bar/index.js +66 -49
  190. package/dist/esm/progress-bar/index.test.js +61 -67
  191. package/dist/esm/radio-control/index.js +34 -15
  192. package/dist/esm/section-header/index.js +140 -38
  193. package/dist/esm/section-header/style.scss +129 -4
  194. package/dist/esm/select-control/ButtonGroupControl.js +23 -18
  195. package/dist/esm/select-control/GroupedSelectControl.js +38 -30
  196. package/dist/esm/select-control/index.js +47 -27
  197. package/dist/esm/settings/MinMaxSetting.js +27 -16
  198. package/dist/esm/settings/SettingsCard.js +18 -13
  199. package/dist/esm/settings/SettingsSection.js +20 -19
  200. package/dist/esm/settings/index.js +3 -3
  201. package/dist/esm/sortable-newsletter-list-control/index.js +74 -53
  202. package/dist/esm/steps-list/index.js +39 -21
  203. package/dist/esm/steps-list-item/index.js +42 -26
  204. package/dist/esm/style-card/index.js +65 -50
  205. package/dist/esm/style.scss +2 -0
  206. package/dist/esm/tabbed-navigation/index.js +35 -33
  207. package/dist/esm/text-control/index.js +25 -19
  208. package/dist/esm/utils/color.js +4 -4
  209. package/dist/esm/utils/editor-toolbar-back-button.js +53 -0
  210. package/dist/esm/utils/index.js +29 -16
  211. package/dist/esm/utils/style.scss +8 -0
  212. package/dist/esm/waiting/index.js +43 -22
  213. package/dist/esm/waiting/style.scss +4 -0
  214. package/dist/esm/web-preview/index.js +218 -147
  215. package/dist/esm/web-preview/style.scss +20 -53
  216. package/dist/esm/with-wizard/index.js +360 -358
  217. package/dist/esm/with-wizard/style.scss +28 -60
  218. package/dist/esm/with-wizard-screen/index.js +48 -46
  219. package/dist/esm/with-wizard-screen/style.scss +77 -10
  220. package/dist/esm/wizard/components/WizardError.js +18 -16
  221. package/dist/esm/wizard/components/WizardSnackbar.js +70 -0
  222. package/dist/esm/wizard/components/style.scss +48 -0
  223. package/dist/esm/wizard/index.js +207 -58
  224. package/dist/esm/wizard/store/index.js +189 -90
  225. package/dist/esm/wizard/store/utils.js +13 -6
  226. package/package.json +30 -17
  227. package/release.config.js +1 -0
  228. package/shared/author-context.js +26 -0
  229. package/shared/hooks/use-coauthors.js +388 -0
  230. package/shared/hooks/use-coauthors.test.js +850 -0
  231. package/shared/hooks/use-custom-byline.js +47 -0
  232. package/shared/hooks/use-custom-byline.test.js +133 -0
  233. package/shared/scss/_mixins.scss +54 -0
  234. package/src/accordion/index.js +8 -7
  235. package/src/action-card/action-card.d.ts +7 -3
  236. package/src/action-card/index.js +39 -12
  237. package/src/action-card/style.scss +25 -2
  238. package/src/autocomplete-tokenfield/index.js +2 -2
  239. package/src/autocomplete-with-latest-posts/index.js +290 -0
  240. package/src/autocomplete-with-latest-posts/style.scss +150 -0
  241. package/src/badge/index.tsx +26 -0
  242. package/src/badge/style.scss +36 -0
  243. package/src/button/index.tsx +3 -2
  244. package/src/card/core-card.js +224 -0
  245. package/src/card/index.js +40 -1
  246. package/src/card/style-core.scss +295 -0
  247. package/src/card/style.scss +4 -4
  248. package/src/card-feature/README.md +188 -0
  249. package/src/card-feature/index.tsx +186 -0
  250. package/src/card-feature/style.scss +75 -0
  251. package/src/card-form/README.md +113 -0
  252. package/src/card-form/index.tsx +128 -0
  253. package/src/card-form/style.scss +20 -0
  254. package/src/card-settings-group/README.md +43 -0
  255. package/src/card-settings-group/index.tsx +78 -0
  256. package/src/card-settings-group/style.scss +33 -0
  257. package/src/card-sortable-list/README.md +51 -0
  258. package/src/card-sortable-list/index.tsx +435 -0
  259. package/src/card-sortable-list/style.scss +48 -0
  260. package/src/color-picker/index.js +15 -8
  261. package/src/color-picker/index.test.js +52 -0
  262. package/src/color-picker/style.scss +11 -13
  263. package/src/confirm-dialog/README.md +128 -0
  264. package/src/confirm-dialog/index.tsx +132 -0
  265. package/src/dataviews/index.tsx +34 -0
  266. package/src/dataviews/style.scss +9 -0
  267. package/src/divider/README.md +25 -0
  268. package/src/divider/index.js +31 -0
  269. package/src/divider/style.scss +51 -0
  270. package/src/form-token-field/style.scss +0 -5
  271. package/src/grid/index.tsx +2 -1
  272. package/src/grid/style.scss +67 -8
  273. package/src/handoff/README.md +93 -0
  274. package/src/handoff/index.js +46 -8
  275. package/src/hooks/index.ts +2 -1
  276. package/src/hooks/use-confirm-dialog.tsx +80 -0
  277. package/src/hooks/use-unsaved-changes-dialog.tsx +140 -0
  278. package/src/image-upload/index.js +10 -15
  279. package/src/image-upload/style.scss +12 -40
  280. package/src/index.js +13 -1
  281. package/src/integration-icons/active-campaign.js +14 -0
  282. package/src/integration-icons/constant-contact.js +22 -0
  283. package/src/integration-icons/fundraise-up.js +18 -0
  284. package/src/integration-icons/index.js +6 -0
  285. package/src/integration-icons/mailchimp.js +10 -0
  286. package/src/integration-icons/salesforce.js +10 -0
  287. package/src/integration-icons/wisepops.js +14 -0
  288. package/src/modal/index.js +12 -3
  289. package/src/modal/style.scss +47 -13
  290. package/src/newspack-icon/style.scss +5 -3
  291. package/src/plugin-installer/index.js +30 -19
  292. package/src/section-header/index.js +105 -14
  293. package/src/section-header/style.scss +129 -4
  294. package/src/select-control/index.js +4 -0
  295. package/src/style.scss +2 -0
  296. package/src/utils/editor-toolbar-back-button.tsx +48 -0
  297. package/src/utils/index.js +6 -0
  298. package/src/utils/style.scss +8 -0
  299. package/src/waiting/index.js +2 -1
  300. package/src/waiting/style.scss +4 -0
  301. package/src/web-preview/index.js +90 -35
  302. package/src/web-preview/style.scss +20 -53
  303. package/src/with-wizard/index.js +1 -1
  304. package/src/with-wizard/style.scss +28 -60
  305. package/src/with-wizard-screen/style.scss +77 -10
  306. package/src/wizard/components/WizardSnackbar.js +58 -0
  307. package/src/wizard/components/style.scss +48 -0
  308. package/src/wizard/index.js +152 -21
  309. package/src/wizard/store/index.js +28 -2
  310. package/dist/cjs/button-card/index.js +0 -72
  311. package/dist/cjs/button-card/style.scss +0 -180
  312. package/dist/esm/button-card/index.js +0 -64
  313. package/dist/esm/button-card/style.scss +0 -180
  314. package/src/button-card/index.js +0 -52
  315. package/src/button-card/style.scss +0 -180
@@ -0,0 +1,290 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import debounce from 'lodash/debounce';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import apiFetch from '@wordpress/api-fetch';
10
+ import { Button, CheckboxControl, FormTokenField, Spinner } from '@wordpress/components';
11
+ import { decodeEntities } from '@wordpress/html-entities';
12
+ import { useEffect, useState } from '@wordpress/element';
13
+ import { __ } from '@wordpress/i18n';
14
+ import { addQueryArgs } from '@wordpress/url';
15
+
16
+ import './style.scss';
17
+
18
+ const AutocompleteWithLatestPosts = ( {
19
+ fetchSuggestions = false, // If passed, will use this function to fetch suggestions data.
20
+ help = __( 'Begin typing search term, click autocomplete result to select.', 'newspack-plugin' ),
21
+ hideHelp = true, // If true, all help text will be hidden.
22
+ hideFormTokenHelp = true, // If true, hides FormTokenField's built-in help text.
23
+ label = __( 'Search', 'newspack-plugin' ),
24
+ maxItemsToSuggest = 0, // If passed, will be used to determine "load more" state. Necessary if you want "load more" functionality when using a custom `fetchSuggestions` function.
25
+ multiSelect = false, // If true, component can select multiple values at once.
26
+ onChange = false, // Function to call when selections change.
27
+ selectedItems = [], // Array of saved items.
28
+ suggestionsToFetch = 20, // Number of suggestions to fetch per query.
29
+ } ) => {
30
+ const [ isLoading, setIsLoading ] = useState( true );
31
+ const [ isLoadingMore, setIsLoadingMore ] = useState( false );
32
+ const [ suggestions, setSuggestions ] = useState( [] );
33
+ const [ maxSuggestions, setMaxSuggestions ] = useState( 0 );
34
+ const [ searchSuggestions, setSearchSuggestions ] = useState( [] );
35
+ const [ validValues, setValidValues ] = useState( {} );
36
+ const [ isSearching, setIsSearching ] = useState( false );
37
+ // Leaving this in here so we can expand to use different post types in the future; right now it defaults to 'post'.
38
+ const postTypeToSearch = 'post';
39
+ const classNames = [ 'newspack-autocomplete-with-latest-posts' ];
40
+
41
+ if ( hideFormTokenHelp ) {
42
+ classNames.push( 'hide-form-token-help' );
43
+ }
44
+
45
+ if ( hideHelp ) {
46
+ classNames.push( 'hide-help' );
47
+ }
48
+
49
+ /**
50
+ * Debounced function to update search suggestions.
51
+ */
52
+ const debouncedUpdateSuggestions = debounce( input => {
53
+ if ( ! input || input.length < 2 ) {
54
+ setSearchSuggestions( [] );
55
+ setIsSearching( false );
56
+ return;
57
+ }
58
+
59
+ setIsSearching( true );
60
+ handleFetchSuggestions( input, 0, postTypeToSearch )
61
+ .then( _suggestions => {
62
+ setSearchSuggestions( _suggestions );
63
+ // Update valid values for token conversion
64
+ const newValidValues = { ...validValues };
65
+ _suggestions.forEach( suggestion => {
66
+ newValidValues[ suggestion.value ] = suggestion.label;
67
+ } );
68
+ setValidValues( newValidValues );
69
+ } )
70
+ .finally( () => setIsSearching( false ) );
71
+ }, 500 );
72
+
73
+ /**
74
+ * Fetch recent posts to show as suggestions.
75
+ */
76
+ useEffect( () => {
77
+ setIsLoading( true );
78
+ handleFetchSuggestions( null, 0, postTypeToSearch )
79
+ .then( _suggestions => {
80
+ if ( 0 < _suggestions.length ) {
81
+ setSuggestions( _suggestions );
82
+ // Update valid values for token conversion
83
+ const newValidValues = { ...validValues };
84
+ _suggestions.forEach( suggestion => {
85
+ newValidValues[ suggestion.value ] = suggestion.label;
86
+ } );
87
+ setValidValues( newValidValues );
88
+ }
89
+ } )
90
+ .finally( () => setIsLoading( false ) );
91
+ }, [] );
92
+
93
+ /**
94
+ * Fetch more suggestions.
95
+ */
96
+ useEffect( () => {
97
+ if ( isLoadingMore ) {
98
+ handleFetchSuggestions( null, suggestions.length, postTypeToSearch )
99
+ .then( _suggestions => {
100
+ if ( 0 < _suggestions.length ) {
101
+ setSuggestions( suggestions.concat( _suggestions ) );
102
+ }
103
+ } )
104
+ .finally( () => setIsLoadingMore( false ) );
105
+ }
106
+ }, [ isLoadingMore ] );
107
+
108
+ /**
109
+ * If passed a `fetchSuggestions` prop, use that, otherwise, build it based on the selected post type.
110
+ */
111
+ const handleFetchSuggestions = fetchSuggestions
112
+ ? fetchSuggestions
113
+ : async ( search = null, offset = 0, searchSlug = null ) => {
114
+ const postTypeSlug = searchSlug || postTypeToSearch;
115
+ const endpoint =
116
+ 'post' === postTypeSlug || 'page' === postTypeSlug
117
+ ? postTypeSlug + 's' // Default post type endpoints are plural.
118
+ : postTypeSlug; // Custom post type endpoints are singular.
119
+ const response = await apiFetch( {
120
+ parse: false,
121
+ path: addQueryArgs( '/wp/v2/' + endpoint, {
122
+ search,
123
+ offset,
124
+ per_page: suggestionsToFetch,
125
+ _fields: 'id,title',
126
+ } ),
127
+ } );
128
+
129
+ const total = parseInt( response.headers.get( 'x-wp-total' ) || 0 );
130
+ const posts = await response.json();
131
+
132
+ setMaxSuggestions( total );
133
+
134
+ // Format suggestions for FormTokenField display.
135
+ return posts.reduce( ( acc, post ) => {
136
+ acc.push( {
137
+ value: parseInt( post.id ),
138
+ label: decodeEntities( post?.title.rendered ) || __( '(no title)', 'newspack-plugin' ),
139
+ } );
140
+
141
+ return acc;
142
+ }, [] );
143
+ };
144
+
145
+ /**
146
+ * Get labels for token values.
147
+ */
148
+ const getLabelsForValues = values => {
149
+ return values.reduce( ( accumulator, value ) => {
150
+ if ( ! value ) {
151
+ return accumulator;
152
+ }
153
+ if ( value.label ) {
154
+ return [ ...accumulator, value.label ];
155
+ }
156
+ return validValues[ value ] ? [ ...accumulator, validValues[ value ] ] : accumulator;
157
+ }, [] );
158
+ };
159
+
160
+ /**
161
+ * Get values for token labels.
162
+ */
163
+ const getValuesForLabels = labels => {
164
+ // eslint-disable-next-line @typescript-eslint/no-shadow
165
+ return labels.reduce( ( acc, label ) => {
166
+ Object.keys( validValues ).forEach( key => {
167
+ if ( validValues[ key ] === label ) {
168
+ const value = isNaN( parseInt( key ) ) ? key.toString() : parseInt( key );
169
+ acc.push( { value, label } );
170
+ }
171
+ } );
172
+ return acc;
173
+ }, [] );
174
+ };
175
+
176
+ /**
177
+ * Handle FormTokenField onChange.
178
+ */
179
+ const handleTokenChange = tokenStrings => {
180
+ const newSelections = getValuesForLabels( tokenStrings );
181
+
182
+ // If only allowing one selection, just return the one selected item.
183
+ if ( ! multiSelect ) {
184
+ const newSelection = newSelections[ newSelections.length - 1 ]; // Get the last selected item
185
+ if ( newSelection ) {
186
+ return onChange( [ { ...newSelection, postType: postTypeToSearch } ] );
187
+ }
188
+ return onChange( [] );
189
+ }
190
+
191
+ // For multi-select, include currently selected post type in selection results.
192
+ onChange( newSelections.map( selection => ( { ...selection, postType: postTypeToSearch } ) ) );
193
+ };
194
+
195
+ /**
196
+ * Get tokens for FormTokenField.
197
+ */
198
+ const getTokens = () => {
199
+ return getLabelsForValues( selectedItems );
200
+ };
201
+
202
+ /**
203
+ * Render a single suggestion object that can be clicked to select it immediately.
204
+ *
205
+ * @param {Object} suggestion Suggestion object with value and label keys.
206
+ */
207
+ const renderSuggestion = suggestion => {
208
+ if ( multiSelect ) {
209
+ const isSelected = !! selectedItems.find(
210
+ _selection => parseInt( _selection.value ) === parseInt( suggestion.value ) && _selection.label === suggestion.label
211
+ );
212
+ return (
213
+ <CheckboxControl
214
+ key={ suggestion.value }
215
+ checked={ isSelected }
216
+ onChange={ () => {
217
+ // For multi-select, we need to toggle the selection
218
+ const currentTokens = getTokens();
219
+ const suggestionLabel = suggestion.label;
220
+
221
+ if ( isSelected ) {
222
+ // Remove the suggestion
223
+ const newTokens = currentTokens.filter( token => token !== suggestionLabel );
224
+ handleTokenChange( newTokens );
225
+ } else {
226
+ // Add the suggestion
227
+ handleTokenChange( [ ...currentTokens, suggestionLabel ] );
228
+ }
229
+ } }
230
+ label={ suggestion.label }
231
+ />
232
+ );
233
+ }
234
+ return (
235
+ <Button isLink key={ suggestion.value } onClick={ () => handleTokenChange( [ ...getTokens(), suggestion.label ] ) }>
236
+ { suggestion.label }
237
+ </Button>
238
+ );
239
+ };
240
+
241
+ /**
242
+ * Render a list of suggestions that can be clicked to select instead of searching by title.
243
+ */
244
+ const renderSuggestions = () => {
245
+ if ( 0 === suggestions.length ) {
246
+ return null;
247
+ }
248
+
249
+ const className = multiSelect
250
+ ? 'newspack-autocomplete-with-latest-posts__search-suggestions-multiselect'
251
+ : 'newspack-autocomplete-with-latest-posts__search-suggestions';
252
+
253
+ return (
254
+ <>
255
+ <div className="newspack-autocomplete-with-latest-posts__search-suggestions-container">
256
+ <p className="newspack-autocomplete-with-suggestions__label">{ __( 'Latest Posts', 'newspack-plugin' ) }</p>
257
+ <div className={ className }>
258
+ { suggestions.map( renderSuggestion ) }
259
+ { suggestions.length < ( maxItemsToSuggest || maxSuggestions ) && (
260
+ <Button disabled={ isLoadingMore } isSecondary onClick={ () => setIsLoadingMore( true ) }>
261
+ { isLoadingMore ? __( 'Loading…', 'newspack-plugin' ) : __( 'Load more', 'newspack-plugin' ) }
262
+ </Button>
263
+ ) }
264
+ </div>
265
+ </div>
266
+ </>
267
+ );
268
+ };
269
+
270
+ return (
271
+ <div className={ classNames.join( ' ' ) }>
272
+ <div className="newspack-autocomplete-with-latest-posts__input-container">
273
+ <FormTokenField
274
+ value={ getTokens() }
275
+ suggestions={ searchSuggestions.map( suggestion => suggestion.label ) }
276
+ onChange={ handleTokenChange }
277
+ onInputChange={ debouncedUpdateSuggestions }
278
+ label={ label }
279
+ help={ ! hideHelp && help }
280
+ __next40pxDefaultSize={ true }
281
+ __nextHasNoMarginBottom={ true }
282
+ />
283
+ { ( isLoading || isSearching ) && <Spinner /> }
284
+ </div>
285
+ { renderSuggestions() }
286
+ </div>
287
+ );
288
+ };
289
+
290
+ export default AutocompleteWithLatestPosts;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Styles for AutocompleteWithLatestPosts component.
3
+ */
4
+
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+
7
+ .newspack-autocomplete-with-latest-posts {
8
+ display: grid;
9
+ gap: 16px;
10
+ width: 100%;
11
+
12
+ .components-select-control__input {
13
+ max-width: none;
14
+ }
15
+
16
+ .components-input-control__backdrop {
17
+ border-color: wp-colors.$gray-700 !important;
18
+ }
19
+
20
+ .newspack-autocomplete-with-latest-posts__label,
21
+ .components-form-token-field__label,
22
+ .components-input-control__label,
23
+ .components-checkbox-control__label {
24
+ color: wp-colors.$gray-900 !important;
25
+ }
26
+
27
+ .newspack-autocomplete-tokenfield__help {
28
+ font-size: inherit;
29
+ margin-bottom: 8px !important;
30
+ }
31
+
32
+ .components-form-token-field {
33
+ &__input-container {
34
+ border-color: wp-colors.$gray-700;
35
+ }
36
+
37
+ &__suggestion {
38
+ padding: 8px;
39
+ }
40
+ }
41
+
42
+ // Hide the built-in "Separate with commas or the Enter key." help text when hideFormTokenHelp is true
43
+ &.hide-form-token-help .components-form-token-field__help {
44
+ display: none;
45
+ }
46
+
47
+ p.newspack-autocomplete-with-latest-posts__label {
48
+ font-size: inherit;
49
+ margin: 0 0 8px;
50
+ }
51
+
52
+ &__selected-items {
53
+ border-bottom: 1px solid wp-colors.$gray-700;
54
+ display: flex;
55
+ flex-wrap: wrap;
56
+ gap: 8px;
57
+ margin-bottom: 16px;
58
+ padding-bottom: 12px;
59
+
60
+ p.newspack-autocomplete-with-latest-posts__label {
61
+ flex: 0 0 100%;
62
+ margin: 0;
63
+ }
64
+ }
65
+
66
+ &__selected-item-button.components-button.is-tertiary {
67
+ background-color: wp-colors.$gray-300;
68
+ border-radius: 2px;
69
+ color: wp-colors.$gray-900;
70
+ font-size: 13px;
71
+ height: auto;
72
+ margin: 0;
73
+ padding-left: 8px;
74
+ padding-right: 8px;
75
+
76
+ &::after {
77
+ content: "✕";
78
+ margin-left: 8px;
79
+ }
80
+
81
+ &:hover {
82
+ box-shadow: none;
83
+ color: black;
84
+ }
85
+ }
86
+
87
+ &__search-suggestions,
88
+ &__search-suggestions-multiselect {
89
+ max-height: 220px;
90
+ overflow: visible auto;
91
+ margin: 0 -4px;
92
+ padding: 4px 4px 0;
93
+
94
+ .components-button {
95
+ display: block;
96
+ line-height: normal;
97
+ margin: auto;
98
+
99
+ &.is-link {
100
+ color: wp-colors.$gray-900;
101
+ padding: 8px;
102
+ text-decoration: none;
103
+ width: 100%;
104
+
105
+ &:active:not(:disabled),
106
+ &:hover:not(:disabled) {
107
+ background: wp-colors.$gray-900;
108
+ background: var(--wp-admin-theme-color);
109
+ color: white;
110
+ }
111
+ }
112
+ &.is-secondary {
113
+ margin-bottom: 8px;
114
+ margin-top: 8px;
115
+ width: 100%;
116
+ }
117
+ }
118
+ }
119
+
120
+ &__search-suggestions {
121
+ border-radius: 2px;
122
+ border: 1px solid wp-colors.$gray-700;
123
+ padding: 0;
124
+
125
+ .components-button {
126
+ line-height: normal;
127
+
128
+ &:focus:not(:disabled) {
129
+ border-radius: 0;
130
+ box-shadow: inset 0 0 0 1.5px wp-colors.$gray-900;
131
+ box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color);
132
+ }
133
+
134
+ &.is-secondary {
135
+ width: calc(100% - 16px);
136
+ }
137
+ }
138
+ }
139
+
140
+ // Temp: CSS work-around.
141
+ .newspack-autocomplete-with-suggestions__label {
142
+ font-size: 11px;
143
+ font-weight: 500;
144
+ line-height: 1.4;
145
+ text-transform: uppercase;
146
+ display: block;
147
+ margin: 0 0 8px;
148
+ padding: 0;
149
+ }
150
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import './style.scss';
5
+
6
+ /**
7
+ * External dependencies
8
+ */
9
+ import classnames from 'classnames';
10
+
11
+ export type BadgeLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
12
+
13
+ type BadgeProps = {
14
+ text: string;
15
+ level?: BadgeLevel;
16
+ };
17
+
18
+ /**
19
+ * Badge component
20
+ */
21
+ const Badge = ( { text, level = 'default' }: BadgeProps ) => {
22
+ const classes = classnames( 'newspack-badge', `is-${ level }` );
23
+ return <span className={ classes }>{ text }</span>;
24
+ };
25
+
26
+ export default Badge;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Badge
3
+ */
4
+
5
+ @use "~@wordpress/base-styles/colors" as wp-colors;
6
+ @use "../../../colors/colors.module" as colors;
7
+
8
+ $badge-colors: (
9
+ "info": colors.$primary-600,
10
+ "warning": wp-colors.$alert-yellow,
11
+ "error": wp-colors.$alert-red,
12
+ "success": wp-colors.$alert-green,
13
+ );
14
+
15
+ .newspack-badge {
16
+ background-color: color-mix(in srgb, wp-colors.$white 90%, var(--base-color));
17
+ border-radius: 2px;
18
+ color: color-mix(in srgb, wp-colors.$black 50%, var(--base-color));
19
+ display: block;
20
+ flex: 0 0 auto;
21
+ font-size: 12px;
22
+ line-height: 1.5;
23
+ padding: 1px 4px;
24
+
25
+ &:where(.is-default) {
26
+ background-color: wp-colors.$gray-100;
27
+ color: wp-colors.$gray-800;
28
+ }
29
+
30
+ // Generate color variants
31
+ @each $type, $color in $badge-colors {
32
+ &.is-#{$type} {
33
+ --base-color: #{$color};
34
+ }
35
+ }
36
+ }
@@ -19,10 +19,11 @@ const { useHistory } = Router;
19
19
  type OriginalButtonProps = typeof BaseComponent.defaultProps;
20
20
  type Props = OriginalButtonProps & {
21
21
  href?: string;
22
+ loading?: boolean;
22
23
  onClick?: () => void;
23
24
  };
24
25
 
25
- const Button = ( { href, onClick, ...otherProps }: Props ) => {
26
+ const Button = ( { href, loading = undefined, onClick, ...otherProps }: Props ) => {
26
27
  const history = useHistory();
27
28
  const [ isAwaitingOnClick, setIsAwaitingOnClick ] = useState( false );
28
29
 
@@ -42,7 +43,7 @@ const Button = ( { href, onClick, ...otherProps }: Props ) => {
42
43
  otherProps.disabled = true;
43
44
  }
44
45
  // @ts-expect-error - @wordpress/components' Button can only have either href or onClick, not both.
45
- return <BaseComponent { ...otherProps } />;
46
+ return <BaseComponent loading={ loading ? true : undefined } { ...otherProps } />;
46
47
  };
47
48
 
48
49
  export default Button;