blueprint gallery integration (#50)

* blueprint gallery feature added

* Created utils.js for common functions and integrated it into OpenJson and Gallery components to streamline JSON handling and validation

* fixed gallery UI

* fixed Open and Gallery button UI in sidebar
This commit is contained in:
Cy-Yaksh 2025-01-21 23:44:40 +05:30 committed by Ajit Bohra
parent 44ce7661ea
commit a8828ed90a
5 changed files with 251 additions and 78 deletions

View file

@ -134,4 +134,10 @@ body.editor-styles-wrapper {
}
.dataforms-layouts-regular__field{
min-height: 32px!important
}
}
.blueprint_gallery_json {
box-shadow: inset 0 0 0 1px #ccc;
width: 100%;
justify-content: center;
position: relative;
}

View file

@ -0,0 +1,144 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import {
Button,
Modal,
Card,
CardBody,
__experimentalText as Text,
__experimentalHeading as Heading,
__experimentalGrid as Grid,
__experimentalVStack as VStack,
} from '@wordpress/components';

/**
* Internal dependencies
*/
import { handleBlueprintData } from './utils';

function Gallery({ onSubmitData }) {
const { createNotice } = useDispatch(noticesStore);
const [isModalOpen, setModalOpen] = useState(false);
const [blueprintList, setBlueprintList] = useState(null);


/**
* Fetches the list of blueprints from the remote JSON file.
*/
useEffect(() => {
const fetchBlueprintList = async () => {
const apiUrl = 'https://raw.githubusercontent.com/WordPress/blueprints/trunk/index.json';
try {
const response = await fetch(apiUrl);
if (!response.ok) throw new Error('Failed to fetch blueprints');
const data = await response.json();
setBlueprintList(data);
} catch (error) {
createNotice('error', __('Error fetching blueprint list:', error, 'wp-playground-blueprint-editor'));
}
};

fetchBlueprintList();
}, []);

/**
* Fetches details for a selected blueprint.
* @param {string} blueprintName - The name of the blueprint to fetch.
*/
const fetchBlueprintDetails = async (blueprintName) => {
const blueprintUrl = `https://raw.githubusercontent.com/WordPress/blueprints/trunk/${blueprintName}`;
try {
const response = await fetch(blueprintUrl);
if (!response.ok) throw new Error(`Failed to fetch blueprint details: ${response.statusText}`);
const data = await response.json();

// Replace 'mu-plugins' with 'plugins' in the blueprint data
const updatedSteps = data.steps.map((step) => {
if (step.path?.includes('mu-plugins')) {
return { ...step, path: step.path.replace('mu-plugins', 'plugins') };
}
return step;
});
handleBlueprintData({ ...data, steps: updatedSteps }, createNotice, onSubmitData);
} catch (error) {
createNotice('error', __('Error fetching blueprint from Gallery', 'wp-playground-blueprint-editor') + `: ${error.message}`);
}
};

return (
<>
{/* Open modal button */}
<Button
className='blueprint_gallery_json'
__next40pxDefaultSize
onClick={() => setModalOpen(true)}>
{__('Gallery', 'wp-playground-blueprint-editor')}
</Button>

{/* Blueprint gallery modal */}
{isModalOpen && (
<Modal
title={__('Blueprint Gallery', 'wp-playground-blueprint-editor')}
onRequestClose={() => setModalOpen(false)}
shouldCloseOnClickOutside
shouldCloseOnEsc
size="large"
>
{blueprintList ? (
<Grid columns={2} gap={6}>
{Object.entries(blueprintList).map(([blueprintName, blueprintDetails], index) => (
<Card
key={index}
elevation={3}
>
<CardBody style={{ height: '100%', justifyContent: 'space-between', display: 'flex', flexDirection: 'column' }}>
{/* Blueprint Info */}
<VStack align='start' spacing={4} >
<Heading level={4}>
{blueprintDetails.title}
</Heading>
<Text>
{__('By', 'wp-playground-blueprint-editor')} {blueprintDetails.author}
</Text>
<Text
lineHeight={'1.5em'}
size={15}
color='#777'
>
{blueprintDetails.description}
</Text>
</VStack>

{/* Action Button */}
<Button
variant="secondary"
style={{
borderRadius: '4px',
alignSelf: 'flex-end',
}}
onClick={() => fetchBlueprintDetails(blueprintName)}
>
{__('Import', 'wp-playground-blueprint-editor')}
</Button>
</CardBody>
</Card>
))}
</Grid>
) : (
<Text>{__('Loading blueprints...', 'wp-playground-blueprint-editor')}</Text>
)}
</Modal>
)}
</>
);
}

export default Gallery;

View file

@ -1,45 +1,12 @@
import { __ } from '@wordpress/i18n';
import { createBlock } from '@wordpress/blocks';
import { dispatch, useDispatch } from '@wordpress/data';
import { useDispatch } from '@wordpress/data';
import { FormFileUpload, DropZone } from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
import {handleBlueprintData} from './utils'

const OpenJson = ({ onSubmitData }) => {
const { createNotice } = useDispatch(noticesStore);

// Utility to convert camelCase to kebab-case with special handling for "WordPress"
const convertToKebabCase = (str) => {
return str
.replace(/WordPress/g, 'Wordpress') // Temporarily normalize "WordPress" casing
.replace(/([a-z])([A-Z])/g, '$1-$2') // Convert camelCase to kebab-case
.replace(/Wordpress/g, 'wordpress') // Convert "Wordpress" back to "wordpress"
.toLowerCase(); // Convert the entire string to lowercase
};

// Validate steps from the JSON schema
const validateBlueprintSteps = (steps) => {
const validBlocks = [];
const invalidSteps = [];

steps.forEach((step, index) => {
const blockType = `playground-step/${convertToKebabCase(step.step)}`;
try {
const block = createBlock(blockType, step || {});
validBlocks.push(block);
} catch (error) {
invalidSteps.push({
stepIndex: index + 1,
stepData: step,
error: error.message,
});
}
});

return { validBlocks, invalidSteps };
};

// Process JSON file content
const processJsonFile = (file) => {
if (file.type === 'application/json') {
@ -47,7 +14,7 @@ const OpenJson = ({ onSubmitData }) => {
reader.onload = () => {
try {
const jsonData = JSON.parse(reader.result);
handleBlueprintData(jsonData);
handleBlueprintData(jsonData , createNotice, onSubmitData);
}
catch (err) {
createNotice('error', __('Invalid JSON file.', 'wp-playground-blueprint-editor'));
@ -59,33 +26,6 @@ const OpenJson = ({ onSubmitData }) => {
}
};

// Handle JSON data processing
const handleBlueprintData = (jsonData) => {
if (!jsonData) {
createNotice('error', __('Invalid blueprint schema.', 'wp-playground-blueprint-editor'));
return;
}

const { steps } = jsonData;
const { validBlocks, invalidSteps } = validateBlueprintSteps(steps);

if (validBlocks.length > 0) {
dispatch('core/block-editor').insertBlocks(validBlocks);
createNotice('success', __('Blueprint imported successfully.', 'wp-playground-blueprint-editor'));
}

if (invalidSteps.length > 0) {
const errorDetails = invalidSteps
.map(({ stepIndex, stepData, error }) => `Step ${stepIndex}: ${stepData.step} (${error})`)
.join(', ');
createNotice('warning', __(`Some steps are invalid: ${errorDetails}.`, 'wp-playground-blueprint-editor'));
}

if (onSubmitData) {
onSubmitData(jsonData);
}
};

// Handle file selection from input
const handleFileSelection = (event) => {
const file = event.target.files[0];
@ -110,7 +50,7 @@ const OpenJson = ({ onSubmitData }) => {
accept="application/json"
onChange={handleFileSelection}
>
{__('Open Blueprint', 'wp-playground-blueprint-editor')}
{__('Open', 'wp-playground-blueprint-editor')}
<DropZone
onFilesDrop={handleFileDrop}
accept="application/json"

View file

@ -15,6 +15,8 @@ import {
Toolbar,
ToolbarButton,
ToggleControl,
Flex,
FlexBlock,
__experimentalVStack as VStack,
__experimentalHStack as HStack,
__experimentalText as Text,
@ -25,6 +27,7 @@ import {
*/
import OpenJson from './open-json';
import { PHP_VERSIONS, WP_VERSIONS, PLAYGROUND_BASE, PLAYGROUND_BUILDER_BASE, PLAYGROUND_BLUEPRINT_SCHEMA_URL } from './constant';
import Gallery from './blueprint-gallery';
import SiteOptionsSettings from './site-options-settings';

/**
@ -81,9 +84,9 @@ function BlueprintSidebarSettings() {
try {
const preparedSchema = prepareSchema();
downloadBlob('playground-blueprint.json', preparedSchema, 'application/json');
createSuccessNotice(__('Blueprint downloaded successfully!','wp-playground-blueprint-editor'), { type: 'snackbar' });
createSuccessNotice(__('Blueprint downloaded successfully!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
} catch (error) {
createErrorNotice(__('Failed to download Blueprint JSON.','wp-playground-blueprint-editor'));
createErrorNotice(__('Failed to download Blueprint JSON.', 'wp-playground-blueprint-editor'));
}
};

@ -92,10 +95,10 @@ function BlueprintSidebarSettings() {
*/
const handleCopy = useCopyToClipboard(() => {
if (!schema.steps.length) {
createErrorNotice(__('No Blueprint steps to copy!','wp-playground-blueprint-editor'));
createErrorNotice(__('No Blueprint steps to copy!', 'wp-playground-blueprint-editor'));
return ''; // Return empty string for invalid data
}
createSuccessNotice(__('Blueprint schema copied to clipboard!','wp-playground-blueprint-editor'), { type: 'snackbar' });
createSuccessNotice(__('Blueprint schema copied to clipboard!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
return prepareSchema();
});

@ -114,7 +117,7 @@ function BlueprintSidebarSettings() {
*/
const handleJsonDataSubmit = (data) => {
if (!data) {
createErrorNotice(__('Failed to update Blueprint configuration.','wp-playground-blueprint-editor'));
createErrorNotice(__('Failed to update Blueprint configuration.', 'wp-playground-blueprint-editor'));
return;
}
updateBlueprintConfig({
@ -127,20 +130,27 @@ function BlueprintSidebarSettings() {
siteOptions: data.siteOptions || undefined,
extra_libraries: data.extraLibraries || undefined,
});
createSuccessNotice(__('Blueprint configuration updated successfully!','wp-playground-blueprint-editor'), { type: 'snackbar' });
createSuccessNotice(__('Blueprint configuration updated successfully!', 'wp-playground-blueprint-editor'), { type: 'snackbar' });
};

return (
<>
<PluginPostStatusInfo>
<VStack spacing={5} style={{ width: '100%' }}>
<OpenJson onSubmitData={handleJsonDataSubmit} />
<VStack spacing={5} style={{ width: '100%' }}>
<Flex>
<FlexBlock>
<OpenJson onSubmitData={handleJsonDataSubmit} />
</FlexBlock>
<FlexBlock>
<Gallery onSubmitData={handleJsonDataSubmit} />
</FlexBlock>
</Flex>
<Toolbar style={{ justifyContent: 'space-between' }}>
<ToolbarButton icon={globe} label={__('Open in Playground', 'wp-playground-blueprint-editor')} href={PLAYGROUND_BASE + prepareSchema()} target="_blank" />
<ToolbarButton icon={download} label={__('Download JSON', 'wp-playground-blueprint-editor')} onClick={handleDownload} />
<ToolbarButton icon={copy} label={__('Copy JSON', 'wp-playground-blueprint-editor')} ref={handleCopy} />
<ToolbarButton icon={code} label={__('Open in Builder', 'wp-playground-blueprint-editor')} href={PLAYGROUND_BUILDER_BASE + prepareSchema()} target="_blank" />
</Toolbar>
<ToolbarButton icon={globe} label={__('Open in Playground', 'wp-playground-blueprint-editor')} href={PLAYGROUND_BASE + prepareSchema()} target="_blank" />
<ToolbarButton icon={download} label={__('Download JSON', 'wp-playground-blueprint-editor')} onClick={handleDownload} />
<ToolbarButton icon={copy} label={__('Copy JSON', 'wp-playground-blueprint-editor')} ref={handleCopy} />
<ToolbarButton icon={code} label={__('Open in Builder', 'wp-playground-blueprint-editor')} href={PLAYGROUND_BUILDER_BASE + prepareSchema()} target="_blank" />
</Toolbar>
</VStack>
</PluginPostStatusInfo>
<PluginDocumentSettingPanel name='playground-settings' title={__('Playground Settings', 'wp-playground-blueprint-editor')}>

73
src/editor/utils.js Normal file
View file

@ -0,0 +1,73 @@
import { __ } from '@wordpress/i18n';
import { createBlock } from "@wordpress/blocks";
import { dispatch } from '@wordpress/data';
/**
* Utility function to convert a camelCase string to kebab-case with special handling for "WordPress".
*
*/
export const convertToKebabCase = (str) => {
return str
.replace(/WordPress/g, 'Wordpress') // Temporarily normalize "WordPress" casing
.replace(/([a-z])([A-Z])/g, '$1-$2') // Convert camelCase to kebab-case
.replace(/Wordpress/g, 'wordpress') // Convert "Wordpress" back to "wordpress"
.toLowerCase(); // Convert the entire string to lowercase
};

/**
* Validates steps from a JSON schema and creates valid WordPress blocks.
*
*/
export const validateBlueprintSteps = (steps) => {
const validBlocks = [];
const invalidSteps = [];

steps.forEach((step, index) => {
const blockType = `playground-step/${convertToKebabCase(step.step)}`;
try {
const block = createBlock(blockType, step || {});
validBlocks.push(block);
} catch (error) {
invalidSteps.push({
stepIndex: index + 1,
stepData: step,
error: error.message,
});
}
});

return { validBlocks, invalidSteps };
};

/**
* Processes JSON blueprint data, validates it, and inserts valid blocks into the WordPress editor.
*
* jsonData - The parsed JSON data from the openJson and Gallery.
* createNotice - Function to create notices in the WordPress admin.
* onSubmitData - Optional callback for additional data handling.
*/
export const handleBlueprintData = (jsonData, createNotice, onSubmitData) => {
if (!jsonData) {
createNotice('error', __('Invalid blueprint schema.', 'wp-playground-blueprint-editor'));
return;
}

const { steps } = jsonData;
const { validBlocks, invalidSteps } = validateBlueprintSteps(steps);

if (validBlocks.length > 0) {
dispatch('core/block-editor').insertBlocks(validBlocks);
createNotice('success', __('Blueprint imported successfully.', 'wp-playground-blueprint-editor'));
}

if (invalidSteps.length > 0) {
const errorDetails = invalidSteps
.map(({ stepIndex, stepData, error }) => `Step ${stepIndex}: ${stepData.step} (${error})`)
.join(', ');
createNotice('warning', __(`Some steps are invalid: ${errorDetails}.`, 'wp-playground-blueprint-editor'));
}

if (onSubmitData) {
onSubmitData(jsonData);
}
};