Skip to content

Instantly share code, notes, and snippets.

@Micemade
Created October 8, 2025 18:07
Show Gist options
  • Select an option

  • Save Micemade/8ad0c334ba8845e5cc75bc51cef06552 to your computer and use it in GitHub Desktop.

Select an option

Save Micemade/8ad0c334ba8845e5cc75bc51cef06552 to your computer and use it in GitHub Desktop.
Persisting block attributes using WP settings API
/**
* WordPress dependecies.
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import {
Placeholder,
Spinner,
Button,
Modal,
TextControl,
ButtonGroup,
Icon,
Dropdown,
Flex,
BaseControl,
Tooltip,
CardDivider,
CheckboxControl
} from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
import { cleanForSlug } from '@wordpress/url';
import { starFilled, chevronUp, chevronDown, helpFilled } from "@wordpress/icons";
/**
* Internal dependencies.
*/
import showNotice from '../utils/showNotice';
/**
* Component for managing plugin settings and layouts.
*
* @param {Object} props - Component properties
* @param {string} props.context - The context where the settings are being rendered. Defaults to 'InspectorControls'.
* @param {string} props.blockType - The type of block being edited (single product, products grid, or categories grid)
* @param {Object} props.attributes - Block attributes containing layout and style settings
* @param {Function} props.setAttributes - Function to update block attributes
* @return {JSX.Element} The rendered plugin settings component
*/
const SaveCurrentSetup = ({ context = 'InspectorControls', blockType, attributes, setAttributes }) => {
// If block settings are for a single product block.
const isBlockTypeSingle = blockType === 'mosaic_product_layouts_single_setups' ?? false;
// If block settings are for a products/categories grid.
const isBlockTypeGrid = (blockType === 'mosaic_product_layouts_products_setups' || blockType === 'mosaic_product_layouts_categories_setups') ?? false;
const {
savedLayouts,
itemZindexes,
gridSettings,
productElementSettings,
featuredImageSize,
grouped,
itemStyleOverrides // Products and categories grid only.
} = attributes;
const [userSetups, setUserSetups] = useState([]);
const [isAPILoaded, setIsAPILoaded] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newSetupName, setNewSetupName] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
const fetchSettings = async () => {
try {
const response = await apiFetch({ path: '/wp/v2/settings' });
const serialzedSetups = response[blockType];
setIsAPILoaded(true);
setUserSetups(serialzedSetups ? JSON.parse(serialzedSetups) : []);
} catch (error) {
console.error('Failed to fetch settings:', error);
setIsAPILoaded(true);
setUserSetups([]);
}
};
fetchSettings();
}, [blockType]);
/**
* Saves the user's settings to the database.
*
* @return {void} No value is returned.
*/
const handleSave = async () => {
try {
await apiFetch({
path: '/wp/v2/settings',
method: 'POST',
data: {
[blockType]: JSON.stringify(userSetups),
},
});
showNotice(__('Layout settings saved successfully!', 'mosaic-product-layouts'));
} catch (error) {
showNotice(__('Failed to save layout settings.', 'mosaic-product-layouts'), 'error');
console.error('Save error:', error);
}
};
/**
* Handles saving a layout setup with the provided name and current settings.
* If a setup with the same name exists, prompts for confirmation before overwriting.
* Updates the userSetups state and triggers a save to the database.
*
* @return {void} No value is returned.
*/
const handleSetupSave = () => {
if (!newSetupName) return;
const idFromNameSlug = cleanForSlug(newSetupName);
const newSetup = {
id: idFromNameSlug,
name: newSetupName,
layout: savedLayouts,
zIndex: itemZindexes,
...gridSettings && { grid: gridSettings },
prodElSettings: productElementSettings,
...(itemStyleOverrides && isBlockTypeGrid) && { overrides: itemStyleOverrides }, // Products and categories grid only.
featImgSize: featuredImageSize,
group: grouped
};
// Check if setup with same name already exists.
const existingSetup = userSetups.find((setup) => setup.id === idFromNameSlug);
if (existingSetup) {
if (!confirm(`Setup with name "${newSetupName}" already exists. Overwrite it?`)) return;
setNewSetupName('');
}
setSaving(true);
// Update existing setup or add new setup.
setUserSetups(
existingSetup ?
userSetups.map((setup) => setup.id === idFromNameSlug ? newSetup : setup) :
[...userSetups, newSetup]);
// setNewSetupName('');
showNotice(
existingSetup
? __('Layout setup updated successfully!', 'mosaic-product-layouts')
: __('New layout setup saved successfully!', 'mosaic-product-layouts')
);
setIsModalOpen(false);
};
useEffect(() => {
if (!userSetups || userSetups === null) {
setUserSetups([]);
}
if (!saving) return;
handleSave();
setSaving(false);
}, [userSetups, saving]);
// REMOVING SETUPS WITH CONFIRMATION.
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [layoutToDelete, setLayoutToDelete] = useState(null);
const shouldShowDeleteConfirm = () => {
const skipConfirm = localStorage.getItem('mpl_skip_delete_confirm');
return skipConfirm !== 'true';
};
/**
* Checks if delete confirmation should be shown based on localStorage setting.
*
* @returns {boolean} True if delete confirmation should be shown, false otherwise.
*/
const handleRemoveSetup = (idToRemove) => {
if (shouldShowDeleteConfirm()) {
setLayoutToDelete(idToRemove);
setShowDeleteConfirm(true);
} else {
deleteLayout(idToRemove);
}
};
/**
* Deletes a layout setup by ID.
*
* @param {string} idToRemove The ID of the layout setup to delete.
* @returns {void}
*/
const deleteLayout = (idToRemove) => {
try {
setSaving(true);
const updatedUserSetups = userSetups.filter((setup) => setup.id !== idToRemove);
setUserSetups(updatedUserSetups);
setNewSetupName('');
showNotice(__('Layout setup removed successfully!', 'mosaic-product-layouts'));
} catch (error) {
showNotice(__('Failed to remove layout setup.', 'mosaic-product-layouts'), 'error');
console.error('Remove error:', error);
}
};
/**
* Sets the block attributes for the selected setup.
*
* @param {{ layout: string, zIndex: string, prodElSettings: string, overrides: string, featImgSize: string, group: string }} setup The selected setup.
*/
const onChangeSetup = ({ layout, zIndex, grid, prodElSettings, overrides, featImgSize, group }) => {
setAttributes({
...overrides && { itemStyleOverrides: overrides },
savedLayouts: layout,
itemZindexes: zIndex,
...grid && { gridSettings: grid },
productElementSettings: prodElSettings,
...featImgSize && { featuredImageSize: featImgSize },
...group && { grouped: group }
})
};
// Component styles.
const dropDownBorder = context === 'BlockControls' ? '1px solid #000' : '1px solid #ccc';
const dropdownToggleStyle = { height: '46px', padding: '6px 0', cursor: 'pointer', [context === 'BlockControls' ? 'borderRight' : 'border']: dropDownBorder, padding: '0 8px', background: '#fff' };
const dropDownStyle = { width: context === 'InspectorControls' ? '100%' : '', zIndex: 9999 }
const label = __('Select or save setups', 'mosaic-product-layouts');
if (!isAPILoaded) {
return (
<Placeholder>
<Spinner />
</Placeholder>
);
}
return (
<BaseControl
label={
<Flex>
{__('My layout and style setups', 'mosaic-product-layouts')}
<Tooltip text={__('Save current layout and style settings for later usage.', 'mosaic-product-layouts')}>
<div className="mosaic-product-layouts-help-icon">{helpFilled}</div>
</Tooltip>
</Flex>
}
__nextHasNoMarginBottom
>
<Dropdown
className="mosaic-product-layouts-popover"
contentClassName="mosaic-product-layouts__popover-content"
style={dropDownStyle}
headerTitle={label}
popoverProps={{ placement: 'bottom' }}
renderToggle={({ isOpen, onToggle, onClose }) => (
<>
{context === 'InspectorControls' && (
<Flex
align="center"
justify="space-between"
style={dropdownToggleStyle}
onClick={onToggle}
aria-expanded={isOpen}
>
<Flex align="center" justify="start"><Icon icon={starFilled} />{label}</Flex>
<span>
<Icon icon={isOpen ? chevronUp : chevronDown} />
</span>
</Flex>
)}
{context === 'BlockControls' && (
<Tooltip text={label} delay={0}>
<ToolbarGroup>
<Button
aria-expanded={isOpen}
onClick={onToggle}
icon={starFilled}
/>
</ToolbarGroup>
</Tooltip>
)}
</>
)}
renderContent={({ isOpen, onToggle, onClose }) => (
<>
{context === 'InspectorControls' && (
<BaseControl label={userSetups && userSetups.length ? __('Saved setups', 'mosaic-product-layouts') : __('No saved setups. Click on the button bellow to save current layout and style.', 'mosaic-product-layouts')} />
)}
<ButtonGroup
className="switch-button-group"
children={
userSetups && (userSetups.map((setup) => {
const setupOptions = {
itemsInactive: setup.itemsInactive ?? null,
layout: setup.layout ?? null,
zIndex: setup.zIndex ?? null,
grid: setup.grid ?? null,
prodElSettings: setup.prodElSettings ?? null,
overrides: setup.overrides ?? null,
featImgSize: setup.featImgSize ?? null,
group: setup.group ?? null
}
return (
<div className='button-wrap'>
<Button
key={setup.id}
className='setup-button'
variant="primary"
size='small'
text={setup.name}
onClick={() => {
setNewSetupName(setup.name);
onChangeSetup(setupOptions);
}}
/>
<Icon icon={'no'} onClick={() => handleRemoveSetup(setup.id)} />
</div>
)
}))}
/>
<CardDivider margin='10px' />
<Button variant="primary" size='default' onClick={() => setIsModalOpen(true)}>
{__('Save current layout and style', 'mosaic-product-layouts')}
</Button>
<div onClick={onClose}></div>
</>
)}
/>
{isModalOpen && (
<Modal
title={__('Save current layout and style', 'mosaic-product-layouts')}
onRequestClose={() => setIsModalOpen(false)}
>
<TextControl
label={newSetupName ? __('Current setup:', 'mosaic-product-layouts') : __('Enter a name to save this setup.', 'mosaic-product-layouts')}
value={newSetupName}
onChange={(value) => setNewSetupName(value)}
/>
<Button variant="primary" onClick={handleSetupSave}>{__('Save', 'mosaic-product-layouts')}</Button>
</Modal>
)}
{showDeleteConfirm && (
<Modal
title={__('Confirm Deletion', 'mosaic-product-layouts')}
onRequestClose={() => {
setShowDeleteConfirm(false);
setLayoutToDelete(null);
}}
>
<p>{__('Are you sure you want to delete this saved layout?', 'mosaic-product-layouts')}</p>
<CheckboxControl
label={__('Don\'t show this confirmation again', 'mosaic-product-layouts')}
onChange={(checked) => {
localStorage.setItem('mpl_skip_delete_confirm', checked);
}}
/>
<Flex justify="flex-end">
<Button
variant="secondary"
onClick={() => {
setShowDeleteConfirm(false);
setLayoutToDelete(null);
}}
>
{__('Cancel', 'mosaic-product-layouts')}
</Button>
<Button
variant="primary"
onClick={() => {
deleteLayout(layoutToDelete);
setShowDeleteConfirm(false);
setLayoutToDelete(null);
}}
>
{__('Delete', 'mosaic-product-layouts')}
</Button>
</Flex>
</Modal>
)}
</BaseControl>
);
};
export default SaveCurrentSetup;
@Micemade
Copy link
Author

Micemade commented Nov 10, 2025

Component for displaying notices (missing in Gist):

import { dispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';

/**
 * Shows a notice message in the WordPress admin interface.
 *
 * @param {string} message - The message to display in the notice.
 * @param {number} [timeout=2000] - Time in milliseconds before the notice is automatically dismissed.
 * @param {string} [type='success'] - The type of notice to show ('success' or 'error').
 * @return {Promise<void>} A promise that resolves when the notice is created.
 */
const showNotice = async (message, timeout = 2000, type = 'success') => {
	const { createSuccessNotice, createErrorNotice, removeNotice } = dispatch(noticesStore);

	const noticePromise = type === 'success'
		? createSuccessNotice(message, {
			type: 'snackbar',
			isDismissible: true,
		})
		: createErrorNotice(message, {
			type: 'snackbar',
			isDismissible: true,
		});

	const result = await noticePromise;
	const noticeId = result.notice.id;

	// Force dismiss after 2 seconds
	setTimeout(() => {
		removeNotice(noticeId);
	}, timeout);
};

export default showNotice;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment