Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 (
<Dropdown
popoverProps={ { placement: 'bottom-start' } }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,51 @@
/**
* WordPress dependencies
*/
import { useState, useRef } from '@wordpress/element';
import { Button, Notice } from '@wordpress/components';
import { withInstanceId } from '@wordpress/compose';
import { useRef, useState } from '@wordpress/element';
import { __, _x } from '@wordpress/i18n';
import { Button, Notice } from '@wordpress/components';

/**
* Internal dependencies
*/
import importReusableBlock from '../../utils/import';
import type { ImportFormProps } from '../../utils/types';

function ImportForm( { instanceId, onUpload } ) {
function ImportForm( { instanceId, onUpload }: ImportFormProps ) {
const inputId = 'list-reusable-blocks-import-form-' + instanceId;

const formRef = useRef();
const formRef = useRef< HTMLFormElement >( 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( { isLoading: true } );
setIsLoading( true );
importReusableBlock( file )
.then( ( reusableBlock ) => {
if ( ! formRef ) {
.then( ( reusableBlock: any ) => {
if ( ! formRef.current ) {
return;
}

setIsLoading( false );
onUpload( reusableBlock );
} )
.catch( ( errors ) => {
if ( ! formRef ) {
.catch( ( errors: any ) => {
if ( ! formRef.current ) {
return;
}

let uiMessage;
let uiMessage: string;
switch ( errors.message ) {
case 'Invalid JSON file':
uiMessage = __( 'Invalid JSON file' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -34,14 +39,19 @@ document.addEventListener( 'DOMContentLoaded', () => {
notice.innerHTML = `<p>${ __( 'Pattern imported successfully!' ) }</p>`;

const headerEnd = document.querySelector( '.wp-header-end' );
if ( ! headerEnd ) {
if ( ! headerEnd || ! headerEnd.parentNode ) {
return;
}
headerEnd.parentNode.insertBefore( notice, headerEnd );
};

const container = document.createElement( 'div' );
container.className = 'list-reusable-blocks__container';

if ( ! button.parentNode ) {
return;
}

button.parentNode.insertBefore( container, button );
createRoot( container ).render(
<StrictMode>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,37 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies
*/
import { downloadBlob } from '@wordpress/blob';
import type { ExportedBlock, Post, PostType } from './types';
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved

/**
* 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',
title,
content,
syncStatus,
},
} as ExportedBlock,
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved
null,
2
);

const fileName = kebabCase( title ) + '.json';

downloadBlob( fileName, fileContent, 'application/json' );
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* Reads the textual content of the given file.
*
* @param {File} file File.
* @return {Promise<string>} 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 );
} );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
parsedContent = JSON.parse( fileContent ) as ParsedContent;
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved
} catch ( e ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch ( e ) {
} catch {

throw new Error( 'Invalid JSON file' );
}

if (
parsedContent.__file !== 'wp_block' ||
! parsedContent.title ||
Expand All @@ -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,
Expand All @@ -44,7 +56,7 @@ async function importReusableBlock( file ) {
parsedContent.syncStatus === 'unsynced'
? { wp_pattern_sync_status: parsedContent.syncStatus }
: undefined,
},
} as ReusableBlockData,
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved
method: 'POST',
} );

Expand Down
76 changes: 76 additions & 0 deletions packages/list-reusable-blocks/src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
interface ImportDropdownProps {
onUpload: ( data: any ) => void;
}

interface ImportFormProps {
instanceId: string | number;
onUpload: ( reusableBlock: any ) => void;

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type definitions use any type which defeats the purpose of TypeScript migration. The onUpload callback should have a properly typed parameter instead of any.

Suggested change
onUpload: ( data: any ) => void;
}
interface ImportFormProps {
instanceId: string | number;
onUpload: ( reusableBlock: any ) => void;
onUpload: ( data: ReusableBlockData ) => void;
}
interface ImportFormProps {
instanceId: string | number;
onUpload: ( reusableBlock: ReusableBlockData ) => void;

Copilot uses AI. Check for mistakes.

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type definitions use any type which defeats the purpose of TypeScript migration. The onUpload callback should have a properly typed parameter instead of any.

Suggested change
onUpload: ( data: any ) => void;
}
interface ImportFormProps {
instanceId: string | number;
onUpload: ( reusableBlock: any ) => void;
onUpload: ( data: ReusableBlockData ) => void;
}
interface ImportFormProps {
instanceId: string | number;
onUpload: ( reusableBlock: ReusableBlockData ) => void;

Copilot uses AI. Check for mistakes.
}

interface PostType {
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved
rest_base: string;
[ key: string ]: any;

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using index signatures [ key: string ]: any weakens type safety. Consider defining specific properties or using a more restrictive approach if these extra properties are truly needed.

Copilot uses AI. Check for mistakes.
}

interface Post {
title: {
raw: string;
};
content: {
raw: string;
};
wp_pattern_sync_status: string;
[ key: string ]: any;

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using index signatures [ key: string ]: any weakens type safety. Consider defining specific properties or using a more restrictive approach if these extra properties are truly needed.

Copilot uses AI. Check for mistakes.
}

interface ExportedBlock {
__file: string;
title: string;
content: string;
syncStatus: string;
}

interface ParsedContent {
__file: string;
title: string;
content: string;
syncStatus?: string;
[ key: string ]: any;

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using index signatures [ key: string ]: any weakens type safety. Consider defining specific properties or using a more restrictive approach if these extra properties are truly needed.

Suggested change
[ key: string ]: any;
[ key: string ]: unknown;

Copilot uses AI. Check for mistakes.
}

interface ReusableBlockMeta {
wp_pattern_sync_status?: string;
}

interface ReusableBlockData {
title: string;
content: string;
status: string;
meta?: ReusableBlockMeta;
}

interface ReusableBlock {
id: number;
title: {
raw: string;
rendered: string;
};
content: {
raw: string;
rendered: string;
};
status: string;
[ key: string ]: any;

Copilot AI Jan 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using index signatures [ key: string ]: any weakens type safety. Consider defining specific properties or using a more restrictive approach if these extra properties are truly needed.

Suggested change
[ key: string ]: any;
[ key: string ]: unknown;

Copilot uses AI. Check for mistakes.
}

export type {
ExportedBlock,
ImportDropdownProps,
ImportFormProps,
ParsedContent,
Post,
PostType,
ReusableBlock,
ReusableBlockData,
ReusableBlockMeta,
};
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved
12 changes: 12 additions & 0 deletions packages/list-reusable-blocks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "../../tsconfig.base.json",
"references": [
{ "path": "../i18n" },
{ "path": "../compose" },
{ "path": "../components" },
{ "path": "../api-fetch" },
{ "path": "../blob" },
{ "path": "../element" }
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved
]
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
{ "path": "packages/is-shallow-equal" },
{ "path": "packages/keycodes" },
{ "path": "packages/lazy-import" },
{ "path": "packages/list-reusable-blocks" },
Comment thread
Infinite-Null marked this conversation as resolved.
Resolved
{ "path": "packages/media-utils" },
{ "path": "packages/notices" },
{ "path": "packages/plugins" },
Expand Down
Loading