diff --git a/src/block/design-library/edit.js b/src/block/design-library/edit.js index 2f4643b07c..71a300db50 100644 --- a/src/block/design-library/edit.js +++ b/src/block/design-library/edit.js @@ -116,7 +116,7 @@ const Edit = props => { const spacingSize = ! presetMarks || ! Array.isArray( presetMarks ) ? 120 : presetMarks[ presetMarks.length - 2 ].value // Replaces the current block with a block made out of attributes. - const createBlockWithAttributes = async ( category, blockName, attributes, innerBlocks, substituteBlocks, parentClientId ) => { + const createBlockWithAttributes = async ( category, blockName, attributes, innerBlocks, substituteBlocks, parentClientId, type ) => { const disabledBlocks = settings.stackable_block_states || {} // eslint-disable-line camelcase // Recursively substitute core blocks to disabled Stackable blocks @@ -207,7 +207,7 @@ const Edit = props => { innerBlocks = block[ 0 ].innerBlocks const isDesignLibraryDevMode = devMode && localStorage.getItem( 'stk__design_library__dev_mode' ) === '1' - if ( ! isDesignLibraryDevMode ) { + if ( ! isDesignLibraryDevMode && type !== 'saved' ) { if ( category !== 'Header' ) { if ( ! parentClientId && attributes.hasBackground ) { attributes.blockMargin = { @@ -255,14 +255,16 @@ const Edit = props => { const blocks = [] for ( const blockDesign of designs ) { - const { designData, category } = blockDesign + const { + designData, category, type, + } = blockDesign for ( const patterns of designData ) { const { name, attributes, innerBlocks, } = patterns if ( name && attributes ) { - const block = await createBlockWithAttributes( category, name, applyFilters( 'stackable.design-library.attributes', attributes ), innerBlocks || [], substituteBlocks, parentClientId ) + const block = await createBlockWithAttributes( category, name, applyFilters( 'stackable.design-library.attributes', attributes ), innerBlocks || [], substituteBlocks, parentClientId, type ) blocks.push( block ) } else { console.error( 'Design library selection failed: No block data found' ) // eslint-disable-line no-console @@ -336,14 +338,16 @@ const Edit = props => { _designs.forEach( design => { const { - designData, blocksForSubstitution, category, + designData, blocksForSubstitution, category, type: designType, } = design if ( blocksForSubstitution.size ) { disabledBlocks = disabledBlocks.union( blocksForSubstitution ) } - designs.push( { designData, category } ) + designs.push( { + designData, category, type: designType, + } ) } ) designsRef.current = designs diff --git a/src/components/pro-control/index.js b/src/components/pro-control/index.js index fce309e547..0d5278cb3b 100644 --- a/src/components/pro-control/index.js +++ b/src/components/pro-control/index.js @@ -150,6 +150,14 @@ const LABELS = {
  • { __( 'Override styles while keeping them synced', i18n ) }
  • , }, + 'design-library-saved-patterns': { + title: __( 'Design Library Saved Patterns', i18n ), + description: , + }, } const ProControl = props => { diff --git a/src/design-library/index.js b/src/design-library/index.js index 72c87d36cb..3c5a9adca5 100644 --- a/src/design-library/index.js +++ b/src/design-library/index.js @@ -35,7 +35,7 @@ export const fetchDesignLibrary = async ( forceReset = false, version = '', type } } - return designLibrary[ type ]?.[ version || LATEST_API_VERSION ] ?? designLibrary[ type ] + return designLibrary[ type ]?.[ version || LATEST_API_VERSION ] ?? designLibrary[ type ] ?? {} } export const fetchDesign = async designId => { @@ -82,8 +82,9 @@ export const filterDesigns = async ( { library = [], plan: isPlan = '', category: isCategory = '', + type = 'patterns', } ) => { - if ( isPlan ) { + if ( isPlan && type !== 'saved' ) { library = library.filter( ( { plan } ) => plan === isPlan ) } diff --git a/src/design-library/init.php b/src/design-library/init.php index e48bb1a6d3..627863ea68 100644 --- a/src/design-library/init.php +++ b/src/design-library/init.php @@ -29,6 +29,8 @@ class Stackable_Design_Library { public function __construct() { add_action( 'rest_api_init', array( $this, 'register_route' ) ); + add_action( 'register_stackable_global_settings', array( $this, 'register_saved_patterns' ) ); + add_action( 'stackable_delete_design_library_cache', array( $this, 'delete_cache_v3' ) ); add_filter( 'stackable_localize_script', array( $this, 'add_wp_theme_global_styles' ) ); @@ -90,6 +92,37 @@ public function register_route() { ) ); } + public function register_saved_patterns() { + register_setting( + 'stackable_design_library', + 'stackable_design_library_saved_patterns', + array( + 'type' => 'array', + 'description' => __( 'Stackable Design Library User-Saved Patterns', STACKABLE_I18N ), + 'sanitize_callback' => array( $this, 'sanitize_array_setting' ), + 'show_in_rest' => array( + 'schema' => array( + 'items' => array( + 'type'=>'object', + 'properties'=> array( + 'id' => array( 'type' => 'string' ), + 'label' => array( 'type' => 'string' ), + 'description' => array( 'type' => 'string' ), + 'category' => array( 'type' => 'string' ), + 'template' => array( 'type' => 'string' ) + ) + ) + ) + ), + 'default' => array(), + ) + ); + } + + public function sanitize_array_setting( $input ) { + return is_array( $input ) ? $input : array(); + } + /** * Deletes all design library v3 caches. */ @@ -289,6 +322,11 @@ public function get_design_library( $request ) { $this->delete_cache(); } + if ( $type === 'saved' ) { + $saved_patterns = get_option( 'stackable_design_library_saved_patterns', [] ); + return rest_ensure_response( $saved_patterns ); + } + return rest_ensure_response( $this->get_design_library_from_cloud( $type ) ); } diff --git a/src/lazy-components/design-library/design-library-list/design-library-list-item.js b/src/lazy-components/design-library/design-library-list/design-library-list-item.js index d4983380e8..bd9e96c310 100644 --- a/src/lazy-components/design-library/design-library-list/design-library-list-item.js +++ b/src/lazy-components/design-library/design-library-list/design-library-list-item.js @@ -21,9 +21,11 @@ import { import { useState, useRef, memo, useMemo, + Fragment, } from '@wordpress/element' import { Dashicon, Spinner } from '@wordpress/components' import { __ } from '@wordpress/i18n' +import { applyFilters } from '@wordpress/hooks' const DesignLibraryListItem = memo( props => { const { @@ -86,20 +88,29 @@ const DesignLibraryListItem = memo( props => { const onClickHost = e => { e.stopPropagation() - if ( selectedTab === 'pages' ) { - return - } onClickDesign() } + const buttonAttributes = { + tabIndex: 0, + role: 'button', + onClick: onClickHost, + onKeyDown: e => { + if ( e.key === 'Enter' || e.key === ' ' ) { + e.preventDefault() + onClickHost( e ) + } + }, + } + return ( - // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events - + ) } ) diff --git a/src/lazy-components/design-library/design-library-list/design-preview.js b/src/lazy-components/design-library/design-library-list/design-preview.js index 0d97058f18..95e0fcc9c8 100644 --- a/src/lazy-components/design-library/design-library-list/design-preview.js +++ b/src/lazy-components/design-library/design-library-list/design-preview.js @@ -55,7 +55,7 @@ export const DesignPreview = ( { useEffect( () => { const container = ref.current - if ( ! container || selectedTab === 'patterns' ) { + if ( ! container || selectedTab !== 'pages' ) { return } @@ -90,6 +90,9 @@ export const DesignPreview = ( { setIsLoading( true ) + // Prevent interaction and focus within the preview content + wrapper.setAttribute( 'inert', '' ) + const ric = window.requestIdleCallback ? ( cb => window.requestIdleCallback( cb, { timeout: 5000 } ) ) : ( cb => setTimeout( cb, designIndex * 20 ) ) const sanitizedHTML = safeHTML( blocks ) @@ -121,6 +124,7 @@ export const DesignPreview = ( { >
    diff --git a/src/lazy-components/design-library/design-library-list/editor.scss b/src/lazy-components/design-library/design-library-list/editor.scss index 6c5118ff02..5c91505b8d 100644 --- a/src/lazy-components/design-library/design-library-list/editor.scss +++ b/src/lazy-components/design-library/design-library-list/editor.scss @@ -54,13 +54,51 @@ opacity: 1; } } + .stk-block-design__edit-btn-container { + opacity: 0; + z-index: 2; + transition: opacity 0.4s cubic-bezier(0.2, 0.6, 0.4, 1), visibility 0.4s cubic-bezier(0.2, 0.6, 0.4, 1); + position: absolute; + padding: 60px 30px 30px; + background: linear-gradient(180deg, transparent, #fffe 40px, #fff) !important; + top: auto; + bottom: -1px; + left: 0; + right: 0; + display: flex; + gap: 24px; + visibility: hidden; + @media (hover: none) { + opacity: 1; + } + + .ugb-button-component { + width: 100%; + justify-content: center; + pointer-events: none; + @media (hover: none) { + pointer-events: auto; + } + } + } &:hover { // box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px; box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; .ugb-button-component { opacity: 1; } + + .stk-block-design__edit-btn-container { + visibility: visible; + opacity: 1; + transition-delay: 1s; + + .ugb-button-component { + pointer-events: initial; + } + } } + &.ugb--is-hidden { opacity: 0; pointer-events: none; @@ -236,7 +274,6 @@ } } - .ugb-design-library-items { .stk-spinner-container { height: 100%; @@ -257,3 +294,10 @@ } } } + +.stk-design-library__item-saved .ugb-design-library-item { + cursor: default; + footer { + cursor: pointer; + } +} diff --git a/src/lazy-components/design-library/design-library-list/index.js b/src/lazy-components/design-library/design-library-list/index.js index 9a9b79a3f1..20febd7bdc 100644 --- a/src/lazy-components/design-library/design-library-list/index.js +++ b/src/lazy-components/design-library/design-library-list/index.js @@ -7,8 +7,9 @@ import { useDesignLibraryContext } from '../context' /** * External dependencies */ -import { i18n } from 'stackable' +import { i18n, isPro } from 'stackable' import { usePresetControls } from '~stackable/hooks' +import { ProControl } from '~stackable/components' import classnames from 'classnames' /** @@ -25,6 +26,7 @@ const DesignLibraryList = memo( props => { className = '', designs, isBusy, + selectedTab, errors, } = props const containerRef = useRef( null ) @@ -42,30 +44,33 @@ const DesignLibraryList = memo( props => { className="ugb-modal-design-library__designs" ref={ containerRef } > - { isBusy && } - { ! isBusy && <> -
    - { ( designs || [] ).map( ( design, i ) => { - return ( - - ) - } ) } - - { ! ( designs || [] ).length && -

    { __( 'No designs found', i18n ) }

    - } - { typeof errors === 'object' && errors && Object.keys( errors ).length && -

    - { __( 'An error has occurred:', i18n ) } -
    - { Object.values( errors ).join( '; ' ) } -

    } -
    - } + { selectedTab === 'saved' && ! isPro + ? + : <> + { isBusy && } + { ! isBusy &&
    + { ( designs || [] ).map( ( design, i ) => { + return ( + + ) + } ) } + + { ! ( designs || [] ).length && +

    { __( 'No designs found', i18n ) }

    + } + { typeof errors === 'object' && errors && Object.keys( errors ).length && +

    + { __( 'An error has occurred:', i18n ) } +
    + { Object.values( errors ).join( '; ' ) } +

    } +
    } + + }
    } ) diff --git a/src/lazy-components/design-library/design-library-list/use-preview-renderer.js b/src/lazy-components/design-library/design-library-list/use-preview-renderer.js index 737ad6c805..76d162f0fa 100644 --- a/src/lazy-components/design-library/design-library-list/use-preview-renderer.js +++ b/src/lazy-components/design-library/design-library-list/use-preview-renderer.js @@ -71,7 +71,7 @@ export const usePreviewRenderer = ( const siteTitle = useSelect( select => select( 'core' ).getEntityRecord( 'root', 'site' )?.title || 'InnovateCo', [] ) const isDesignLibraryDevMode = devMode && localStorage.getItem( 'stk__design_library__dev_mode' ) === '1' - const addHasBackground = selectedTab === 'patterns' + const addHasBackground = selectedTab !== 'pages' const updateShadowBodySize = _shadowBody => { const shadowBody = _shadowBody || shadowRoot?.querySelector( 'body' ) @@ -118,7 +118,7 @@ export const usePreviewRenderer = ( const scaleFactor = cardWidth > 0 ? cardWidth / 1300 : 1 // Divide by 1300, which is the width of preview in the shadow DOM let _bodyHeight = 1200 - if ( selectedTab === 'patterns' ) { + if ( selectedTab !== 'pages' ) { _bodyHeight = shadowBody.offsetHeight } @@ -225,13 +225,13 @@ export const usePreviewRenderer = ( let _parsedBlocksForInsertion = null const initialize = async () => { const _content = template - if ( selectedTab === 'patterns' ) { + if ( selectedTab !== 'pages' ) { const categorySlug = getCategorySlug( designId ) // For preview: always replace placeholders (ignore dev mode) - const _contentForPreview = replacePlaceholders( _content, categorySlug, false ) + const _contentForPreview = replacePlaceholders( _content, categorySlug, false, selectedTab ) // For insertion: only create separate content if dev mode is enabled - const _contentForInsertion = isDesignLibraryDevMode ? replacePlaceholders( _content, categorySlug, true ) : _contentForPreview + const _contentForInsertion = isDesignLibraryDevMode ? replacePlaceholders( _content, categorySlug, true, selectedTab ) : _contentForPreview categoriesRef.current.push( categorySlug ) @@ -249,12 +249,12 @@ export const usePreviewRenderer = ( // For preview: always replace placeholders (ignore dev mode) const designsContentForPreview = designs.map( ( design, i ) => - replacePlaceholders( design.template || design.content, categorySlugs[ i ], false ) + replacePlaceholders( design.template || design.content, categorySlugs[ i ], false, selectedTab ) ).join( '\n' ) // For insertion: only create separate content if dev mode is enabled const designsContentForInsertion = isDesignLibraryDevMode ? designs.map( ( design, i ) => - replacePlaceholders( design.template || design.content, categorySlugs[ i ], true ) + replacePlaceholders( design.template || design.content, categorySlugs[ i ], true, selectedTab ) ).join( '\n' ) : designsContentForPreview diff --git a/src/lazy-components/design-library/editor.scss b/src/lazy-components/design-library/editor.scss index 3fadaa780e..1ac949db94 100644 --- a/src/lazy-components/design-library/editor.scss +++ b/src/lazy-components/design-library/editor.scss @@ -102,6 +102,10 @@ padding: 24px; grid-column: 2 / 3; grid-row: 1 / 3; + + .ugb-design-control-pro-note { + height: 100%; + } } .ugb-modal-design-library__search { @@ -464,3 +468,9 @@ div.ugb-modal-design-library__enable-background { min-width: unset; } } + +body:has(.stk-design-library__item-saved) { + .components-snackbar-list { + z-index: 100001; + } +} diff --git a/src/lazy-components/design-library/header-actions.js b/src/lazy-components/design-library/header-actions.js index 3e87fb3f31..58b8aed93a 100644 --- a/src/lazy-components/design-library/header-actions.js +++ b/src/lazy-components/design-library/header-actions.js @@ -2,7 +2,7 @@ * External deprendencies */ import { - i18n, isPro, devMode, + i18n, isPro, devMode, showProNotice, } from 'stackable' import { AdvancedToolbarControl, Button } from '~stackable/components' @@ -28,21 +28,29 @@ export const HeaderActions = props => { setDoReset, onClose, } = props + + let controls = [ + { + value: 'patterns', + title: __( 'Patterns', i18n ), + }, + { + value: 'pages', + title: __( 'Pages', i18n ), + }, + ] + + controls = ! isPro && ! showProNotice ? controls : [ ...controls, { + value: 'saved', + title: __( 'Saved', i18n ), + } ] + return <> { /* DEV NOTE: hide for now */ } { const [ selectedContainerScheme, setSelectedContainerScheme ] = useState( '' ) const [ selectedBackgroundScheme, setSelectedBackgroundScheme ] = useState( '' ) + const savedPatterns = useSelect( select => { + return applyFilters( 'stackable.design-library.patterns', [], select ) + }, [] ) + // For version 4, the default tab is now 'patterns' and for category, we use '' instead of 'All'. // So we need to update the local storage values here. useEffect( () => { @@ -85,6 +91,14 @@ const ModalDesignLibrary = props => { // Update the designs on the sidebar. (this will trigger the display designs update next) useEffect( () => { + if ( selectedTab === 'saved' ) { + setSidebarDesigns( savedPatterns ) + setSelectedCategory( '' ) + setDoReset( false ) + setIsBusy( false ) + return + } + setIsBusy( true ) setSidebarDesigns( [] ) setErrors( null ) @@ -106,7 +120,7 @@ const ModalDesignLibrary = props => { setDoReset( false ) setIsBusy( false ) } ) - }, [ doReset, selectedTab ] ) + }, [ doReset, selectedTab, savedPatterns ] ) // This updates the displayed designs the user can pick. useEffect( () => { @@ -114,6 +128,7 @@ const ModalDesignLibrary = props => { library: sidebarDesigns, category: selectedCategory, plan: selectedPlan.key, + type: selectedTab, } ).then( designs => { setDisplayDesigns( designs ) } ) @@ -140,7 +155,7 @@ const ModalDesignLibrary = props => { const onSelectDesign = useCallback( ( designId, category, parsedBlocks, blocksForSubstitution, selectedPreviewSize ) => { if ( selectedTab === 'pages' ) { const selectedDesign = [ { - designId, category, designData: parsedBlocks, blocksForSubstitution, selectedPreviewSize, + designId, category, designData: parsedBlocks, blocksForSubstitution, selectedPreviewSize, type: selectedTab, } ] addDesign( selectedDesign ) @@ -170,6 +185,7 @@ const ModalDesignLibrary = props => { } else { newSelectedDesignData.push( { designId, category, designData: parsedBlocks, blocksForSubstitution, selectedPreviewSize, + type: selectedTab, } ) } @@ -248,7 +264,7 @@ const ModalDesignLibrary = props => { - { selectedTab === 'patterns' && { className={ `stk-design-library__item-${ selectedTab }` } isBusy={ isBusy } designs={ displayDesigns } + selectedTab={ selectedTab } errors={ errors } /> - { selectedTab === 'patterns' &&