Created
October 8, 2025 18:07
-
-
Save Micemade/8ad0c334ba8845e5cc75bc51cef06552 to your computer and use it in GitHub Desktop.
Persisting block attributes using WP settings API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Component for displaying notices (missing in Gist):