@automattic/jetpack-shared-extension-utils 0.16.5 → 0.17.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 CHANGED
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.17.0] - 2025-02-05
9
+ ### Changed
10
+ - External Media: Move the GooglePhotosMedia, OpenverseMedia, PexelsMedia to @automattic/jetpack-shared-extension-utils [#41078]
11
+ - Updated package dependencies. [#41491] [#41577]
12
+
8
13
  ## [0.16.5] - 2025-01-27
9
14
  ### Changed
10
15
  - Internal updates.
@@ -523,6 +528,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
523
528
  ### Changed
524
529
  - Core: prepare utility for release
525
530
 
531
+ [0.17.0]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.16.5...0.17.0
526
532
  [0.16.5]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.16.4...0.16.5
527
533
  [0.16.4]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.16.3...0.16.4
528
534
  [0.16.3]: https://github.com/Automattic/jetpack-shared-extension-utils/compare/0.16.2...0.16.3
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './src/block-icons';
1
2
  export { default as getJetpackData, JETPACK_DATA_PATH } from './src/get-jetpack-data';
2
3
  export { default as getSiteFragment } from './src/get-site-fragment';
3
4
  export * from './src/site-type-utils';
@@ -15,9 +16,12 @@ export {
15
16
  } from './src/plan-utils';
16
17
  export { default as isCurrentUserConnected } from './src/is-current-user-connected';
17
18
  export { default as useAnalytics } from './src/hooks/use-analytics';
19
+ export { default as useAutosaveAndRedirect } from './src/hooks/use-autosave-and-redirect';
20
+ export * from './src/hooks/use-plan-type';
21
+ export { default as useRefInterval } from './src/hooks/use-ref-interval';
18
22
  export { default as useModuleStatus } from './src/hooks/use-module-status';
19
- export { default as JetpackEditorPanelLogo } from './src/components/jetpack-editor-panel-logo';
20
23
  export { getBlockIconComponent, getBlockIconProp } from './src/get-block-icon-from-metadata';
21
24
  export { default as getJetpackBlocksVariation } from './src/get-jetpack-blocks-variation';
22
25
  export * from './src/modules-state';
23
26
  export { default as isMyJetpackAvailable } from './src/is-my-jetpack-available';
27
+ export * from './src/libs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/jetpack-shared-extension-utils",
3
- "version": "0.16.5",
3
+ "version": "0.17.0",
4
4
  "description": "Utility functions used by the block editor extensions",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/shared-extension-utils/#readme",
6
6
  "bugs": {
@@ -18,16 +18,22 @@
18
18
  "test-coverage": "pnpm run test --coverage"
19
19
  },
20
20
  "dependencies": {
21
+ "@automattic/color-studio": "4.0.0",
21
22
  "@automattic/jetpack-analytics": "^0.1.35",
22
- "@automattic/jetpack-components": "^0.65.4",
23
- "@automattic/jetpack-connection": "^0.36.4",
24
- "@wordpress/api-fetch": "7.16.0",
25
- "@wordpress/compose": "7.16.0",
26
- "@wordpress/data": "10.16.0",
27
- "@wordpress/element": "6.16.0",
28
- "@wordpress/i18n": "5.16.0",
29
- "@wordpress/plugins": "7.16.0",
30
- "@wordpress/url": "4.16.0",
23
+ "@automattic/jetpack-base-styles": "^0.6.42",
24
+ "@automattic/jetpack-components": "^0.66.0",
25
+ "@automattic/jetpack-connection": "^0.36.5",
26
+ "@wordpress/api-fetch": "7.17.0",
27
+ "@wordpress/block-editor": "14.12.0",
28
+ "@wordpress/components": "29.3.0",
29
+ "@wordpress/compose": "7.17.0",
30
+ "@wordpress/data": "10.17.0",
31
+ "@wordpress/element": "6.17.0",
32
+ "@wordpress/i18n": "5.17.0",
33
+ "@wordpress/plugins": "7.17.0",
34
+ "@wordpress/url": "4.17.0",
35
+ "clsx": "2.1.1",
36
+ "debug": "4.4.0",
31
37
  "lodash": "4.17.21"
32
38
  },
33
39
  "devDependencies": {
@@ -35,10 +41,11 @@
35
41
  "@babel/core": "7.26.0",
36
42
  "@babel/plugin-transform-react-jsx": "7.25.9",
37
43
  "@babel/preset-react": "7.26.3",
44
+ "@babel/runtime": "7.26.0",
38
45
  "@testing-library/dom": "10.4.0",
39
- "@testing-library/react": "16.0.1",
40
- "@testing-library/user-event": "14.5.2",
41
- "@wordpress/babel-plugin-import-jsx-pragma": "5.16.0",
46
+ "@testing-library/react": "16.2.0",
47
+ "@testing-library/user-event": "14.6.1",
48
+ "@wordpress/babel-plugin-import-jsx-pragma": "5.17.0",
42
49
  "babel-jest": "29.3.1",
43
50
  "jest": "29.7.0",
44
51
  "jest-environment-jsdom": "29.7.0",
@@ -47,6 +54,10 @@
47
54
  "react-dom": "18.3.1"
48
55
  },
49
56
  "exports": {
50
- ".": "./index.js"
57
+ ".": "./index.js",
58
+ "./components": "./src/components/index.js",
59
+ "./icons": "./src/icons.js",
60
+ "./store/wordpress-com": "./src/store/wordpress-com/index.ts",
61
+ "./store/wordpress-com/types": "./src/store/wordpress-com/types.ts"
51
62
  }
52
63
  }
@@ -0,0 +1,25 @@
1
+ import colorStudio from '@automattic/color-studio';
2
+ import { isAtomicSite, isSimpleSite } from './site-type-utils';
3
+
4
+ /**
5
+ * Constants
6
+ */
7
+ const PALETTE = colorStudio.colors;
8
+ const COLOR_JETPACK = PALETTE[ 'Jetpack Green 40' ];
9
+
10
+ /**
11
+ * Returns the icon color for Jetpack blocks.
12
+ *
13
+ * Green in the Jetpack context, otherwise black for Simple sites or Atomic sites.
14
+ *
15
+ * @return {string} HEX color for block editor icons
16
+ */
17
+ export function getIconColor() {
18
+ if ( isAtomicSite() || isSimpleSite() ) {
19
+ // Return null to match core block styling
20
+ return null;
21
+ }
22
+
23
+ // Jetpack Green
24
+ return COLOR_JETPACK;
25
+ }
@@ -0,0 +1,2 @@
1
+ export { default as JetpackEditorPanelLogo } from './jetpack-editor-panel-logo';
2
+ export { Nudge } from './upgrade-nudge';
@@ -0,0 +1,59 @@
1
+ import { Button } from '@wordpress/components';
2
+ import { __ } from '@wordpress/i18n';
3
+ import clsx from 'clsx';
4
+ import React from 'react';
5
+
6
+ import './style.scss';
7
+
8
+ export const Nudge = ( {
9
+ className,
10
+ description,
11
+ align = null,
12
+ title = null,
13
+ buttonText = null,
14
+ visible = true,
15
+ context = null,
16
+ checkoutUrl = null,
17
+ goToCheckoutPage = null,
18
+ isRedirecting = false,
19
+ showButton = true,
20
+ target = '_top',
21
+ } ) => {
22
+ const cssClasses = clsx( className, 'jetpack-upgrade-plan-banner', {
23
+ 'wp-block': context === 'editor-canvas',
24
+ 'block-editor-block-list__block': context === 'editor-canvas',
25
+ 'jetpack-upgrade-plan__hidden': ! visible,
26
+ } );
27
+
28
+ const redirectingText = __( 'Redirecting…', 'jetpack-shared-extension-utils' );
29
+
30
+ return (
31
+ <div className={ cssClasses } data-align={ align }>
32
+ <div className="jetpack-upgrade-plan-banner__wrapper">
33
+ { title && (
34
+ <strong className={ clsx( 'banner-title', { [ `${ className }__title` ]: className } ) }>
35
+ { title }
36
+ </strong>
37
+ ) }
38
+ { description && (
39
+ <span className={ `${ className }__description banner-description` }>
40
+ { description }
41
+ </span>
42
+ ) }
43
+ { showButton && (
44
+ <Button
45
+ href={ isRedirecting ? null : checkoutUrl } // Only for server-side rendering, since onClick doesn't work there.
46
+ onClick={ goToCheckoutPage }
47
+ target={ target }
48
+ className={ clsx( 'is-primary', {
49
+ 'jetpack-upgrade-plan__hidden': ! checkoutUrl,
50
+ } ) }
51
+ isBusy={ isRedirecting }
52
+ >
53
+ { isRedirecting ? redirectingText : buttonText }
54
+ </Button>
55
+ ) }
56
+ </div>
57
+ </div>
58
+ );
59
+ };
@@ -0,0 +1,42 @@
1
+ @import '@automattic/jetpack-base-styles/gutenberg-base-styles';
2
+ @import "@automattic/color-studio/dist/color-variables";
3
+
4
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper {
5
+ display: flex;
6
+ justify-content: space-between;
7
+ align-items: center;
8
+ font-size: 14px;
9
+ background: $studio-black;
10
+ padding: 0 20px;
11
+ border-radius: 2px;
12
+ box-shadow: 0 0 1px inset $studio-white; }
13
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .banner-title,
14
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .banner-description {
15
+ color: $studio-white; }
16
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .jetpack-upgrade-plan-banner__title,
17
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .jetpack-upgrade-plan-banner__description {
18
+ margin-right: 10px; }
19
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .components-button {
20
+ flex-shrink: 0;
21
+ line-height: 1;
22
+ margin-left: auto;
23
+ height: 28px; }
24
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .components-button.is-primary {
25
+ background: $studio-pink-40;
26
+ color: $studio-white; }
27
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .components-button.is-primary:hover {
28
+ background: $studio-pink-30; }
29
+ .jetpack-upgrade-plan-banner .jetpack-upgrade-plan-banner__wrapper .components-button.is-primary.is-busy {
30
+ background-size: 100px 100%;
31
+ background-image: linear-gradient(-45deg, $studio-pink-40 28%, $studio-pink-60 28%, $studio-pink-60 72%, $studio-pink-40 72%); }
32
+
33
+ .jetpack-upgrade-plan-banner.block-editor-block-list__block {
34
+ margin-top: 0;
35
+ margin-bottom: 0;
36
+ }
37
+
38
+ .jetpack-upgrade-plan-banner.wp-block[data-align=right] .jetpack-upgrade-plan-banner__wrapper,
39
+ .jetpack-upgrade-plan-banner.wp-block[data-align=left] .jetpack-upgrade-plan-banner__wrapper {
40
+ max-width: 580px;
41
+ width: 100%;
42
+ }
@@ -0,0 +1,54 @@
1
+ ## useAutosaveAndRedirect hook
2
+
3
+ Use this hook to implement autosave and redirect functionality that works in both the block and site editor.
4
+
5
+ ### Usage
6
+
7
+ ```es6
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import useAutosaveAndRedirect from '../../shared/use-autosave-and-redirect/index';
12
+
13
+ const myComponent = ( myUrl ) => {
14
+ const [ autosave, autosaveAndRedirect, isRedirecting ] = useAutosaveAndRedirect( myUrl );
15
+ return (
16
+ <Button href={ myUrl } onClick={ autosaveAndRedirect } isBusy={ isRedirecting }>
17
+ Checkout
18
+ </Button>
19
+ );
20
+ };
21
+ ```
22
+
23
+ ### API
24
+
25
+ `const { autosave, autosaveAndRedirect, isRedirecting } = useAutosaveAndRedirect( redirectUrl, onRedirect );`
26
+
27
+ #### Arguments
28
+
29
+ The hook accepts two arguments.
30
+
31
+ - `redirectUrl` (`string`) - URL to redirect to after saving.
32
+ - _(optional)_ `onRedirect` (`(string) => void`) - callback function that will
33
+ be run when the redirect process triggers. The URL is passed.
34
+
35
+ ### Return Values
36
+
37
+ The hook returns an array with three items.
38
+
39
+
40
+ - `autosave` (`(event) => void`): Callback to be used in an onClick event.
41
+
42
+ Checks whether the current post/page/etc has changes to save and saves them. If
43
+ in the site editor, entities are saved. This callback can be used when a redirect
44
+ is not required (for example if an action is performed in a modal).
45
+
46
+ - `autosaveAndRedirect` (`(event) => void`): Callback to be used in an onClick event.
47
+
48
+ Redirects the user to the redirectURL, checking before whether the current
49
+ post/page/etc has changes to save. If so, it saves them before redirecting. If
50
+ in the site editor, entities are saved.
51
+
52
+ - `isRedirecting` (`bool`): If the component is in the process of redirecting the
53
+ user. It may be waiting for a save to complete before redirecting. Use
54
+ this to set a button as busy or in a loading state.
@@ -0,0 +1,103 @@
1
+ import { useSelect, dispatch } from '@wordpress/data';
2
+ import { useState } from '@wordpress/element';
3
+ import { noop } from 'lodash';
4
+
5
+ /**
6
+ * To handle the redirection
7
+ * @param {string} url - The redirect URL.
8
+ * @param {Function} callback - The callback of the redirection.
9
+ * @param {boolean} shouldOpenNewWindow - Whether to open the new window.
10
+ * @return {Window | null} - The open window.
11
+ */
12
+ function redirect( url, callback, shouldOpenNewWindow = false ) {
13
+ if ( callback ) {
14
+ callback( url );
15
+ }
16
+
17
+ return shouldOpenNewWindow ? window.open( url, '_blank' ) : ( window.top.location.href = url );
18
+ }
19
+
20
+ /**
21
+ * Hook to get properties for AiImage
22
+ *
23
+ * @param {string} redirectUrl - The redirect URL.
24
+ * @param {Function} onRedirect - To handle the redirection.
25
+ * @return {object} - Object containing properties to handle autosave and redirect.
26
+ */
27
+ export default function useAutosaveAndRedirect( redirectUrl, onRedirect = noop ) {
28
+ const [ isRedirecting, setIsRedirecting ] = useState( false );
29
+
30
+ const { isAutosaveablePost, isDirtyPost, currentPost } = useSelect( select => {
31
+ const editorSelector = select( 'core/editor' );
32
+
33
+ return {
34
+ isAutosaveablePost: editorSelector.isEditedPostAutosaveable(),
35
+ isDirtyPost: editorSelector.isEditedPostDirty(),
36
+ currentPost: editorSelector.getCurrentPost(),
37
+ };
38
+ }, [] );
39
+
40
+ const isPostEditor = Object.keys( currentPost ).length > 0;
41
+
42
+ const isWidgetEditor = useSelect( select => {
43
+ if ( window.wp?.customize ) {
44
+ return true;
45
+ }
46
+
47
+ return !! select( 'core/edit-widgets' );
48
+ } );
49
+
50
+ // Alias. Save post by dispatch.
51
+ const savePost = dispatch( 'core/editor' ).savePost;
52
+
53
+ // For the site editor, save entities
54
+ const entityRecords = useSelect( select => {
55
+ return select( 'core' ).__experimentalGetDirtyEntityRecords();
56
+ } );
57
+
58
+ // Save
59
+ const saveEntities = async () => {
60
+ for ( let i = 0; i < entityRecords.length; i++ ) {
61
+ // await is needed here due to the loop.
62
+ await dispatch( 'core' ).saveEditedEntityRecord(
63
+ entityRecords[ i ].kind,
64
+ entityRecords[ i ].name,
65
+ entityRecords[ i ].key
66
+ );
67
+ }
68
+ };
69
+
70
+ const autosave = async event => {
71
+ event.preventDefault();
72
+
73
+ if ( isPostEditor ) {
74
+ /**
75
+ * If there are not unsaved values, return.
76
+ * If the post is not auto-savable, return.
77
+ */
78
+ if ( isDirtyPost && isAutosaveablePost ) {
79
+ await savePost( event );
80
+ }
81
+ } else {
82
+ // Save entities in the site editor.
83
+ await saveEntities( event );
84
+ }
85
+ };
86
+
87
+ const autosaveAndRedirect = async event => {
88
+ event.preventDefault();
89
+
90
+ // Lock re-redirecting attempts.
91
+ if ( isRedirecting ) {
92
+ return;
93
+ }
94
+
95
+ setIsRedirecting( true );
96
+
97
+ autosave( event ).then( () => {
98
+ redirect( redirectUrl, onRedirect, isWidgetEditor );
99
+ } );
100
+ };
101
+
102
+ return { autosave, autosaveAndRedirect, isRedirecting };
103
+ }
@@ -0,0 +1,27 @@
1
+ export const PLAN_TYPE_FREE = 'free';
2
+ export const PLAN_TYPE_TIERED = 'tiered';
3
+ export const PLAN_TYPE_UNLIMITED = 'unlimited';
4
+
5
+ export type PlanType = typeof PLAN_TYPE_FREE | typeof PLAN_TYPE_TIERED | typeof PLAN_TYPE_UNLIMITED;
6
+
7
+ /**
8
+ * Simple hook to get the plan type from the current tier
9
+ *
10
+ * @param {object} currentTier - the current tier from the AI Feature data
11
+ * @return {PlanType} the plan type
12
+ */
13
+ export const usePlanType = ( currentTier ): PlanType => {
14
+ if ( ! currentTier ) {
15
+ return null;
16
+ }
17
+
18
+ if ( currentTier?.value === 0 ) {
19
+ return PLAN_TYPE_FREE;
20
+ }
21
+
22
+ if ( currentTier?.value === 1 ) {
23
+ return PLAN_TYPE_UNLIMITED;
24
+ }
25
+
26
+ return PLAN_TYPE_TIERED;
27
+ };
@@ -0,0 +1,67 @@
1
+ import { useCallback, useEffect, useRef } from '@wordpress/element';
2
+
3
+ interface RafHandle {
4
+ id: number;
5
+ }
6
+
7
+ const setRafInterval = ( callback: () => void, timeout: number = 0 ) => {
8
+ const interval = timeout < 0 ? 0 : timeout;
9
+ const handle: RafHandle = {
10
+ id: 0,
11
+ };
12
+
13
+ let startTime = Date.now();
14
+
15
+ const loop = () => {
16
+ const nowTime = Date.now();
17
+ if ( nowTime - startTime >= interval ) {
18
+ startTime = nowTime;
19
+ callback();
20
+ }
21
+
22
+ handle.id = requestAnimationFrame( loop );
23
+ };
24
+
25
+ handle.id = requestAnimationFrame( loop );
26
+
27
+ return handle;
28
+ };
29
+
30
+ const clearRafInterval = ( handle?: RafHandle | null ) => {
31
+ if ( handle ) {
32
+ cancelAnimationFrame( handle.id );
33
+ }
34
+ };
35
+
36
+ /**
37
+ * Invoke a function on an interval that uses requestAnimationFrame.
38
+ *
39
+ * @param {Function} callback - Function to invoke
40
+ * @param {number} timeout - Interval timout in MS.
41
+ *
42
+ * @return {Function} Function to clear the interval.
43
+ */
44
+ const useRafInterval = ( callback: () => void, timeout = 0 ) => {
45
+ const timerRef = useRef< RafHandle >();
46
+
47
+ const callbackRef = useRef( callback );
48
+ callbackRef.current = callback;
49
+
50
+ useEffect( () => {
51
+ timerRef.current = setRafInterval( () => {
52
+ callbackRef.current();
53
+ }, timeout );
54
+
55
+ return () => {
56
+ clearRafInterval( timerRef.current );
57
+ };
58
+ }, [ timeout ] );
59
+
60
+ const clear = useCallback( () => {
61
+ clearRafInterval( timerRef.current );
62
+ }, [] );
63
+
64
+ return clear;
65
+ };
66
+
67
+ export default useRafInterval;