diff --git a/package-lock.json b/package-lock.json index 8ab1e6e1c17f04..3c859e5f04bc47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60779,6 +60779,7 @@ "@wordpress/blob": "file:../blob", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", + "@wordpress/core-data": "file:../core-data", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "change-case": "^4.1.2" diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index b34b568e3b455e..a2f41f10a039ef 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -35,6 +35,7 @@ "module": "build-module/index.mjs", "exports": { ".": { + "types": "./build-types/index.d.ts", "import": "./build-module/index.mjs", "require": "./build/index.cjs" }, @@ -42,12 +43,14 @@ "./build-style/": "./build-style/" }, "wpScript": true, + "types": "build-types", "dependencies": { "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/base-styles": "file:../base-styles", "@wordpress/blob": "file:../blob", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", + "@wordpress/core-data": "file:../core-data", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "change-case": "^4.1.2" diff --git a/packages/list-reusable-blocks/src/components/import-dropdown/index.js b/packages/list-reusable-blocks/src/components/import-dropdown/index.tsx similarity index 80% rename from packages/list-reusable-blocks/src/components/import-dropdown/index.js rename to packages/list-reusable-blocks/src/components/import-dropdown/index.tsx index fdad08f80d213c..ea3759e0345992 100644 --- a/packages/list-reusable-blocks/src/components/import-dropdown/index.js +++ b/packages/list-reusable-blocks/src/components/import-dropdown/index.tsx @@ -1,16 +1,17 @@ /** * WordPress dependencies */ +import { Button, Dropdown } from '@wordpress/components'; import { pipe } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; -import { Dropdown, Button } from '@wordpress/components'; /** * Internal dependencies */ +import type { ImportDropdownProps } from '../../utils/types'; import ImportForm from '../import-form'; -function ImportDropdown( { onUpload } ) { +function ImportDropdown( { onUpload }: ImportDropdownProps ) { return ( ( null ); const [ isLoading, setIsLoading ] = useState( false ); - const [ error, setError ] = useState( null ); - const [ file, setFile ] = useState( null ); + const [ error, setError ] = useState< string | null >( null ); + const [ file, setFile ] = useState< File | null >( null ); - const onChangeFile = ( event ) => { - setFile( event.target.files[ 0 ] ); + const onChangeFile = ( event: React.ChangeEvent< HTMLInputElement > ) => { + setFile( event.target.files?.[ 0 ] || null ); setError( null ); }; - const onSubmit = ( event ) => { + const onSubmit = ( event: React.FormEvent< HTMLFormElement > ) => { event.preventDefault(); if ( ! file ) { return; } setIsLoading( true ); importReusableBlock( file ) - .then( ( reusableBlock ) => { - if ( ! formRef ) { + .then( ( reusableBlock: ReusableBlock ) => { + if ( ! formRef.current ) { return; } setIsLoading( false ); onUpload( reusableBlock ); } ) - .catch( ( errors ) => { - if ( ! formRef ) { + .catch( ( errors: Error ) => { + if ( ! formRef.current ) { return; } - let uiMessage; + let uiMessage: string; switch ( errors.message ) { case 'Invalid JSON file': uiMessage = __( 'Invalid JSON file' ); diff --git a/packages/list-reusable-blocks/src/index.js b/packages/list-reusable-blocks/src/index.tsx similarity index 75% rename from packages/list-reusable-blocks/src/index.js rename to packages/list-reusable-blocks/src/index.tsx index f5cb411726963c..398197a39f5ff1 100644 --- a/packages/list-reusable-blocks/src/index.js +++ b/packages/list-reusable-blocks/src/index.tsx @@ -7,18 +7,23 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import exportReusableBlock from './utils/export'; import ImportDropdown from './components/import-dropdown'; +import exportReusableBlock from './utils/export'; // Setup Export Links. -document.body.addEventListener( 'click', ( event ) => { - if ( - ! event.target.classList.contains( 'wp-list-reusable-blocks__export' ) - ) { +document.body.addEventListener( 'click', ( event: MouseEvent ) => { + const target = event.target as HTMLElement; + + if ( ! target.classList.contains( 'wp-list-reusable-blocks__export' ) ) { return; } + event.preventDefault(); - exportReusableBlock( event.target.dataset.id ); + + const blockId = target.dataset.id; + if ( blockId ) { + exportReusableBlock( Number( blockId ) ); + } } ); // Setup Import Form. @@ -34,7 +39,7 @@ document.addEventListener( 'DOMContentLoaded', () => { notice.innerHTML = `

${ __( 'Pattern imported successfully!' ) }

`; const headerEnd = document.querySelector( '.wp-header-end' ); - if ( ! headerEnd ) { + if ( ! headerEnd || ! headerEnd.parentNode ) { return; } headerEnd.parentNode.insertBefore( notice, headerEnd ); @@ -42,6 +47,11 @@ document.addEventListener( 'DOMContentLoaded', () => { const container = document.createElement( 'div' ); container.className = 'list-reusable-blocks__container'; + + if ( ! button.parentNode ) { + return; + } + button.parentNode.insertBefore( container, button ); createRoot( container ).render( diff --git a/packages/list-reusable-blocks/src/utils/export.js b/packages/list-reusable-blocks/src/utils/export.ts similarity index 71% rename from packages/list-reusable-blocks/src/utils/export.js rename to packages/list-reusable-blocks/src/utils/export.ts index 4d7e76afcaa8fd..e58dea3d89605c 100644 --- a/packages/list-reusable-blocks/src/utils/export.js +++ b/packages/list-reusable-blocks/src/utils/export.ts @@ -12,20 +12,26 @@ import apiFetch from '@wordpress/api-fetch'; * Internal dependencies */ import { downloadBlob } from '@wordpress/blob'; +import type { Post, PostType } from './types'; /** * Export a reusable block as a JSON file. * - * @param {number} id + * @param id - The ID of the reusable block to export */ -async function exportReusableBlock( id ) { - const postType = await apiFetch( { path: `/wp/v2/types/wp_block` } ); - const post = await apiFetch( { +async function exportReusableBlock( id: number ): Promise< void > { + const postType = await apiFetch< PostType >( { + path: `/wp/v2/types/wp_block`, + } ); + + const post = await apiFetch< Post >( { path: `/wp/v2/${ postType.rest_base }/${ id }?context=edit`, } ); + const title = post.title.raw; const content = post.content.raw; const syncStatus = post.wp_pattern_sync_status; + const fileContent = JSON.stringify( { __file: 'wp_block', @@ -36,6 +42,7 @@ async function exportReusableBlock( id ) { null, 2 ); + const fileName = kebabCase( title ) + '.json'; downloadBlob( fileName, fileContent, 'application/json' ); diff --git a/packages/list-reusable-blocks/src/utils/file.js b/packages/list-reusable-blocks/src/utils/file.ts similarity index 51% rename from packages/list-reusable-blocks/src/utils/file.js rename to packages/list-reusable-blocks/src/utils/file.ts index d31e54ae41896a..71d4a03e41c6f1 100644 --- a/packages/list-reusable-blocks/src/utils/file.js +++ b/packages/list-reusable-blocks/src/utils/file.ts @@ -1,14 +1,14 @@ /** * Reads the textual content of the given file. * - * @param {File} file File. - * @return {Promise} Content of the file. + * @param file - File to read + * @return Promise that resolves to the content of the file */ -export function readTextFile( file ) { +export function readTextFile( file: File ): Promise< string > { const reader = new window.FileReader(); return new Promise( ( resolve ) => { reader.onload = () => { - resolve( reader.result ); + resolve( reader.result as string ); }; reader.readAsText( file ); } ); diff --git a/packages/list-reusable-blocks/src/utils/import.js b/packages/list-reusable-blocks/src/utils/import.ts similarity index 68% rename from packages/list-reusable-blocks/src/utils/import.js rename to packages/list-reusable-blocks/src/utils/import.ts index adc3f457474e32..c85f9aa5e4361a 100644 --- a/packages/list-reusable-blocks/src/utils/import.js +++ b/packages/list-reusable-blocks/src/utils/import.ts @@ -7,21 +7,29 @@ import apiFetch from '@wordpress/api-fetch'; * Internal dependencies */ import { readTextFile } from './file'; +import type { + ParsedContent, + PostType, + ReusableBlock, + ReusableBlockData, +} from './types'; /** * Import a reusable block from a JSON file. * - * @param {File} file File. - * @return {Promise} Promise returning the imported reusable block. + * @param file - File to import + * @return Promise returning the imported reusable block */ -async function importReusableBlock( file ) { +async function importReusableBlock( file: File ): Promise< ReusableBlock > { const fileContent = await readTextFile( file ); - let parsedContent; + let parsedContent: ParsedContent; + try { parsedContent = JSON.parse( fileContent ); } catch { throw new Error( 'Invalid JSON file' ); } + if ( parsedContent.__file !== 'wp_block' || ! parsedContent.title || @@ -33,8 +41,12 @@ async function importReusableBlock( file ) { ) { throw new Error( 'Invalid pattern JSON file' ); } - const postType = await apiFetch( { path: `/wp/v2/types/wp_block` } ); - const reusableBlock = await apiFetch( { + + const postType = await apiFetch< PostType >( { + path: `/wp/v2/types/wp_block`, + } ); + + const reusableBlock = await apiFetch< ReusableBlock >( { path: `/wp/v2/${ postType.rest_base }`, data: { title: parsedContent.title, @@ -44,7 +56,7 @@ async function importReusableBlock( file ) { parsedContent.syncStatus === 'unsynced' ? { wp_pattern_sync_status: parsedContent.syncStatus } : undefined, - }, + } satisfies ReusableBlockData, method: 'POST', } ); diff --git a/packages/list-reusable-blocks/src/utils/types.ts b/packages/list-reusable-blocks/src/utils/types.ts new file mode 100644 index 00000000000000..b076191326944b --- /dev/null +++ b/packages/list-reusable-blocks/src/utils/types.ts @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import type { Post as CorePost, Type } from '@wordpress/core-data'; + +export interface ImportDropdownProps { + onUpload: ( data: ReusableBlock ) => void; +} + +export interface ImportFormProps { + instanceId: string | number; + onUpload: ( reusableBlock: ReusableBlock ) => void; +} + +export type PostType = Type; + +export type Post = CorePost & { wp_pattern_sync_status?: string }; + +export interface ParsedContent { + __file: string; + title: string; + content: string; + syncStatus?: string; + [ key: string ]: unknown; +} + +export interface ReusableBlockMeta { + wp_pattern_sync_status?: string; +} + +export interface ReusableBlockData { + title: string; + content: string; + status: string; + meta?: ReusableBlockMeta; +} + +export interface ReusableBlock { + id: number; + title: { + raw: string; + rendered: string; + }; + content: { + raw: string; + rendered: string; + }; + status: string; + [ key: string ]: unknown; +} diff --git a/packages/list-reusable-blocks/tsconfig.json b/packages/list-reusable-blocks/tsconfig.json new file mode 100644 index 00000000000000..8f14eee8dfb48a --- /dev/null +++ b/packages/list-reusable-blocks/tsconfig.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "references": [ + { "path": "../api-fetch" }, + { "path": "../blob" }, + { "path": "../compose" }, + { "path": "../components" }, + { "path": "../core-data" }, + { "path": "../element" }, + { "path": "../i18n" } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 9f4dce3ff440af..05569fea216f1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,6 +49,7 @@ { "path": "packages/latex-to-mathml" }, { "path": "packages/lazy-editor" }, { "path": "packages/lazy-import" }, + { "path": "packages/list-reusable-blocks" }, { "path": "packages/media-editor" }, { "path": "packages/media-fields" }, { "path": "packages/media-utils" },