diff --git a/plugin.php b/plugin.php index 24078763be..284a570a18 100644 --- a/plugin.php +++ b/plugin.php @@ -278,6 +278,7 @@ function is_frontend() { } // Deprecated. +require_once( plugin_dir_path( __FILE__ ) . 'src/deprecated/editor-settings.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/deprecated/native-global-colors.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/deprecated/navigation-panel-pre-enabled.php' ); require_once( plugin_dir_path( __FILE__ ) . 'src/deprecated/font-awesome-version.php' ); diff --git a/src/block/accordion/block.json b/src/block/accordion/block.json index 4582c7ea38..27a49c0d00 100644 --- a/src/block/accordion/block.json +++ b/src/block/accordion/block.json @@ -11,5 +11,13 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/accordion-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/accordion-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/icon-label", + "stackable/heading", + "stackable/icon" + ], + "stk-substitution-blocks": [ + "stackable/text" + ] } diff --git a/src/block/accordion/index.js b/src/block/accordion/index.js index 25323c31e3..fd451093d5 100644 --- a/src/block/accordion/index.js +++ b/src/block/accordion/index.js @@ -17,6 +17,7 @@ import metadata from './block.json' import variations from './variations' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * WordPress dependencies @@ -39,4 +40,5 @@ export const settings = { variations, edit, save, + substitute, } diff --git a/src/block/accordion/substitute.js b/src/block/accordion/substitute.js new file mode 100644 index 0000000000..bcb9cb66ed --- /dev/null +++ b/src/block/accordion/substitute.js @@ -0,0 +1,19 @@ +export const substitute = { + from: 'stackable/accordion', + transform: ( oldAttributes, innerBlocks ) => { + // Get the heading + const heading = innerBlocks[ 0 ][ 2 ][ 0 ][ 2 ][ 0 ][ 1 ].text + const insideBlocks = innerBlocks[ 1 ][ 2 ] + + return [ + 'core/details', + { + open: false, + summary: heading, + }, + insideBlocks, + ] + }, +} + +export default substitute diff --git a/src/block/blockquote/block.json b/src/block/blockquote/block.json index 5f10563d53..1b4f9f0ca3 100644 --- a/src/block/blockquote/block.json +++ b/src/block/blockquote/block.json @@ -7,5 +7,11 @@ "usesContext": [ "postId", "postType", "queryId" , "stackable/innerBlockOrientation" ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/blockquote-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/blockquote-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/icon" + ], + "stk-substitution-blocks": [ + "stackable/text" + ] } diff --git a/src/block/blockquote/index.js b/src/block/blockquote/index.js index d358b9f78e..165c91db76 100644 --- a/src/block/blockquote/index.js +++ b/src/block/blockquote/index.js @@ -11,6 +11,7 @@ import save from './save' import schema from './schema' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * External dependencies @@ -40,4 +41,5 @@ export const settings = { variations, edit, save, + substitute, } diff --git a/src/block/blockquote/substitute.js b/src/block/blockquote/substitute.js new file mode 100644 index 0000000000..1e9a3db7a4 --- /dev/null +++ b/src/block/blockquote/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/blockquote', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'core/pullquote', + { + value: innerBlocks[ 1 ][ 1 ].text, + align: oldAttributes.contentAlign, + }, + ] + }, +} + +export default substitute diff --git a/src/block/button-group/block.json b/src/block/button-group/block.json index 838f9adf2c..cb394b3c3c 100644 --- a/src/block/button-group/block.json +++ b/src/block/button-group/block.json @@ -31,9 +31,13 @@ "description": "Add social buttons.", "category": "stackable", "stk-type": "special", - "stk-demo": "https://wpstackable.com/social-buttons-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/social-buttons-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/button-group|icon-button" + ] } ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "hidden" + } diff --git a/src/block/button-group/index.js b/src/block/button-group/index.js index 516efa69b7..8d79d0e9d1 100644 --- a/src/block/button-group/index.js +++ b/src/block/button-group/index.js @@ -16,6 +16,7 @@ import metadata from './block.json' import variations from './variations' import deprecated from './deprecated' import { buttonExample } from './example' +import substitute from './substitute' /** * WordPress dependencies @@ -40,4 +41,5 @@ export const settings = { example: buttonExample, edit, save, + substitute, } diff --git a/src/block/button-group/substitute.js b/src/block/button-group/substitute.js new file mode 100644 index 0000000000..5d04e27af8 --- /dev/null +++ b/src/block/button-group/substitute.js @@ -0,0 +1,27 @@ +import { BLOCK_STATE } from '~stackable/util' + +export const substitute = { + from: 'stackable/button-group', + variants: [ 'stackable/button-group|icon-button', 'stackable/button-group|button' ], + to: [ 'core/buttons', 'core/social-links' ], + transform: ( oldAttributes, innerBlocks, disabledBlocks ) => { + if ( 'stackable/button-group|icon-button' in disabledBlocks && disabledBlocks[ 'stackable/button-group|icon-button' ] === BLOCK_STATE.DISABLED && // eslint-disable-line camelcase + innerBlocks.length && + innerBlocks[ 0 ][ 0 ] === 'stackable/icon-button' + ) { + return [ 'core/social-links', + { align: oldAttributes.contentAlign }, + [ + [ 'core/social-link', { service: 'facebook' } ], + [ 'core/social-link', { service: 'twitter' } ], + ], + ] + } + if ( 'stackable/button-group|button' in disabledBlocks && disabledBlocks[ 'stackable/button-group|button' ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return [ 'core/buttons', {}, innerBlocks ] + } + return [ 'stackable/button-group', oldAttributes, innerBlocks ] + }, +} + +export default substitute diff --git a/src/block/button/block.json b/src/block/button/block.json index 9f79e69747..2b0d449e53 100644 --- a/src/block/button/block.json +++ b/src/block/button/block.json @@ -13,5 +13,6 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "hidden", - "stk-demo": "https://wpstackable.com/button-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/button-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-block-dependency": "stackable/button-group|button" } diff --git a/src/block/button/index.js b/src/block/button/index.js index 051a55e921..1f70fd7548 100644 --- a/src/block/button/index.js +++ b/src/block/button/index.js @@ -11,6 +11,7 @@ import schema from './schema' import metadata from './block.json' import transforms from './transforms' import deprecated from './deprecated' +import substitute from './substitute' /** * External dependencies @@ -36,4 +37,5 @@ export const settings = { example, edit, save, + substitute, } diff --git a/src/block/button/substitute.js b/src/block/button/substitute.js new file mode 100644 index 0000000000..6a449b5565 --- /dev/null +++ b/src/block/button/substitute.js @@ -0,0 +1,17 @@ +import { BLOCK_STATE } from '~stackable/util' + +export const substitute = { + from: 'stackable/button', + variants: [ 'stackable/button-group|button' ], + to: 'core/paragraph', + transform: ( oldAttributes, innerBlocks, disabledBlocks ) => { + if ( 'stackable/button-group|button' in disabledBlocks && disabledBlocks[ 'stackable/button-group|button' ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return [ 'core/button', { + text: oldAttributes.text, + } ] + } + return [ 'stackable/button', oldAttributes ] + }, +} + +export default substitute diff --git a/src/block/call-to-action/block.json b/src/block/call-to-action/block.json index 1b38094be3..0e68d217a5 100644 --- a/src/block/call-to-action/block.json +++ b/src/block/call-to-action/block.json @@ -13,5 +13,11 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/call-to-action-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/call-to-action-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-substitution-blocks": [ + "stackable/heading", + "stackable/text", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/call-to-action/index.js b/src/block/call-to-action/index.js index 4748a5ff51..87a638839f 100644 --- a/src/block/call-to-action/index.js +++ b/src/block/call-to-action/index.js @@ -11,6 +11,7 @@ import save from './save' import schema from './schema' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * External dependencies @@ -40,4 +41,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/call-to-action/substitute.js b/src/block/call-to-action/substitute.js new file mode 100644 index 0000000000..696640b634 --- /dev/null +++ b/src/block/call-to-action/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/call-to-action', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { + ...oldAttributes, + }, + [ [ 'stackable/column', {}, innerBlocks ] ], + ] + }, +} + +export default substitute diff --git a/src/block/card/block.json b/src/block/card/block.json index 2025e1f7dd..827bdec12e 100644 --- a/src/block/card/block.json +++ b/src/block/card/block.json @@ -10,5 +10,12 @@ }, "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/card-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/card-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-substitution-blocks": [ + "stackable/heading", + "stackable/text", + "stackable/subtitle", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/card/index.js b/src/block/card/index.js index 13bb7b2d98..4a58256390 100644 --- a/src/block/card/index.js +++ b/src/block/card/index.js @@ -17,6 +17,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * WordPress dependencies @@ -40,4 +41,5 @@ export const settings = { variations, edit, save, + substitute, } diff --git a/src/block/card/substitute.js b/src/block/card/substitute.js new file mode 100644 index 0000000000..0550df98fd --- /dev/null +++ b/src/block/card/substitute.js @@ -0,0 +1,24 @@ +export const substitute = { + from: 'stackable/card', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { ...oldAttributes }, + [ + [ + 'stackable/column', + { + align: oldAttributes.align, + hasContainer: true, + }, + [ + [ 'stackable/image', { imageUrl: oldAttributes.imageUrl } ], + ...innerBlocks, + ], + ], + ], + ] + }, +} + +export default substitute diff --git a/src/block/carousel/index.js b/src/block/carousel/index.js index be3ba26b39..b0a4f9b449 100644 --- a/src/block/carousel/index.js +++ b/src/block/carousel/index.js @@ -15,6 +15,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -34,4 +35,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/carousel/substitute.js b/src/block/carousel/substitute.js new file mode 100644 index 0000000000..25e3df485b --- /dev/null +++ b/src/block/carousel/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/carousel', + variants: [], + to: [ 'stackable/columns' ], + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + {}, + innerBlocks, + ] + }, +} + +export default substitute diff --git a/src/block/column/block.json b/src/block/column/block.json index fce9089cc5..3bd52de703 100644 --- a/src/block/column/block.json +++ b/src/block/column/block.json @@ -19,5 +19,6 @@ "stackable/tab-content" ], "textdomain": "stackable-ultimate-gutenberg-blocks", - "stk-type": "hidden" + "stk-type": "hidden", + "stk-available-states": [ "enabled", "hidden" ] } diff --git a/src/block/columns/block.json b/src/block/columns/block.json index 93ff48c38a..a4511b0ef9 100644 --- a/src/block/columns/block.json +++ b/src/block/columns/block.json @@ -19,5 +19,6 @@ }, "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "essential", - "stk-demo": "https://wpstackable.com/columns-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/columns-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-available-states": [ "enabled", "hidden" ] } diff --git a/src/block/count-up/index.js b/src/block/count-up/index.js index 9af52d64e9..1abe3a90b0 100644 --- a/src/block/count-up/index.js +++ b/src/block/count-up/index.js @@ -15,6 +15,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -29,4 +30,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/count-up/substitute.js b/src/block/count-up/substitute.js new file mode 100644 index 0000000000..1bd95070aa --- /dev/null +++ b/src/block/count-up/substitute.js @@ -0,0 +1,16 @@ +export const substitute = { + from: 'stackable/count-up', + transform: oldAttributes => { + return [ + 'core/paragraph', + { + fontSize: 'x-large', + content: oldAttributes.text, + align: oldAttributes.contentAlign, + style: { spacing: { margin: { top: '0', bottom: '0' } } }, + }, + ] + }, +} + +export default substitute diff --git a/src/block/countdown/index.js b/src/block/countdown/index.js index 354ca4f12a..76dc08a0c1 100644 --- a/src/block/countdown/index.js +++ b/src/block/countdown/index.js @@ -15,6 +15,7 @@ import save from './save' import schema from './schema' import metadata from './block.json' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -29,5 +30,6 @@ export const settings = { align: [ 'center', 'wide', 'full' ], spacing: true, }, + substitute, } diff --git a/src/block/countdown/substitute.js b/src/block/countdown/substitute.js new file mode 100644 index 0000000000..8c4af29cb8 --- /dev/null +++ b/src/block/countdown/substitute.js @@ -0,0 +1,49 @@ +export const substitute = { + from: 'stackable/countdown', + variations: [], + to: 'stackable/columns', + transform: () => { + return [ + 'stackable/columns', + {}, + [ + [ + 'stackable/column', + { align: 'center' }, + [ + [ 'core/text', { + fontSize: 'x-large', + content: '3', + align: 'center', + style: { spacing: { margin: { top: '0', bottom: '0' } } }, + } ], + [ 'core/text', { + content: 'Days', + align: 'center', + style: { spacing: { margin: { top: '0', bottom: '0' } } }, + } ], + ], + ], + [ + 'stackable/column', + { align: 'center' }, + [ + [ 'core/text', { + fontSize: 'x-large', + content: '16', + align: 'center', + style: { spacing: { margin: { top: '0', bottom: '0' } } }, + } ], + [ 'core/text', { + content: 'Hours', + align: 'center', + style: { spacing: { margin: { top: '0', bottom: '0' } } }, + } ], + ], + ], + ], + ] + }, +} + +export default substitute diff --git a/src/block/design-library/block.json b/src/block/design-library/block.json index cf56eaba99..6afd87ef5d 100644 --- a/src/block/design-library/block.json +++ b/src/block/design-library/block.json @@ -15,5 +15,6 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/designs/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/designs/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-available-states": [ "enabled", "hidden" ] } diff --git a/src/block/design-library/edit.js b/src/block/design-library/edit.js index 96285034a6..a1f62895e4 100644 --- a/src/block/design-library/edit.js +++ b/src/block/design-library/edit.js @@ -2,7 +2,11 @@ * Internal dependencies */ import previewImage from './images/preview.jpg' -import { i18n, srcUrl } from 'stackable' +import { + i18n, + srcUrl, + settings, +} from 'stackable' import { Button, ModalDesignLibrary, @@ -11,6 +15,8 @@ import { SVGStackableIcon } from '~stackable/icons' import { deprecateBlockBackgroundColorOpacity, deprecateContainerBackgroundColorOpacity, deprecateTypographyGradientColor, } from '~stackable/block-components' +import { substituteCoreIfDisabled, BLOCK_STATE } from '~stackable/util' +import { substitutionRules } from '../../blocks' /** * WordPress dependencies @@ -18,7 +24,7 @@ import { import { __ } from '@wordpress/i18n' import { dispatch } from '@wordpress/data' import { - createBlock, parse, createBlocksFromInnerBlocksTemplate, getBlockVariations, + createBlock, parse, createBlocksFromInnerBlocksTemplate, getBlockVariations, getBlockType, } from '@wordpress/blocks' import { useState } from '@wordpress/element' import { addFilter, applyFilters } from '@wordpress/hooks' @@ -27,6 +33,48 @@ import { useBlockProps } from '@wordpress/block-editor' // Replaces the current block with a block made out of attributes. const createBlockWithAttributes = ( blockName, attributes, innerBlocks, design ) => { + const disabledBlocks = settings.stackable_block_states || {} // eslint-disable-line camelcase + let hasSubstituted = false + + // Recursively substitute core blocks to disabled Stackable blocks + const traverseBlocksAndSubstitute = blocks => { + return blocks.map( block => { + let isDisabled = true + // Maximum attempt to error if no substitution rule for the block + let attempts = 10 + + // Check if the new substituted block is still disabled + while ( isDisabled && attempts > 0 ) { + const previousBlockName = block[ 0 ] + block = substituteCoreIfDisabled( ...block, substitutionRules ) + isDisabled = block[ 0 ] in disabledBlocks && disabledBlocks[ block[ 0 ] ] === BLOCK_STATE.DISABLED + // If the previous block is different from the new block, substitution has been made + if ( ! hasSubstituted && previousBlockName !== block[ 0 ] ) { + hasSubstituted = true + } + attempts-- + } + + // Do a preorder traversal by subsituting first before traversing + if ( block[ 2 ] && block[ 2 ].length > 0 ) { + block[ 2 ] = traverseBlocksAndSubstitute( block[ 2 ] ) + } + + if ( ! Array.isArray( block[ 2 ] ) ) { + block[ 2 ] = [] + } + return block + } ) + } + + // Substitute from the root of the design + [ blockName, attributes, innerBlocks ] = traverseBlocksAndSubstitute( [ [ blockName, attributes, innerBlocks ] ] )[ 0 ] + + if ( hasSubstituted ) { + // eslint-disable-next-line no-alert + alert( 'Notice: Disabled blocks in the design will be substituted with other Stackable or core blocks' ) + } + // const { replaceBlock } = dispatch( 'core/block-editor' ) // For wireframes, we'll need to apply any default block attributes to @@ -44,9 +92,9 @@ const createBlockWithAttributes = ( blockName, attributes, innerBlocks, design ) blocks.forEach( block => { const blockName = block[ 0 ] - // For blocks with varitions, do not remove the uniqueId + // For blocks with variations, do not remove the uniqueId // since that will prompt the layout picker to show. - const hasVariations = getBlockVariations( blockName ).length > 0 + const hasVariations = !! getBlockType( blockName ) && getBlockVariations( blockName ).length > 0 if ( ! hasVariations && block[ 1 ].uniqueId ) { delete block[ 1 ].uniqueId } @@ -62,13 +110,16 @@ const createBlockWithAttributes = ( blockName, attributes, innerBlocks, design ) // Recursively update the attributes of all inner blocks for the new Color Picker const migrateToNewColorPicker = blocks => { - blocks.forEach( block => { - let newAttributes = block[ 1 ] - newAttributes = deprecateContainerBackgroundColorOpacity.migrate( newAttributes ) - newAttributes = deprecateBlockBackgroundColorOpacity.migrate( newAttributes ) - newAttributes = deprecateTypographyGradientColor.migrate( '%s' )( newAttributes ) - block[ 1 ] = newAttributes - migrateToNewColorPicker( block[ 2 ] ) + blocks?.forEach( block => { + try { + let newAttributes = block[ 1 ] + newAttributes = deprecateContainerBackgroundColorOpacity.migrate( newAttributes ) + newAttributes = deprecateBlockBackgroundColorOpacity.migrate( newAttributes ) + newAttributes = deprecateTypographyGradientColor.migrate( '%s' )( newAttributes ) + block[ 1 ] = newAttributes + migrateToNewColorPicker( block[ 2 ] ) + } catch ( error ) { + } } ) } diff --git a/src/block/divider/index.js b/src/block/divider/index.js index 190409537c..044defb5c4 100644 --- a/src/block/divider/index.js +++ b/src/block/divider/index.js @@ -15,6 +15,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -30,4 +31,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/divider/substitute.js b/src/block/divider/substitute.js new file mode 100644 index 0000000000..56da8a5bcd --- /dev/null +++ b/src/block/divider/substitute.js @@ -0,0 +1,10 @@ +export const substitute = { + from: 'stackable/divider', + transform: () => { + return [ + 'core/separator', + ] + }, +} + +export default substitute diff --git a/src/block/expand/block.json b/src/block/expand/block.json index ed0bcba11a..32ebaecf43 100644 --- a/src/block/expand/block.json +++ b/src/block/expand/block.json @@ -11,5 +11,9 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/expand-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/expand-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/text", + "stackable/button-group|button" + ] } diff --git a/src/block/expand/index.js b/src/block/expand/index.js index ff0dfe9744..a6928b0529 100644 --- a/src/block/expand/index.js +++ b/src/block/expand/index.js @@ -16,6 +16,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * WordPress dependencies @@ -37,4 +38,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/expand/substitute.js b/src/block/expand/substitute.js new file mode 100644 index 0000000000..a1f18e18ea --- /dev/null +++ b/src/block/expand/substitute.js @@ -0,0 +1,11 @@ +export const substitute = { + from: 'stackable/expand', + to: 'stackable/text', + transform: ( oldAttributes, innerBlocks ) => { + return { + text: innerBlocks[ 2 ][ 1 ].text, + } + }, +} + +export default substitute diff --git a/src/block/feature-grid/block.json b/src/block/feature-grid/block.json index bb62bda42d..e82e88c709 100644 --- a/src/block/feature-grid/block.json +++ b/src/block/feature-grid/block.json @@ -10,5 +10,12 @@ }, "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/feature-grid-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/feature-grid-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-substitution-blocks": [ + "stackable/image", + "stackable/heading", + "stackable/text", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/feature-grid/index.js b/src/block/feature-grid/index.js index 033d814ca5..75837323ef 100644 --- a/src/block/feature-grid/index.js +++ b/src/block/feature-grid/index.js @@ -17,6 +17,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * WordPress dependencies @@ -41,4 +42,5 @@ export const settings = { variations, edit, save, + substitute, } diff --git a/src/block/feature-grid/substitute.js b/src/block/feature-grid/substitute.js new file mode 100644 index 0000000000..223b6a6727 --- /dev/null +++ b/src/block/feature-grid/substitute.js @@ -0,0 +1,15 @@ +export const substitute = { + from: 'stackable/feature-grid', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { + ...oldAttributes, + contentAlign: 'center', + }, + innerBlocks, + ] + }, +} + +export default substitute diff --git a/src/block/feature/block.json b/src/block/feature/block.json index 422623c381..4c4559aad6 100644 --- a/src/block/feature/block.json +++ b/src/block/feature/block.json @@ -10,5 +10,14 @@ }, "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/feature-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/feature-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/image" + ], + "stk-substitution-blocks": [ + "stackable/heading", + "stackable/text", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/feature/index.js b/src/block/feature/index.js index f7a371b2bc..3c3830e4f2 100644 --- a/src/block/feature/index.js +++ b/src/block/feature/index.js @@ -17,6 +17,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * WordPress dependencies @@ -42,4 +43,5 @@ export const settings = { variations, edit, save, + substitute, } diff --git a/src/block/feature/substitute.js b/src/block/feature/substitute.js new file mode 100644 index 0000000000..433d76f9a9 --- /dev/null +++ b/src/block/feature/substitute.js @@ -0,0 +1,12 @@ +export const substitute = { + from: 'stackable/feature', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { ...oldAttributes }, + innerBlocks, + ] + }, +} + +export default substitute diff --git a/src/block/heading/index.js b/src/block/heading/index.js index 19ffd5910d..02e0fd2869 100644 --- a/src/block/heading/index.js +++ b/src/block/heading/index.js @@ -16,6 +16,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -41,4 +42,5 @@ export const settings = { ( ( attributesToMerge.hasOwnProperty( 'content' ) ? attributesToMerge.content : attributesToMerge.text ) || '' ), } }, + substitute, } diff --git a/src/block/heading/substitute.js b/src/block/heading/substitute.js new file mode 100644 index 0000000000..c89fa9d491 --- /dev/null +++ b/src/block/heading/substitute.js @@ -0,0 +1,15 @@ +export const substitute = { + from: 'stackable/heading', + transform: oldAttributes => { + return [ + 'core/heading', + { + content: oldAttributes.text, + level: oldAttributes.textTag ? Number( oldAttributes.textTag.replace( 'h', '' ) ) : 2, + textAlign: oldAttributes.contentAlign, + }, + ] + }, +} + +export default substitute diff --git a/src/block/hero/block.json b/src/block/hero/block.json index e7946996bc..847c179a44 100644 --- a/src/block/hero/block.json +++ b/src/block/hero/block.json @@ -18,5 +18,11 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/hero-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/hero-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-substitution-blocks": [ + "stackable/heading", + "stackable/text", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/hero/index.js b/src/block/hero/index.js index 29e6635ab2..a591be4f8c 100644 --- a/src/block/hero/index.js +++ b/src/block/hero/index.js @@ -11,6 +11,7 @@ import save from './save' import schema from './schema' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * External dependencies @@ -40,4 +41,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/hero/substitute.js b/src/block/hero/substitute.js new file mode 100644 index 0000000000..5195eb4edb --- /dev/null +++ b/src/block/hero/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/hero', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { + ...oldAttributes, + }, + [ [ 'stackable/column', {}, innerBlocks ] ], + ] + }, +} + +export default substitute diff --git a/src/block/horizontal-scroller/index.js b/src/block/horizontal-scroller/index.js index 64902e7c25..4200c51295 100644 --- a/src/block/horizontal-scroller/index.js +++ b/src/block/horizontal-scroller/index.js @@ -7,6 +7,7 @@ import schema from './schema' import example from './example' import metadata from './block.json' import deprecated from './deprecated' +import substitute from './substitute' import { HorizontalScrollerIcon } from '~stackable/icons' @@ -28,4 +29,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/horizontal-scroller/substitute.js b/src/block/horizontal-scroller/substitute.js new file mode 100644 index 0000000000..9c1b4dd43d --- /dev/null +++ b/src/block/horizontal-scroller/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/horizontal-scroller', + variants: [], + to: [ 'stackable/columns' ], + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + {}, + innerBlocks, + ] + }, +} + +export default substitute diff --git a/src/block/icon-box/block.json b/src/block/icon-box/block.json index bca7d3200a..0e061c9927 100644 --- a/src/block/icon-box/block.json +++ b/src/block/icon-box/block.json @@ -12,5 +12,10 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/icon-box-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/icon-box-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/icon-label", + "stackable/icon", + "stackable/heading" + ] } diff --git a/src/block/icon-box/edit.js b/src/block/icon-box/edit.js index 74a20ccbcc..8ad37547b7 100644 --- a/src/block/icon-box/edit.js +++ b/src/block/icon-box/edit.js @@ -32,6 +32,8 @@ import { import { withBlockAttributeContext, withBlockWrapperIsHovered, withQueryLoopContext, } from '~stackable/higher-order' +import { substituteCoreIfDisabled } from '~stackable/util' +import { substitutionRules } from '../../blocks' /** * WordPress dependencies @@ -49,9 +51,12 @@ export const TEMPLATE = [ text: __( 'Icon Box', i18n ), hasP: true, textTag: 'h4', } ], ] ], - [ 'stackable/text', { - text: 'Description for this block. Use this space for describing your block.', - } ], + substituteCoreIfDisabled( + 'stackable/text', + { text: 'Description for this block.' }, + [], + substitutionRules, + ), ] const Edit = props => { diff --git a/src/block/icon-box/substitute.js b/src/block/icon-box/substitute.js new file mode 100644 index 0000000000..f85a6f3fa6 --- /dev/null +++ b/src/block/icon-box/substitute.js @@ -0,0 +1,15 @@ +export const substitute = { + from: 'stackable/icon-box', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { + ...oldAttributes, + }, + [ [ 'stackable/column', {}, innerBlocks ] ], + ] + }, + +} + +export default substitute diff --git a/src/block/icon-button/block.json b/src/block/icon-button/block.json index 10ccb4bf68..05e5611572 100644 --- a/src/block/icon-button/block.json +++ b/src/block/icon-button/block.json @@ -13,5 +13,6 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "hidden", - "stk-demo": "https://wpstackable.com/icon-button-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/icon-button-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-block-dependency": "stackable/button-group|icon-button" } diff --git a/src/block/icon-label/block.json b/src/block/icon-label/block.json index 8d0df07d5e..7f455d2c85 100644 --- a/src/block/icon-label/block.json +++ b/src/block/icon-label/block.json @@ -15,5 +15,9 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/icon-label-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/icon-label-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/icon", + "stackable/heading" + ] } diff --git a/src/block/icon-label/index.js b/src/block/icon-label/index.js index a8ed2a2616..d52348578e 100644 --- a/src/block/icon-label/index.js +++ b/src/block/icon-label/index.js @@ -15,6 +15,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -30,4 +31,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/icon-label/substitute.js b/src/block/icon-label/substitute.js new file mode 100644 index 0000000000..8f9046f6bc --- /dev/null +++ b/src/block/icon-label/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/icon-label', + to: 'stackable/text', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/text', + { + text: innerBlocks[ 1 ][ 1 ].text, + }, + ] + }, +} + +export default substitute diff --git a/src/block/icon-list/substitute.js b/src/block/icon-list/substitute.js new file mode 100644 index 0000000000..ac1c1da6c1 --- /dev/null +++ b/src/block/icon-list/substitute.js @@ -0,0 +1,16 @@ +export const substitute = { + from: 'stackable/icon-list', + transform: () => { + return [ + 'core/list', + {}, + [ + [ 'core/list-item', { content: 'First item list' } ], + [ 'core/list-item', { content: 'Second item list' } ], + [ 'core/list-item', { content: 'Third item list' } ], + ], + ] + }, +} + +export default substitute diff --git a/src/block/icon/index.js b/src/block/icon/index.js index 51a877663e..f04cdc7816 100644 --- a/src/block/icon/index.js +++ b/src/block/icon/index.js @@ -17,6 +17,7 @@ import schema from './schema' import example from './example' import transforms from './transforms' import deprecated from './deprecated' +import substitute from './substitute' /** * WordPress dependencies @@ -36,4 +37,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/icon/substitute.js b/src/block/icon/substitute.js new file mode 100644 index 0000000000..8c6798a4ed --- /dev/null +++ b/src/block/icon/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/icon', + transform: () => { + return [ + 'core/paragraph', + { + content: '', + }, + ] + }, + +} + +export default substitute diff --git a/src/block/image-box/block.json b/src/block/image-box/block.json index 4cc47bd030..3d0ae9fd0a 100644 --- a/src/block/image-box/block.json +++ b/src/block/image-box/block.json @@ -7,5 +7,14 @@ "usesContext": [ "postId", "postType", "queryId" , "stackable/innerBlockOrientation" ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/image-box-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/image-box-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/image", + "stackable/subtitle", + "stackable/icon" + ], + "stk-substitution-blocks": [ + "stackable/heading", + "stackable/text" + ] } diff --git a/src/block/image-box/substitute.js b/src/block/image-box/substitute.js new file mode 100644 index 0000000000..1c839995f4 --- /dev/null +++ b/src/block/image-box/substitute.js @@ -0,0 +1,27 @@ +export const substitute = { + from: 'stackable/image-box', + transform: ( oldAttributes, innerBlocks ) => { + const imageUrl = innerBlocks[ 0 ][ 1 ]?.imageUrl + const imageHeight = innerBlocks[ 0 ][ 1 ]?.imageHeight + + innerBlocks = innerBlocks.filter( block => ! [ 'stackable/image', 'core/image' ].includes( block[ 0 ] ) ) + + return [ + 'stackable/columns', + { + ...oldAttributes, + hasBackground: true, + blockBackgroundMediaUrl: imageUrl, + blockPadding: { + top: imageHeight, + right: 0, + bottom: 0, + left: 0, + }, + }, + innerBlocks, + ] + }, +} + +export default substitute diff --git a/src/block/image/index.js b/src/block/image/index.js index 21f0bbc318..fb65bc75bb 100644 --- a/src/block/image/index.js +++ b/src/block/image/index.js @@ -17,6 +17,7 @@ import metadata from './block.json' import example from './example' import transforms from './transforms' import deprecated from './deprecated' +import substitute from './substitute' /** * WordPress dependencies @@ -38,4 +39,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/image/substitute.js b/src/block/image/substitute.js new file mode 100644 index 0000000000..3f78ffef02 --- /dev/null +++ b/src/block/image/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/image', + transform: oldAttributes => { + return [ + 'core/image', + { + height: oldAttributes?.imageHeight ? ( oldAttributes?.imageHeight + 'px' ) : undefined, + url: oldAttributes?.imageUrl, + }, + ] + }, +} + +export default substitute diff --git a/src/block/notification/block.json b/src/block/notification/block.json index a7bf51c75a..5934e7745c 100644 --- a/src/block/notification/block.json +++ b/src/block/notification/block.json @@ -14,5 +14,14 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/notification-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/notification-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/icon" + ], + "stk-substitution-blocks": [ + "stackable/heading", + "stackable/text", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/number-box/index.js b/src/block/number-box/index.js index cd0306a118..895ff00aa2 100644 --- a/src/block/number-box/index.js +++ b/src/block/number-box/index.js @@ -15,6 +15,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -29,4 +30,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/number-box/substitute.js b/src/block/number-box/substitute.js new file mode 100644 index 0000000000..a9fcbe1ee6 --- /dev/null +++ b/src/block/number-box/substitute.js @@ -0,0 +1,16 @@ +export const substitute = { + from: 'stackable/number-box', + transform: oldAttributes => { + return [ + 'core/paragraph', + { + fontSize: 'xx-large', + content: oldAttributes.text, + align: oldAttributes.contentAlign, + style: { spacing: { margin: { top: '0', bottom: '0' } } }, + }, + ] + }, +} + +export default substitute diff --git a/src/block/posts/index.js b/src/block/posts/index.js index 51acf5b8e7..7b88491292 100644 --- a/src/block/posts/index.js +++ b/src/block/posts/index.js @@ -13,6 +13,7 @@ import metadata from './block.json' import example from './example' import { BlogPostsIcon } from '~stackable/icons' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -30,4 +31,5 @@ export const settings = { variations, edit, save, + substitute, } diff --git a/src/block/posts/substitute.js b/src/block/posts/substitute.js new file mode 100644 index 0000000000..8d70041f1a --- /dev/null +++ b/src/block/posts/substitute.js @@ -0,0 +1,49 @@ +export const substitute = { + from: 'stackable/posts', + variants: [], + to: [ 'core/query' ], + transform: oldAttributes => { + return [ + 'core/query', + { + queryId: 0, + query: { + perPage: 10, + pages: 0, + offset: 0, + postType: 'post', + order: 'desc', + orderBy: 'date', + author: '', + search: '', + exclude: [], + sticky: '', + inherit: false, + taxQuery: null, + parents: [], + format: [], + }, + layout: { type: 'default' }, + }, + [ + [ + 'core/post-template', + { + layout: { + type: 'grid', + columnCount: oldAttributes.columns, + }, + }, + [ + [ 'post-featured-image', { aspectRatio: '1' } ], + [ 'post-title', {} ], + [ 'post-date', {} ], + [ 'post-excerpt', { moreText: 'Continue Reading' } ], + ], + ], + ], + ] + }, +} + +export default substitute diff --git a/src/block/price/block.json b/src/block/price/block.json index c907c9bbb5..11941dd7ac 100644 --- a/src/block/price/block.json +++ b/src/block/price/block.json @@ -17,5 +17,8 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/price-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/price-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/text" + ] } diff --git a/src/block/price/index.js b/src/block/price/index.js index 200fd9cbd3..0fc67fcc67 100644 --- a/src/block/price/index.js +++ b/src/block/price/index.js @@ -15,6 +15,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -28,4 +29,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/price/substitute.js b/src/block/price/substitute.js new file mode 100644 index 0000000000..ee75649e85 --- /dev/null +++ b/src/block/price/substitute.js @@ -0,0 +1,16 @@ +export const substitute = { + from: 'stackable/price', + transform: ( oldAttributes, innerBlocks ) => { + let content = '' + for ( const block of innerBlocks ) { + content += block[ 1 ].text + } + return [ 'core/paragraph', { + fontSize: 'x-large', + align: oldAttributes.contentAlign, + content, + } ] + }, +} + +export default substitute diff --git a/src/block/pricing-box/block.json b/src/block/pricing-box/block.json index 4e4deb83d9..a7c39c5952 100644 --- a/src/block/pricing-box/block.json +++ b/src/block/pricing-box/block.json @@ -15,5 +15,16 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/pricing-table-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/pricing-table-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/price", + "stackable/text", + "stackable/icon-list" + ], + "stk-substitution-blocks": [ + "stackable/heading", + "stackable/subtitle", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/pricing-box/substitute.js b/src/block/pricing-box/substitute.js new file mode 100644 index 0000000000..fb0a94e8ff --- /dev/null +++ b/src/block/pricing-box/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/pricing-box', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { + ...oldAttributes, + }, + [ [ 'stackable/column', {}, innerBlocks ] ], + ] + }, +} + +export default substitute diff --git a/src/block/separator/substitute.js b/src/block/separator/substitute.js new file mode 100644 index 0000000000..9f0db44bf5 --- /dev/null +++ b/src/block/separator/substitute.js @@ -0,0 +1,10 @@ +export const substitute = { + from: 'stackable/separator', + transform: () => { + return [ + 'core/separator', + ] + }, +} + +export default substitute diff --git a/src/block/spacer/substitute.js b/src/block/spacer/substitute.js new file mode 100644 index 0000000000..b78731a7c5 --- /dev/null +++ b/src/block/spacer/substitute.js @@ -0,0 +1,13 @@ +export const substitute = { + from: 'stackable/spacer', + transform: oldAttributes => { + return [ + 'core/spacer', + { + height: `${ oldAttributes?.height ?? 50 }px`, + }, + ] + }, +} + +export default substitute diff --git a/src/block/subtitle/index.js b/src/block/subtitle/index.js index 77575ad27c..b5692f142b 100644 --- a/src/block/subtitle/index.js +++ b/src/block/subtitle/index.js @@ -16,6 +16,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -40,4 +41,5 @@ export const settings = { ( ( attributesToMerge.hasOwnProperty( 'content' ) ? attributesToMerge.content : attributesToMerge.text ) || '' ), } }, + substitute, } diff --git a/src/block/subtitle/substitute.js b/src/block/subtitle/substitute.js new file mode 100644 index 0000000000..b61967faed --- /dev/null +++ b/src/block/subtitle/substitute.js @@ -0,0 +1,15 @@ +export const substitute = { + from: 'stackable/subtitle', + transform: oldAttributes => { + return [ + 'core/paragraph', + { + fontSize: 'small', + align: oldAttributes.contentAlign, + content: oldAttributes.text, + }, + ] + }, +} + +export default substitute diff --git a/src/block/table-of-contents/substitute.js b/src/block/table-of-contents/substitute.js new file mode 100644 index 0000000000..4994836a5e --- /dev/null +++ b/src/block/table-of-contents/substitute.js @@ -0,0 +1,19 @@ +export const substitute = { + from: 'stackable/table-of-contents', + transform: oldAttributes => { + return [ + 'stackable/columns', { ...oldAttributes }, [ + [ 'stackable/column', {}, [ + [ 'stackable/text', { text: 'Table of Contents' } ], + [ 'core/list', {}, [ + [ 'core/list-item', { content: 'First line of content' } ], + [ 'core/list-item', { content: 'Second line of content' } ], + [ 'core/list-item', { content: 'Third line of content' } ], + ] ], + ] ], + ], + ] + }, +} + +export default substitute diff --git a/src/block/tabs/substitute.js b/src/block/tabs/substitute.js new file mode 100644 index 0000000000..672118b6f8 --- /dev/null +++ b/src/block/tabs/substitute.js @@ -0,0 +1,26 @@ +export const substitute = { + from: 'stackable/tabs', + transform: ( oldAttributes, innerBlocks ) => { + const labels = innerBlocks[ 0 ][ 1 ]?.tabLabels + const contents = innerBlocks[ 1 ][ 2 ] + + const insideBlocks = [] + + labels.forEach( ( label, index ) => { + insideBlocks.push( [ 'stackable/heading', { text: label?.label } ] ) + if ( contents[ index ] ) { + insideBlocks.push( contents[ index ] ) + } + } ) + + return [ + 'stackable/columns', + { + ...oldAttributes, + }, + [ [ 'stackable/column', {}, insideBlocks ] ], + ] + }, +} + +export default substitute diff --git a/src/block/team-member/block.json b/src/block/team-member/block.json index 6c108fd96b..aed41f9f9d 100644 --- a/src/block/team-member/block.json +++ b/src/block/team-member/block.json @@ -10,5 +10,13 @@ }, "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/team-member-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/team-member-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-substitution-blocks": [ + "stackable/image", + "stackable/heading", + "stackable/subtitle", + "stackable/text", + "stackable/button-group", + "stackable/button" + ] } diff --git a/src/block/team-member/index.js b/src/block/team-member/index.js index 702d64ec5e..1eb552d5b4 100644 --- a/src/block/team-member/index.js +++ b/src/block/team-member/index.js @@ -11,6 +11,7 @@ import save from './save' import schema from './schema' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' /** * External dependencies @@ -40,4 +41,5 @@ export const settings = { deprecated, edit, save, + substitute, } diff --git a/src/block/team-member/substitute.js b/src/block/team-member/substitute.js new file mode 100644 index 0000000000..cc1abc9f0d --- /dev/null +++ b/src/block/team-member/substitute.js @@ -0,0 +1,18 @@ +export const substitute = { + from: 'stackable/team-member', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + {}, + [ + [ + 'stackable/column', + {}, + innerBlocks, + ], + ], + ] + }, +} + +export default substitute diff --git a/src/block/testimonial/block.json b/src/block/testimonial/block.json index 5ff892c680..b19f8e6344 100644 --- a/src/block/testimonial/block.json +++ b/src/block/testimonial/block.json @@ -10,5 +10,14 @@ }, "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "section", - "stk-demo": "https://wpstackable.com/testimonial-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/testimonial-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/image-box" + ], + "stk-substitution-blocks": [ + "stackable/image", + "stackable/heading", + "stackable/subtitle", + "stackable/text" + ] } diff --git a/src/block/testimonial/substitute.js b/src/block/testimonial/substitute.js new file mode 100644 index 0000000000..c82fdf23a1 --- /dev/null +++ b/src/block/testimonial/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/testimonial', + transform: ( oldAttributes, innerBlocks ) => { + return [ + 'stackable/columns', + { + ...oldAttributes, + }, + [ [ 'stackable/column', { hasContainer: true }, innerBlocks ] ], + ] + }, +} + +export default substitute diff --git a/src/block/text/index.js b/src/block/text/index.js index f68753c4a8..a3fe924ba7 100644 --- a/src/block/text/index.js +++ b/src/block/text/index.js @@ -16,6 +16,7 @@ import schema from './schema' import metadata from './block.json' import example from './example' import deprecated from './deprecated' +import substitute from './substitute' export const settings = { ...metadata, @@ -40,4 +41,5 @@ export const settings = { ( ( attributesToMerge.hasOwnProperty( 'content' ) ? attributesToMerge.content : attributesToMerge.text ) || '' ), } }, + substitute, } diff --git a/src/block/text/substitute.js b/src/block/text/substitute.js new file mode 100644 index 0000000000..f013a795c9 --- /dev/null +++ b/src/block/text/substitute.js @@ -0,0 +1,14 @@ +export const substitute = { + from: 'stackable/text', + transform: oldAttributes => { + return [ + 'core/paragraph', + { + content: oldAttributes?.text, + align: oldAttributes?.contentAlign, + }, + ] + }, +} + +export default substitute diff --git a/src/block/timeline/edit.js b/src/block/timeline/edit.js index b5edcf3f38..aadde8f05d 100644 --- a/src/block/timeline/edit.js +++ b/src/block/timeline/edit.js @@ -42,6 +42,8 @@ import { withQueryLoopContext, } from '~stackable/higher-order' import { range } from 'lodash' +import { substituteCoreIfDisabled } from '~stackable/util' +import { substitutionRules } from '../../blocks' /** * WordPress dependencies @@ -68,9 +70,12 @@ const TEMPLATE = [ left: 0, }, }, [ - [ 'stackable/text', { - text: _x( 'Description for this block. Use this space for describing your block. Any text will do.', 'Content placeholder', i18n ), - } ], + substituteCoreIfDisabled( + 'stackable/text', + { text: _x( 'Description for this block. Use this space for describing your block. Any text will do.', 'Content placeholder', i18n ) }, + [], + substitutionRules, + ), ] ], ] diff --git a/src/block/timeline/substitute.js b/src/block/timeline/substitute.js new file mode 100644 index 0000000000..b3a3b350eb --- /dev/null +++ b/src/block/timeline/substitute.js @@ -0,0 +1,24 @@ +export const substitute = { + from: 'stackable/timeline', + transform: ( oldAttributes, innerBlocks ) => { + const formattedDate = new Date().toLocaleDateString( 'en-US', { + month: 'short', + day: '2-digit', + year: 'numeric', + } ) + return [ + 'stackable/columns', + { + ...oldAttributes, + }, + [ + [ 'stackable/column', {}, innerBlocks ], + [ 'stackable/column', {}, [ + [ 'stackable/text', { text: formattedDate } ], + ] ], + ], + ] + }, +} + +export default substitute diff --git a/src/block/video-popup/block.json b/src/block/video-popup/block.json index 958aa67679..8e7a006b37 100644 --- a/src/block/video-popup/block.json +++ b/src/block/video-popup/block.json @@ -12,5 +12,9 @@ ], "textdomain": "stackable-ultimate-gutenberg-blocks", "stk-type": "special", - "stk-demo": "https://wpstackable.com/video-popup-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink" + "stk-demo": "https://wpstackable.com/video-popup-block/?utm_source=welcome&utm_medium=settings&utm_campaign=view_demo&utm_content=demolink", + "stk-required-blocks": [ + "stackable/icon", + "stackable/image" + ] } diff --git a/src/block/video-popup/substitute.js b/src/block/video-popup/substitute.js new file mode 100644 index 0000000000..79adb1ae8a --- /dev/null +++ b/src/block/video-popup/substitute.js @@ -0,0 +1,13 @@ +export const substitute = { + from: 'stackable/video-popup', + transform: oldAttributes => { + return [ + 'core/video', + { + src: oldAttributes?.oldAttributes, + }, + ] + }, +} + +export default substitute diff --git a/src/blocks.js b/src/blocks.js index 6bdc8e1ee0..f0944f1417 100644 --- a/src/blocks.js +++ b/src/blocks.js @@ -12,9 +12,17 @@ import './disabled-blocks' /** * External dependencies */ -import { i18n } from 'stackable' -import { addStackableBlockCategory, registerBlockType } from '~stackable/util' +import { + i18n, + settings as stackableSettings, +} from 'stackable' +import { + addStackableBlockCategory, + registerBlockType, + BLOCK_STATE, +} from '~stackable/util' import { withVisualGuideContext } from '~stackable/higher-order' +import { omit } from 'lodash' /** * WordPress dependencies @@ -26,6 +34,19 @@ import { addFilter } from '@wordpress/hooks' // Register our block category. addStackableBlockCategory() +// Fetch all substitution rules before registering +const fetchSubstitutionRules = r => { + const substitutionRules = {} + r.keys().forEach( key => { + const { substitute } = r( key ) + if ( ! substitute ) { + return + } + substitutionRules[ substitute.from ] = omit( substitute, 'from' ) + } ) + return substitutionRules +} + // Register all the blocks found const importAllAndRegister = r => { r.keys().forEach( key => { @@ -45,9 +66,15 @@ const importAllAndRegister = r => { settings.keywords = settings.keywords.map( keyword => __( keyword, i18n ) ) // eslint-disable-line @wordpress/i18n-no-variables } - // Register the block. + // Register the block if it's not already registered and not disabled. if ( ! getBlockType( name ) ) { - registerBlockType( name, settings ) + // Register the block if the block is not disabled. + if ( ! ( ( settings[ 'stk-block-dependency' ] in stackableSettings.stackable_block_states && + stackableSettings.stackable_block_states[ settings[ 'stk-block-dependency' ] ] === BLOCK_STATE.DISABLED ) || + stackableSettings.stackable_block_states[ name ] === BLOCK_STATE.DISABLED + ) ) { + registerBlockType( name, settings ) + } } } ) } @@ -58,4 +85,6 @@ addFilter( 'stackable.registerBlockType.edit', 'stackable', edit => { return withVisualGuideContext( edit ) } ) +export const substitutionRules = fetchSubstitutionRules( require.context( './block', true, /substitute\.js$/ ) ) + importAllAndRegister( require.context( './block', true, /index\.js$/ ) ) diff --git a/src/components/admin-base-setting/editor.scss b/src/components/admin-base-setting/editor.scss index 6df6ae9129..06c606ef4b 100644 --- a/src/components/admin-base-setting/editor.scss +++ b/src/components/admin-base-setting/editor.scss @@ -1,7 +1,6 @@ .ugb-admin-setting:not(:last-child) { padding-bottom: 32px; - border-bottom: 1px solid #eaecf0; - margin-bottom: 32px; + margin-bottom: 16px; &.ugb-admin-setting--small { padding-bottom: 10px; margin-bottom: 10px; @@ -16,9 +15,9 @@ } .ugb-admin-setting__label { width: auto !important; + flex-basis: 250px; } .ugb-admin-setting__field { - flex: 0; white-space: nowrap; } } diff --git a/src/components/admin-base-setting/index.js b/src/components/admin-base-setting/index.js index 21d78b9e63..3ba23258b9 100644 --- a/src/components/admin-base-setting/index.js +++ b/src/components/admin-base-setting/index.js @@ -4,12 +4,15 @@ import classnames from 'classnames' let i = 1 const AdminBaseSetting = props => { + const { showLabel = true } = props const [ uid ] = useState( `ugb-admin-setting-${ i++ }` ) + const isSearched = props.searchedSettings ? props.searchedSettings.includes( props.label ) : true const mainClasses = classnames( [ 'ugb-admin-setting', props.className, ], { [ `ugb-admin-setting--${ props.size }` ]: props.size, + 'ugb-admin-setting--not-highlight': ! isSearched, } ) return ( @@ -19,7 +22,7 @@ const AdminBaseSetting = props => { htmlFor={ uid } onClick={ props.onClick } > - { !! props.label && { props.label } } + { !! props.label && showLabel && { props.label } }
{ props.children }
diff --git a/src/components/admin-toolbar-setting/editor.scss b/src/components/admin-toolbar-setting/editor.scss new file mode 100644 index 0000000000..3bc31f6d86 --- /dev/null +++ b/src/components/admin-toolbar-setting/editor.scss @@ -0,0 +1,40 @@ +.ugb-admin-toolbar-setting__wrapper { + display: grid; + h3 { + margin: 0 0 0.25em; + } + .ugb-admin-toolbar-setting__group-wrapper { + grid-column: 2 / 3; + grid-row: 1 / 3; + align-items: center; + display: flex; + justify-content: right; + } + a { + margin-right: 10px; + width: 80px; + } + .ugb-admin-toolbar-setting { + display: flex; + background: #ebebeb; + padding: 3px; + border-radius: 50px; + .ugb-button-component { + background: transparent; + box-shadow: none; + border-radius: 50px; + gap: 8px; + padding: 0 16px; + &:not(.is-pressed) { + background: transparent; + color: #575757; + } + &.is-pressed { + background: #fff; + color: #1e1e1e; + box-shadow: 0 1px 3px #1018281a, 0 1px 2px #1018280f; + font-weight: bold; + } + } + } +} diff --git a/src/components/admin-toolbar-setting/index.js b/src/components/admin-toolbar-setting/index.js new file mode 100644 index 0000000000..cf848dcd22 --- /dev/null +++ b/src/components/admin-toolbar-setting/index.js @@ -0,0 +1,86 @@ +import AdminBaseSetting from '../admin-base-setting' +import Button from '../button' +import { ButtonGroup } from '@wordpress/components' +import { __ } from '@wordpress/i18n' +import { i18n } from 'stackable' + +const AdminToolbarSetting = props => { + return ( + +
+

{ props.label }

+ ev.stopPropagation() } + > + { __( 'view demo', i18n ) } + +
+ { + const isSelected = props.value ? props.value === option.value : props.placeholder === option.value + const tabindex = isSelected ? '0' : '-1' + // If no available states provided, all states are allowed. + const isAvailable = props.availableStates ? props.availableStates.includes( option.value ) : true + + // Do not show the button if not available. + if ( ! isAvailable ) { + return null + } + + return
+
+
+ ) +} + +AdminToolbarSetting.defaultProps = { + controls: [], + label: '', + value: '', + onChange: () => {}, +} + +export default AdminToolbarSetting diff --git a/src/deprecated/editor-settings.php b/src/deprecated/editor-settings.php new file mode 100644 index 0000000000..05ec4fc6bd --- /dev/null +++ b/src/deprecated/editor-settings.php @@ -0,0 +1,34 @@ +

(V2)

diff --git a/src/deprecated/v2/optimization-settings.php b/src/deprecated/v2/optimization-settings.php index 294f3be121..0a57a88c8a 100644 --- a/src/deprecated/v2/optimization-settings.php +++ b/src/deprecated/v2/optimization-settings.php @@ -35,7 +35,7 @@ function __construct() { } // Add the optimization setting. - add_action( 'stackable_settings_page_mid', array( $this, 'add_optimization_settings' ) ); + // add_action( 'stackable_settings_page_mid', array( $this, 'add_optimization_settings' ) ); } } diff --git a/src/deprecated/v2/welcome/admin.js b/src/deprecated/v2/welcome/admin.js index 42bf9822f7..fe4de05990 100644 --- a/src/deprecated/v2/welcome/admin.js +++ b/src/deprecated/v2/welcome/admin.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import blockData from './blocks' - /** * WordPress dependencies */ @@ -11,7 +6,6 @@ import { Component, useEffect, useState, Fragment, } from '@wordpress/element' import { send as ajaxSend } from '@wordpress/ajax' -import domReady from '@wordpress/dom-ready' import { Spinner } from '@wordpress/components' import { loadPromise, models } from '@wordpress/api' @@ -19,14 +13,12 @@ import { loadPromise, models } from '@wordpress/api' * External dependencies */ import { - v2disabledBlocks as disabledBlocks, i18n, v2nonce as nonce, } from 'stackable' import AdminToggleSetting from '~stackable/components/admin-toggle-setting' -import { createRoot } from '~stackable/util/element' -class BlockToggler extends Component { +export class BlockToggler extends Component { constructor() { super( ...arguments ) this.toggleBlock = this.toggleBlock.bind( this ) @@ -81,7 +73,7 @@ class BlockToggler extends Component { } render() { - const { blocks: blockData } = this.props + const { blocks: blockData, searchedSettings: searchedSettings } = this.props return (
@@ -105,6 +97,7 @@ class BlockToggler extends Component { this.toggleBlock( blockName ) } size="small" @@ -119,7 +112,7 @@ class BlockToggler extends Component { } } -const OptimizationSettings = () => { +export const OptimizationSettings = ( { searchSettings } ) => { const [ optimizeScriptLoad, setOptimizeScriptLoad ] = useState( false ) useEffect( () => { @@ -140,6 +133,7 @@ const OptimizationSettings = () => { return { /> } - -// Load all the options into the UI. -domReady( () => { - if ( document.querySelector( '.s-settings-wrapper-v2' ) ) { - createRoot( document.querySelector( '.s-settings-wrapper-v2' ) ).render( - - ) - } - - if ( document.querySelector( '.s-optimization-settings' ) ) { - createRoot( document.querySelector( '.s-optimization-settings' ) ).render( - - ) - } -} ) diff --git a/src/disabled-blocks.js b/src/disabled-blocks.js index 039350262b..ffdd7917ae 100644 --- a/src/disabled-blocks.js +++ b/src/disabled-blocks.js @@ -1,40 +1,68 @@ /** - * Filter that modified the metadata of the blocks to disable blocks and + * Filter that modified the metadata of the blocks to hide blocks and * variations depending on the settings of the user. */ import { settings } from 'stackable' import { addFilter } from '@wordpress/hooks' - -// Disable these blocks when the following variables are disabled. We need this -// so that if a variation is disabled, we will no longer be able to add the -// relevant block. -const BLOCK_DEPENDENCIES = { - 'stackable/icon-button': 'stackable/button-group|icon-button', - 'stackable/button': 'stackable/button-group|button', -} +import { + BLOCK_STATE, + substituteCoreIfDisabled, +} from '~stackable/util' +import { substitutionRules } from './blocks' +import { cloneDeep } from 'lodash' const getDefaultVariation = variations => { return variations?.find( ( { isDefault } ) => isDefault )?.name } const getVariationsToRemove = ( disabledBlocks, blockName ) => { - return disabledBlocks.filter( disabledBlock => disabledBlock.startsWith( `${ blockName }|` ) ) - .map( disabledBlock => disabledBlock.split( '|' )[ 1 ] ) + const variations = [] + for ( const block in disabledBlocks ) { + if ( block.startsWith( `${ blockName }|` ) ) { + variations.push( block.split( '|' )[ 1 ] ) + } + } + return variations +} + +// Traverse the innerblocks of a given block definition and substitute core blocks if disabled and whitelisted. +const traverseBlocksAndSubstitute = ( blocks, whitelist ) => { + return blocks.map( block => { + let [ blockName, blockAttributes, innerBlocks ] = block + + // If there are innerBlocks, recursively traverse them. + if ( innerBlocks && innerBlocks.length > 0 ) { + innerBlocks = traverseBlocksAndSubstitute( innerBlocks, whitelist ) + } + + if ( whitelist.includes( blockName ) ) { + return substituteCoreIfDisabled( blockName, blockAttributes, innerBlocks, substitutionRules ) + } + + if ( innerBlocks ) { + return [ blockName, blockAttributes, innerBlocks ] + } + return [ blockName, blockAttributes ] + } ) } const applySettingsToMeta = metadata => { - let inserter = ! settings.stackable_disabled_blocks.includes( metadata.name ) + const disabledBlocks = settings.stackable_block_states || {} // eslint-disable-line camelcase + let inserter = true + + // If the block is hidden, set the inserter to false. + if ( metadata.name in disabledBlocks ) { + inserter = ! disabledBlocks[ metadata.name ] === BLOCK_STATE.HIDDEN + } // Check if this block is dependent on another variation being enabled. - if ( BLOCK_DEPENDENCIES[ metadata.name ] ) { - if ( settings.stackable_disabled_blocks.includes( BLOCK_DEPENDENCIES[ metadata.name ] ) ) { - inserter = false - } + if ( metadata[ 'stk-block-dependency' ] && metadata[ 'stk-block-dependency' ] in disabledBlocks ) { + inserter = ! disabledBlocks[ metadata[ 'stk-block-dependency' ] ] === BLOCK_STATE.HIDDEN } - const variationsToRemove = getVariationsToRemove( settings.stackable_disabled_blocks, metadata.name ) + const variationsToRemove = getVariationsToRemove( disabledBlocks, metadata.name ) let variations = metadata.variations || [] - // Remove variations if there are ones disabled. + // Remove the variations that are hidden which removes the block from the inserter. if ( variationsToRemove.length ) { const hasDefaultVariation = !! getDefaultVariation( metadata.variations ) variations = variations.filter( variation => ! variationsToRemove.includes( variation.name ) ) @@ -53,6 +81,17 @@ const applySettingsToMeta = metadata => { } } + const whitelist = metadata[ 'stk-substitution-blocks' ] + if ( whitelist ) { + variations = variations.map( variation => { + const newVariation = cloneDeep( variation ) + if ( newVariation.innerBlocks && Array.isArray( newVariation.innerBlocks ) ) { + newVariation.innerBlocks = traverseBlocksAndSubstitute( newVariation.innerBlocks, whitelist ) + } + return newVariation + } ) + } + // Adjust the metadata. metadata.variations = variations if ( typeof metadata.supports === 'undefined' ) { diff --git a/src/editor-settings.php b/src/editor-settings.php index 072499d06a..bb9c061d7f 100644 --- a/src/editor-settings.php +++ b/src/editor-settings.php @@ -32,17 +32,20 @@ function __construct() { public function register_settings() { register_setting( 'stackable_editor_settings', - 'stackable_disabled_blocks', + 'stackable_block_states', + // Use an object to store the block names as keys and the value that represents if disabled or hidden. + // Enabled blocks are not stored in the object to save memory. array( - 'type' => 'array', + 'type' => 'object', 'description' => __( 'Blocks that should be hidden in the block editor', STACKABLE_I18N ), 'sanitize_callback' => array( $this, 'sanitize_array_setting' ), 'show_in_rest' => array( 'schema' => array( - 'items' => array( - 'type' => 'string', - ) - ) + 'type' => 'object', + 'additionalProperties' => array( + 'type' => 'number', + ), + ), ), 'default' => array(), ) @@ -120,6 +123,18 @@ public function register_settings() { ) ); + register_setting( + 'stackable_editor_settings', + 'stackable_enable_global_settings', + array( + 'type' => 'boolean', + 'description' => __( 'Allow the configuration of global settings such as color palette, typography, and block defaults', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + register_setting( 'stackable_editor_settings', 'stackable_enable_block_linking', @@ -154,7 +169,67 @@ public function register_settings() { 'default' => true, ) ); - + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_text_highlight', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for highlighting text', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_dynamic_content', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for inserting dynamic content', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_copy_paste_styles', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for copying and pasting block styles', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_reset_layout', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for resetting the layout of a block', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + + register_setting( + 'stackable_editor_settings', + 'stackable_enable_save_as_default_block', + array( + 'type' => 'boolean', + 'description' => __( 'Adds a toolbar button for saving the current block variation as the default block', STACKABLE_I18N ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => true, + ) + ); + register_setting( 'stackable_editor_settings', 'stackable_enable_text_default_block', @@ -180,13 +255,20 @@ public function sanitize_array_setting( $input ) { */ public function add_settings( $settings ) { $settings['stackable_google_maps_api_key'] = get_option( 'stackable_google_maps_api_key' ); - $settings['stackable_disabled_blocks'] = get_option( 'stackable_disabled_blocks' ); + $settings['stackable_block_states'] = get_option( 'stackable_block_states' ); $settings['stackable_enable_design_library'] = get_option( 'stackable_enable_design_library' ); $settings['stackable_optimize_inline_css'] = get_option( 'stackable_optimize_inline_css' ); $settings['stackable_auto_collapse_panels'] = get_option( 'stackable_auto_collapse_panels' ); + $settings['stackable_enable_global_settings'] = get_option( 'stackable_enable_global_settings' ); $settings['stackable_enable_block_linking'] = get_option( 'stackable_enable_block_linking' ); $settings['stackable_enable_carousel_lazy_loading'] = get_option( 'stackable_enable_carousel_lazy_loading' ); + $settings['stackable_enable_text_highlight'] = get_option( 'stackable_enable_text_highlight' ); + $settings['stackable_enable_dynamic_content'] = get_option( 'stackable_enable_dynamic_content' ); + $settings['stackable_enable_copy_paste_styles'] = get_option( 'stackable_enable_copy_paste_styles' ); + $settings['stackable_enable_reset_layout'] = get_option( 'stackable_enable_reset_layout' ); + $settings['stackable_enable_save_as_default_block'] = get_option( 'stackable_enable_save_as_default_block' ); $settings['stackable_enable_text_default_block'] = get_option( 'stackable_enable_text_default_block' ); + return $settings; } diff --git a/src/format-types/highlight/index.js b/src/format-types/highlight/index.js index 3106e69881..6666730842 100644 --- a/src/format-types/highlight/index.js +++ b/src/format-types/highlight/index.js @@ -5,7 +5,7 @@ import { ColorPaletteControl, AdvancedToolbarControl, Popover, } from '~stackable/components' import { whiteIfDarkBlackIfLight } from '~stackable/util' -import { i18n } from 'stackable' +import { i18n, settings } from 'stackable' /** * WordPress dependencies @@ -256,17 +256,19 @@ const HighlightButton = props => { ) } -registerFormatType( - 'stk/highlight', { - title: __( 'Highlight Text', i18n ), - tagName: 'span', - className: 'stk-highlight', - edit: HighlightButton, - attributes: { - style: 'style', - }, - } -) +if ( settings.stackable_enable_text_highlight ) { + registerFormatType( + 'stk/highlight', { + title: __( 'Highlight Text', i18n ), + tagName: 'span', + className: 'stk-highlight', + edit: HighlightButton, + attributes: { + style: 'style', + }, + } + ) +} // Backward compatibility, ugb/highlight, but this is not visible. registerFormatType( diff --git a/src/plugins/global-settings/index.js b/src/plugins/global-settings/index.js index 2bf5fcc5cf..bb57dc12c6 100644 --- a/src/plugins/global-settings/index.js +++ b/src/plugins/global-settings/index.js @@ -9,7 +9,11 @@ import './icon-library' * External dependencies */ import { SVGStackableIcon } from '~stackable/icons' -import { i18n, isContentOnlyMode } from 'stackable' +import { + i18n, + isContentOnlyMode, + settings, +} from 'stackable' /** WordPress dependencies */ @@ -48,7 +52,7 @@ const GlobalSettings = () => { ) } -if ( ! isContentOnlyMode ) { +if ( ! isContentOnlyMode && settings.stackable_enable_global_settings ) { registerPlugin( 'stackable-global-settings', { render: GlobalSettings, } ) diff --git a/src/plugins/layout-picker-reset/index.js b/src/plugins/layout-picker-reset/index.js index eae9c27e21..236a54baf3 100644 --- a/src/plugins/layout-picker-reset/index.js +++ b/src/plugins/layout-picker-reset/index.js @@ -1,7 +1,9 @@ /** * External dependencies */ -import { i18n, isContentOnlyMode } from 'stackable' +import { + i18n, isContentOnlyMode, settings, +} from 'stackable' // import { Button } from '~stackable/components' /** @@ -55,7 +57,7 @@ if ( ! isContentOnlyMode ) { return ( <> - { hasVariations && hasLayoutReset && ( + { settings.stackable_enable_reset_layout && hasVariations && hasLayoutReset && ( ) } diff --git a/src/plugins/save-block/index.js b/src/plugins/save-block/index.js index a73cc3b754..bb8c594de4 100644 --- a/src/plugins/save-block/index.js +++ b/src/plugins/save-block/index.js @@ -6,6 +6,7 @@ import './variation-picker' import './custom-block-styles-editor' import SaveMenu from './save-menu' import { useSavedDefaultBlockStyle } from '~stackable/hooks' +import { settings } from 'stackable' /** * WordPress dependencies @@ -25,7 +26,9 @@ const _SaveMenu = withSelect( select => { } } )( SaveMenu ) -registerPlugin( 'stackable-save-block-menu', { render: _SaveMenu } ) +if ( settings.stackable_enable_save_as_default_block ) { + registerPlugin( 'stackable-save-block-menu', { render: _SaveMenu } ) +} /** * Add the block style loader to each Stackable block. diff --git a/src/util/blocks.js b/src/util/blocks.js index ccdcda2786..7bb1b515aa 100644 --- a/src/util/blocks.js +++ b/src/util/blocks.js @@ -13,7 +13,11 @@ import { orderBy, last, } from 'lodash' -import { blockCategoryIndex, i18n } from 'stackable' +import { + blockCategoryIndex, + i18n, + settings as stackableSettings, +} from 'stackable' /** * WordPress dependencies @@ -33,6 +37,15 @@ import { useMemo } from '@wordpress/element' import { BlockIcon } from '@wordpress/block-editor' import { __ } from '@wordpress/i18n' +/** + * Enum for disabling and hiding blocks. + */ +export const BLOCK_STATE = Object.freeze( { + ENABLED: 1, + HIDDEN: 2, + DISABLED: 3, +} ) + /** * Converts the registered block name into a block name string that can be used in hook names or ids. * @@ -504,3 +517,33 @@ export const registerBlockType = ( name, _settings ) => { settings = applyFilters( `stackable.${ name.replace( 'stackable/', '' ) }.settings`, settings ) _registerBlockType( name, settings ) } + +/** + * Substitutes a stackable block with an equivalent core block if the block is disabled. + * + * @param {string} blockName The block name + * @param {Object} blockAttributes The block attributes + * @param {Array} innerBlocks The children blocks + * @param {Object} substitutionRules The substitution rules for transforming from stackable to core blocks + * + * @return {Array} The resulting block definition + */ +export const substituteCoreIfDisabled = ( blockName, blockAttributes, innerBlocks, substitutionRules ) => { + const disabledBlocks = stackableSettings.stackable_block_states || {} // eslint-disable-line camelcase + + if ( substitutionRules && blockName in substitutionRules ) { + const substitutionRule = substitutionRules[ blockName ] + // If a block have variants, let the the transform handle checking for disabled + if ( 'variants' in substitutionRule ) { + return substitutionRule.transform( blockAttributes, innerBlocks, disabledBlocks ) + } + if ( blockName in disabledBlocks && disabledBlocks[ blockName ] === BLOCK_STATE.DISABLED ) { // eslint-disable-line camelcase + return substitutionRule.transform( blockAttributes, innerBlocks ) + } + } + + if ( innerBlocks ) { + return [ blockName, blockAttributes, innerBlocks ] + } + return [ blockName, blockAttributes, [] ] +} diff --git a/src/util/colors.js b/src/util/colors.js index 21507ca7b1..df5aed0645 100644 --- a/src/util/colors.js +++ b/src/util/colors.js @@ -90,6 +90,9 @@ export const extractColor = value => { * @return {string} color in #RRGGBBAA format */ export const colorOpacityToHexAplha = ( color, opacity ) => { + if ( ! color ) { + return '' + } if ( color.startsWith( '#' ) ) { // Get the first 6 hex digits. const hex = color.slice( 0, 7 ) diff --git a/src/welcome/admin.js b/src/welcome/admin.js index 42d6987e24..7dada873ed 100644 --- a/src/welcome/admin.js +++ b/src/welcome/admin.js @@ -9,12 +9,14 @@ import SVGSectionIcon from './images/settings-icon-section.svg' /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n' +import { __, sprintf } from '@wordpress/i18n' import { - useEffect, useState, Fragment, useCallback, + useEffect, useState, useCallback, useMemo, lazy, Suspense, } from '@wordpress/element' import domReady from '@wordpress/dom-ready' -import { Spinner, CheckboxControl } from '@wordpress/components' +import { + Button, Flex, FlexItem, Spinner, CheckboxControl, Modal, +} from '@wordpress/components' import { loadPromise, models } from '@wordpress/api' import { applyFilters } from '@wordpress/hooks' @@ -24,6 +26,8 @@ import { applyFilters } from '@wordpress/hooks' import { i18n, showProNoticesOption, + isPro, + v2disabledBlocks, } from 'stackable' import classnames from 'classnames' import { importBlocks } from '~stackable/util/admin' @@ -31,9 +35,14 @@ import { createRoot } from '~stackable/util/element' import AdminSelectSetting from '~stackable/components/admin-select-setting' import AdminToggleSetting from '~stackable/components/admin-toggle-setting' import AdminTextSetting from '~stackable/components/admin-text-setting' +import AdminToolbarSetting from '~stackable/components/admin-toolbar-setting' import { GettingStarted } from './getting-started' +import { BLOCK_STATE } from '~stackable/util/blocks' +import { BlockToggler, OptimizationSettings } from '~stackable/deprecated/v2/welcome/admin' +import blockData from '~stackable/deprecated/v2/welcome/blocks' const FREE_BLOCKS = importBlocks( require.context( '../block', true, /block\.json$/ ) ) + export const getAllBlocks = () => applyFilters( 'stackable.settings.blocks', FREE_BLOCKS ) export const BLOCK_CATEROGIES = [ @@ -57,6 +66,208 @@ export const BLOCK_CATEROGIES = [ }, ] +const SEARCH_TREE = [ + { + id: 'editor-settings', + label: __( 'Editor Settings', i18n ), + groups: [ + { + id: 'blocks', + children: [ + __( 'Nested Block Width', i18n ), + __( 'Nested Wide Block Width', i18n ), + __( 'Stackable Text as Default Block', i18n ), + ], + }, + { + id: 'editor', + children: [ + __( 'Design Library', i18n ), + __( 'Stackable Settings', i18n ), + __( 'Block Linking (Beta)', i18n ), + ], + }, + { + id: 'toolbar', + children: [ + __( 'Toolbar Text Highlight', i18n ), + __( 'Toolbar Dynamic Content', i18n ), + __( 'Copy & Paste Styles', i18n ), + __( 'Reset Layout', i18n ), + __( 'Save as Default Block', i18n ), + ], + }, + { + id: 'inspector', + children: [ + __( 'Don\'t show help video tooltips', i18n ), + __( 'Auto-Collapse Panels', i18n ), + ], + }, + ], + }, + { + id: 'responsiveness', + label: __( 'Responsiveness', i18n ), + groups: [ + { + id: 'dynamic-breakpoints', + children: [ + __( 'Tablet Breakpoint', i18n ), + __( 'Mobile Breakpoint', i18n ), + ], + }, + ], + }, + { + id: 'blocks', + label: __( 'Blocks', i18n ), + // Categories are essential, special, and section + groups: BLOCK_CATEROGIES.map( ( { id } ) => { + const DERIVED_BLOCKS = getAllBlocks() + return { + id, + children: DERIVED_BLOCKS[ id ].map( block => { + return block.title + } ), + } + } ), + }, + { + id: 'optimizations', + label: __( 'Optimization', i18n ), + groups: [ + { + id: 'optimizations', + children: [ + __( 'Optimize Inline CSS', i18n ), + __( 'Lazy Load Images within Carousels', i18n ), + ], + }, + ], + }, + { + id: 'global-settings', + label: __( 'Global Settings', i18n ), + groups: [ + { + id: 'global-settings', + children: [ + __( 'Force Typography Styles', i18n ), + ], + }, + ], + }, + { + id: 'role-manager', + label: __( 'Role Manager', i18n ), + groups: [ + { + id: 'role-manager', + children: [ + __( 'Role Manager', i18n ), + __( 'Administrator', i18n ), + __( 'Editor', i18n ), + __( 'Author', i18n ), + __( 'Contributor', i18n ), + __( 'Subscriber', i18n ), + ], + }, + ], + }, + { + id: 'custom-fields-settings', + label: __( 'Custom Fields', i18n ), + groups: [ + { + id: 'custom-fields-settings', + children: [ + __( 'Custom Fields', i18n ), + __( 'Administrator', i18n ), + __( 'Editor', i18n ), + __( 'Author', i18n ), + __( 'Contributor', i18n ), + __( 'Subscriber', i18n ), + ], + }, + ], + }, + { + id: 'integrations', + label: __( 'Integration', i18n ), + groups: [ + { + id: 'integrations', + children: [ + __( 'Google Maps API Key', i18n ), + __( 'FontAwesome Pro Kit', i18n ), + __( 'FontAwesome Icon Library Version', i18n ), + ], + }, + ], + }, + { + id: 'other-settings', + label: __( 'Miscellaneous ', i18n ), + groups: [ + { + id: 'miscellaneous', + children: [ + __( 'Show Go premium notices', i18n ), + __( 'Generate Global Colors for native blocks', i18n ), + ], + }, + { + id: 'migration-settings', + children: [ + __( 'Load version 2 blocks in the editor', i18n ), + __( 'Load version 2 blocks in the editor only when the page was using version 2 blocks', i18n ), + __( 'Load version 2 frontend block stylesheet and scripts for backward compatibility', i18n ), + ], + }, + ], + }, + { + id: 'v2-settings', + label: __( 'V2 Settings', i18n ), + groups: [ + { + id: 'optimizations', + children: [ + __( 'Frontend JS & CSS Files' ), + ], + }, + { + id: 'blocks', + children: Object.values( blockData ).map( block => block.title ), + }, + ], + }, +] + +const DERIVED_BLOCKS = getAllBlocks() +// An object containing all the blocks and their required children +const REQUIRED_BLOCKS = BLOCK_CATEROGIES.reduce( ( acc, { id } ) => { + DERIVED_BLOCKS[ id ].forEach( block => { + acc[ block.name ] = block[ 'stk-required-blocks' ] ?? [] + } ) + return acc +}, {} ) + +const getChildrenBlocks = blockname => { + return REQUIRED_BLOCKS[ blockname ] || [] +} + +const getParentBlocks = blockName => { + const parents = [] + for ( const parent in REQUIRED_BLOCKS ) { + if ( REQUIRED_BLOCKS[ parent ].includes( blockName ) ) { + parents.push( parent ) + } + } + return parents +} + const BlockList = () => { const DERIVED_BLOCKS = getAllBlocks() return ( @@ -92,680 +303,1255 @@ const BlockList = () => { ) } -const BlockToggler = () => { - const DERIVED_BLOCKS = getAllBlocks() - const [ isSaving, setIsSaving ] = useState( false ) - const [ disabledBlocks, setDisabledBlocks ] = useState( [] ) +// Create an admin notice if there's an error fetching the settings. +const RestSettingsNotice = () => { + const [ error, setError ] = useState( null ) useEffect( () => { loadPromise.then( () => { const settings = new models.Settings() - settings.fetch().then( response => { - setDisabledBlocks( response.stackable_disabled_blocks ) + settings.fetch().catch( error => { + setError( error ) } ) } ) }, [] ) - const save = ( disabledBlocks, type ) => { - setIsSaving( type ) - const model = new models.Settings( { stackable_disabled_blocks: disabledBlocks } ) // eslint-disable-line camelcase - model.save().then( () => setIsSaving( false ) ) + if ( ! error ) { + return null } - const enableAllBlocks = type => () => { - let newDisabledBlocks = [ ...disabledBlocks ] - DERIVED_BLOCKS[ type ].forEach( block => { - newDisabledBlocks = newDisabledBlocks.filter( blockName => blockName !== block.name ) - } ) - setDisabledBlocks( newDisabledBlocks ) - save( newDisabledBlocks, type ) - } + return ( +
+

{ __( 'Error getting Stackable settings. We got the following error. Please contact your administrator.', i18n ) }

+ { error.responseJSON && +

{ error.responseJSON.data.status } ({ error.responseJSON.code }). { error.responseJSON.message }

+ } +
+ ) +} + +// Confirmation dialog when disabling a block that is dependent on another block. +const ToggleBlockDialog = ( { + blockName, + blockList, + isDisabled, + onConfirm, + onCancel, +} ) => { + const DERIVED_BLOCKS = getAllBlocks() - const disableAllBlocks = type => () => { - const newDisabledBlocks = [ ...disabledBlocks ] - DERIVED_BLOCKS[ type ].forEach( block => { - if ( ! newDisabledBlocks.includes( block.name ) ) { - newDisabledBlocks.push( block.name ) + const getBlockTitle = name => { + for ( const category in DERIVED_BLOCKS ) { + for ( const block of DERIVED_BLOCKS[ category ] ) { + if ( block.name === name ) { + return block.title + } } - } ) - setDisabledBlocks( newDisabledBlocks ) - save( newDisabledBlocks, type ) + } + return name } - const toggleBlock = useCallback( ( name, type ) => { - let newDisabledBlocks = null - if ( disabledBlocks.includes( name ) ) { - newDisabledBlocks = disabledBlocks.filter( block => block !== name ) - } else { - newDisabledBlocks = [ - ...disabledBlocks, - name, - ] - } - setDisabledBlocks( newDisabledBlocks ) - save( newDisabledBlocks, type ) - }, [ setDisabledBlocks, disabledBlocks ] ) + const blockTitle = getBlockTitle( blockName ) + + return ( + + { isDisabled + ?

{ __( 'Disabling this block will also disable these blocks that require this block to function:', i18n ) }

// eslint-disable-line @wordpress/i18n-no-variables + :

{ __( 'Enabling this block will also enable these blocks that are needed for this block to function:', i18n ) }

// eslint-disable-line @wordpress/i18n-no-variables + } +
    + { blockList.map( ( block, i ) => ( +
  • { getBlockTitle( block ) }
  • + ) ) } +
+ + + + + + + + +
+ ) +} + +// Side navigation with the save changes button and search on tabs +const Sidenav = ( { + currentTab, + handleTabChange, + hasUnsavedChanges, + handleSettingsSave, + currentSearch, + filteredSearchTree, + isSaving, + isRecentlySaved, + hasV2Tab, +} ) => { + const saveButtonClasses = classnames( [ + 's-save-changes', + { 's-button-has-unsaved-changes': hasUnsavedChanges && ! isRecentlySaved }, + ] ) return ( <> - { BLOCK_CATEROGIES.map( ( { - id, label, Icon, - } ) => { - const classes = classnames( [ - 's-box-block__title', - `s-box-block__title--${ id }`, - ] ) - return ( -
-

- { Icon && } - { label } -

-
- { isSaving === id && } - - -
-
- { DERIVED_BLOCKS[ id ].map( ( block, i ) => { - const isDisabled = disabledBlocks.includes( block.name ) - - const demoLink = block[ 'stk-demo' ] && ( - ev.stopPropagation() } - > - { __( 'view demo', i18n ) } - - ) +
+ ) } -// Implement pick without using lodash, because themes and plugins might remove -// lodash from the admin. -const pick = ( obj, keys ) => { - return keys.reduce( ( acc, key ) => { - if ( obj && obj.hasOwnProperty( key ) ) { - acc[ key ] = obj[ key ] - } - return acc - }, {} ) +const Searchbar = ( { currentSearch, handleSearchChange } ) => { + const handleSearch = e => { + handleSearchChange( e.target.value.toLowerCase() ) + } + return ( +
+ +
+ ) } -// Create an admin notice if there's an error fetching the settings. -const SettingsNotice = () => { - const [ error, setError ] = useState( null ) +// Main settings component +const Settings = () => { + const [ settings, setSettings ] = useState( {} ) + const [ unsavedChanges, setUnsavedChanges ] = useState( {} ) + const [ currentTab, setCurrentTab ] = useState( 'editor-settings' ) + const [ currentSearch, setCurrentSearch ] = useState( '' ) + const [ isSaving, setIsSaving ] = useState( false ) + const [ isRecentlySaved, setIsRecentlySaved ] = useState( false ) + const [ hasV2Tab, setHasV2Tab ] = useState( false ) + + const hasV2Compatibility = currentSettings => { + return currentSettings.stackable_v2_frontend_compatibility === '1' || + currentSettings.stackable_v2_editor_compatibility === '1' || + currentSettings.stackable_v2_editor_compatibility_usage === '1' + } + + const handleSettingsChange = useCallback( newSettings => { + setSettings( prev => ( { ...prev, ...newSettings } ) ) + setUnsavedChanges( prev => ( { ...prev, ...newSettings } ) ) + }, [] ) + + const handleSettingsSave = useCallback( () => { + if ( Object.keys( unsavedChanges ).length === 0 ) { + return + } + setIsSaving( true ) + setIsRecentlySaved( true ) + const model = new models.Settings( unsavedChanges ) + model.save().then( () => { + // Add a little more time for the spinner for better feedback + setTimeout( () => { + setIsSaving( false ) + }, 500 ) + setTimeout( () => { + setIsRecentlySaved( false ) + }, 1500 ) + } ) + setUnsavedChanges( {} ) + }, [ unsavedChanges, settings ] ) useEffect( () => { loadPromise.then( () => { const settings = new models.Settings() - settings.fetch().catch( error => { - setError( error ) + settings.fetch().then( response => { + setSettings( response ) + // Should only be set initially since we have to reload after setting for it to work with the backend + setHasV2Tab( hasV2Compatibility( response ) ) } ) } ) }, [] ) - if ( ! error ) { - return null - } + // However, when disabling V2 blocks, update the settings page to disallow further configuration + useEffect( () => { + if ( ! hasV2Compatibility( settings ) ) { + setHasV2Tab( false ) + } + }, [ settings ] ) - return ( -
-

{ __( 'Error getting Stackable settings. We got the following error. Please contact your administrator.', i18n ) }

- { error.responseJSON && -

{ error.responseJSON.data.status } ({ error.responseJSON.code }). { error.responseJSON.message }

+ const hasUnsavedChanges = useMemo( () => Object.keys( unsavedChanges ).length > 0, [ unsavedChanges ] ) + + useEffect( () => { + const handleBeforeUnload = event => { + if ( hasUnsavedChanges ) { + event.preventDefault() + // Most browsers ignore the custom message, but returning a value triggers the dialog + // https://developer.mozilla.org/en-US/docs/Web/API/BeforeUnloadEvent/returnValue + event.returnValue = true } -
- ) -} + } -const EditorSettings = () => { - const [ settings, setSettings ] = useState( {} ) - const [ isBusy, setIsBusy ] = useState( false ) - const [ saveTimeout, setSaveTimeout ] = useState( null ) + window.addEventListener( 'beforeunload', handleBeforeUnload ) - useEffect( () => { - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - setSettings( pick( response, [ - 'stackable_google_maps_api_key', - 'stackable_enable_design_library', - 'stackable_optimize_inline_css', - 'stackable_block_default_width', - 'stackable_block_wide_width', - 'stackable_auto_collapse_panels', - 'stackable_enable_block_linking', - 'stackable_enable_carousel_lazy_loading', - 'stackable_enable_text_default_block', - ] ) ) + return () => { + window.removeEventListener( 'beforeunload', handleBeforeUnload ) + } + }, [ hasUnsavedChanges ] ) + + const filteredSearchTree = useMemo( () => { + if ( ! currentSearch ) { + return SEARCH_TREE + } + const loweredSearch = currentSearch.toLowerCase() + + return SEARCH_TREE.map( tabs => { + const filtedGroups = tabs.groups.map( group => { + const filteredChildren = group.children.filter( child => { + return child.toLowerCase().includes( loweredSearch.toLowerCase() ) + } ) + return { ...group, children: filteredChildren } } ) + return { ...tabs, groups: filtedGroups } } ) - }, [] ) + }, [ currentSearch ] ) + + const props = { + settings, + handleSettingsChange, + filteredSearchTree, + currentTab, + } return <> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_design_library: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_design_library: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Adds a button on the top of the editor which gives access to a collection of pre-made block designs.', i18n ) } + - { - setIsBusy( true ) - const model = new models.Settings( { stackable_optimize_inline_css: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_optimize_inline_css: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Optimize inlined CSS styles. If this is enabled, similar selectors will be combined together, helpful if you changed Block Defaults.', i18n ) } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_auto_collapse_panels: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_auto_collapse_panels: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Collapse other inspector panels when opening another, keeping only one open at a time.', i18n ) } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_block_linking: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_block_linking: value, // eslint-disable-line camelcase - } ) - } } - help={ +
+ + { currentTab === 'editor-settings' && } + { currentTab === 'responsiveness' && } + { currentTab === 'blocks' && } + { currentTab === 'optimizations' && } + { currentTab === 'global-settings' && } + { currentTab === 'role-manager' && } + { currentTab === 'custom-fields-settings' && } + { currentTab === 'integrations' && } + { currentTab === 'other-settings' && } + { /* Render the V2 settings and show/hide via CSS */ } + +
+ +} + +const EditorSettings = props => { + const { + settings, + handleSettingsChange, + filteredSearchTree, + } = props + + const groups = filteredSearchTree.find( tab => tab.id === 'editor-settings' ).groups + const blocks = groups.find( group => group.id === 'blocks' ) + const editor = groups.find( group => group.id === 'editor' ) + const toolbar = groups.find( group => group.id === 'toolbar' ) + const inspector = groups.find( group => group.id === 'inspector' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) + + return ( +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( <> - { __( 'Gives you the ability to link columns. Any changes you make on one column will automatically get applied on the other columns.', i18n ) } -   - { __( 'Learn more', i18n ) } + { blocks.children.length > 0 && +
+

{ __( 'Block Widths', i18n ) }

+

{ __( 'Adjust the width of Stackable blocks here.', i18n ) }

+ { + handleSettingsChange( { stackable_block_default_width: value } ) // eslint-disable-line camelcase + } } + help={ __( 'The width used when a Columns block has its Content Width set to center. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } + /> + { + handleSettingsChange( { stackable_block_wide_width: value } ) // eslint-disable-line camelcase + } } + help={ __( 'The width used when a Columns block has its Content Width set to wide. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } + /> +
+ } + { editor.children.length > 0 && +
+

{ __( 'Editor', i18n ) }

+

{ __( 'You can customize some of the features and behavior of Stackable in the editor here.' ) }

+ { + handleSettingsChange( { stackable_enable_text_default_block: value } ) // eslint-disable-line camelcase + } } + help={ __( 'If enabled, Stackable Text blocks will be added by default instead of the native Paragraph Block.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_design_library: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a button on the top of the editor which gives access to a collection of pre-made block designs. Note: You can still access the Design Library by adding the Design Library block.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_global_settings: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a button on the top of the editor which gives access to Stackable settings. Note: You won\'t be able to access Stackable settings when this is disabled.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_block_linking: value } ) // eslint-disable-line camelcase + } } + help={ + <> + { __( 'Gives you the ability to link columns. Any changes you make on one column will automatically get applied on the other columns.', i18n ) } +   + { __( 'Learn more', i18n ) } + + } + /> +
+ } + { toolbar.children.length > 0 && +
+

{ __( 'Toolbar', i18n ) }

+

{ __( 'You can disable some toolbar features here.', i18n ) }

+ { + handleSettingsChange( { stackable_enable_text_highlight: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for highlighting text', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_dynamic_content: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for inserting and modifying dynamic content', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_copy_paste_styles: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for advanced copying and pasting block styles', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_reset_layout: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Adds a toolbar button for resetting the layout of a stackble block back to the original', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_save_as_default_block: value } ) // eslint-disable-line + } } + help={ __( 'Adds a toolbar button for saving a block as the default block', i18n ) } + /> +
+ } + { inspector.children.length > 0 && +
+

{ __( 'Inspector', i18n ) }

+

{ __( 'You can customize some of the features and behavior of Stackable in the inspector here.' ) }

+ { + handleSettingsChange( { stackable_help_tooltip_disabled: value ? '1' : '' } ) // eslint-disable-line camelcase + } } + help={ __( 'Disables the help video tooltips that appear in the inspector.', i18n ) } + /> + { + handleSettingsChange( { stackable_auto_collapse_panels: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Collapse other inspector panels when opening another, keeping only one open at a time.', i18n ) } + /> +
+ } - } - /> - { - clearTimeout( saveTimeout ) - setSettings( { - ...settings, - stackable_block_default_width: value, // eslint-disable-line camelcase - } ) - setSaveTimeout( setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { stackable_block_default_width: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - }, 400 ) ) - } } - help={ __( 'The width used when a Columns block has its Content Width set to center. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } - > - { - clearTimeout( saveTimeout ) - setSettings( { - ...settings, - stackable_block_wide_width: value, // eslint-disable-line camelcase - } ) - setSaveTimeout( setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { stackable_block_wide_width: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - }, 400 ) ) - } } - help={ __( 'The width used when a Columns block has its Content Width set to wide. This is automatically detected from your theme. You can adjust it if your blocks are not aligned correctly. In px, you can also use other units or use a calc() formula.', i18n ) } - /> - { - clearTimeout( saveTimeout ) - setSettings( { - ...settings, - stackable_google_maps_api_key: value, // eslint-disable-line camelcase - } ) - setSaveTimeout( - setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { - stackable_google_maps_api_key: value, // eslint-disable-line camelcase - } ) - model.save().then( () => setIsBusy( false ) ) - }, 400 ) - ) - } } - help={ + ) } +
+ ) +} + +const Responsiveness = props => { + const { + settings, + handleSettingsChange, + filteredSearchTree, + } = props + + const groups = filteredSearchTree.find( tab => tab.id === 'responsiveness' ).groups + const dynamicBreakpoints = groups.find( group => group.id === 'dynamic-breakpoints' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) + + return ( +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( <> - { __( - 'Adding a Google API Key enables additional features of the Stackable Map Block.', - i18n - ) } -   - { __( 'Learn more', i18n ) } + { dynamicBreakpoints.children.length > 0 && +
+

{ __( 'Dynamic Breakpoints', i18n ) }

+

+ { __( 'Blocks can be styles differently for tablet and mobile screens, and some styles adjust to make them fit better in smaller screens. You can change the widths when tablet and mobile views are triggered. ', i18n ) } + + { __( 'Learn more', i18n ) } + +

+ { + handleSettingsChange( { + stackable_dynamic_breakpoints: { // eslint-disable-line camelcase + tablet: value, + mobile: settings.stackable_dynamic_breakpoints.mobile || '', // eslint-disable-line camelcase + }, + } ) + } } + placeholder="1024" + > px + { + handleSettingsChange( { + stackable_dynamic_breakpoints: { // eslint-disable-line camelcase + tablet: settings.stackable_dynamic_breakpoints.tablet || '', // eslint-disable-line camelcase + mobile: value, + }, + } ) + } } + placeholder="768" + > px +
+ } - } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_carousel_lazy_loading: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_carousel_lazy_loading: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'Disable this if you encounter layout or spacing issues when using images inside carousel-type blocks because of image lazy loading.', i18n ) } - /> - { - setIsBusy( true ) - const model = new models.Settings( { stackable_enable_text_default_block: value } ) // eslint-disable-line camelcase - model.save().then( () => setIsBusy( false ) ) - setSettings( { - ...settings, - stackable_enable_text_default_block: value, // eslint-disable-line camelcase - } ) - } } - help={ __( 'If enabled, Stackable Text blocks will be added by default instead of the native Paragraph Block.', i18n ) } - /> - { isBusy && -
- -
- } - + ) } +
+ ) } -const DynamicBreakpointsSettings = () => { - const [ tabletBreakpoint, setTabletBreakpoint ] = useState( '' ) - const [ mobileBreakpoint, setMobileBreakpoint ] = useState( '' ) - const [ isReady, setIsReady ] = useState( false ) - const [ isBusy, setIsBusy ] = useState( false ) +// Toggle the block states between enabled, disabled and hidden. +// Enabled blocks are not stored in the settings object. +const Blocks = props => { + const { + settings, + handleSettingsChange, + filteredSearchTree, + } = props - useEffect( () => { - setIsBusy( true ) - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - const breakpoints = response.stackable_dynamic_breakpoints - if ( breakpoints ) { - setTabletBreakpoint( breakpoints.tablet || '' ) - setMobileBreakpoint( breakpoints.mobile || '' ) + const BLOCK_STATE_MAP = Object.freeze( { + enabled: BLOCK_STATE.ENABLED, + hidden: BLOCK_STATE.HIDDEN, + disabled: BLOCK_STATE.DISABLED, + } ) + const DERIVED_BLOCKS = getAllBlocks() + const groups = filteredSearchTree.find( tab => tab.id === 'blocks' ).groups + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) + const disabledBlocks = settings.stackable_block_states ?? {} // eslint-disable-line camelcase + + const [ isDisabledDialogOpen, setIsDisabledDialogOpen ] = useState( false ) + const [ isEnabledDialogOpen, setIsEnabledDialogOpen ] = useState( false ) + const [ currentToggleBlock, setCurrentToggleBlock ] = useState( '' ) + const [ currentToggleBlockList, setCurrentToggleBlockList ] = useState( [] ) + + // Map string states to integer block states + const mapStringStates = states => { + return states?.map( + state => BLOCK_STATE_MAP[ state.toLowerCase() ] + ) + } + + const enableAllBlocks = () => { + const newDisabledBlocks = {} + BLOCK_CATEROGIES.forEach( ( { id } ) => { + DERIVED_BLOCKS[ id ].forEach( block => { + const availableStates = mapStringStates( block[ 'stk-available-states' ] ) + // Retain previous state if cannot be enabled + if ( availableStates && ! availableStates.includes( BLOCK_STATE.ENABLED ) && disabledBlocks[ block.name ] ) { + newDisabledBlocks[ block.name ] = disabledBlocks[ block.name ] } - setIsReady( true ) - setIsBusy( false ) } ) } ) - }, [] ) + handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase + } - useEffect( () => { - if ( isReady ) { - const t = setTimeout( () => { - setIsBusy( true ) - const model = new models.Settings( { - stackable_dynamic_breakpoints: { // eslint-disable-line camelcase - tablet: tabletBreakpoint, - mobile: mobileBreakpoint, - }, - } ) - model.save().then( () => setIsBusy( false ) ) - }, 400 ) - return () => clearTimeout( t ) + const disableAllBlocks = () => { + const newDisabledBlocks = {} + BLOCK_CATEROGIES.forEach( ( { id } ) => { + DERIVED_BLOCKS[ id ].forEach( block => { + const availableStates = mapStringStates( block[ 'stk-available-states' ] ) + if ( ! availableStates || availableStates.includes( BLOCK_STATE.DISABLED ) ) { + newDisabledBlocks[ block.name ] = BLOCK_STATE.DISABLED + } else if ( availableStates.includes( BLOCK_STATE.HIDDEN ) ) { // If the block cannot be disabled, default to hidden. + newDisabledBlocks[ block.name ] = BLOCK_STATE.HIDDEN + } else if ( disabledBlocks[ block.name ] ) { // Retain previous state if cannot be disabled or hidden + newDisabledBlocks[ block.name ] = disabledBlocks[ block.name ] + } + } ) + } ) + handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + const hideAllBlocks = () => { + const newDisabledBlocks = {} + BLOCK_CATEROGIES.forEach( ( { id } ) => { + DERIVED_BLOCKS[ id ].forEach( block => { + const availableStates = mapStringStates( block[ 'stk-available-states' ] ) + if ( ! availableStates || availableStates.includes( BLOCK_STATE.HIDDEN ) ) { + newDisabledBlocks[ block.name ] = BLOCK_STATE.HIDDEN + } else if ( disabledBlocks[ block.name ] ) { // Retain previous state if cannot be hidden + newDisabledBlocks[ block.name ] = disabledBlocks[ block.name ] + } + } ) + } ) + handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + const toggleBlock = ( name, value ) => { + const valueInt = Number( value ) + let newDisabledBlocks = { ...disabledBlocks } + + setCurrentToggleBlock( name ) + + // Check if a parent is being enabled + if ( valueInt === BLOCK_STATE.ENABLED ) { + // Get the parent's disabled/hidden children and confirm if they will also be enabled + const childrenBlocks = getChildrenBlocks( name ).filter( block => block in disabledBlocks ) + if ( childrenBlocks.length > 0 ) { + setCurrentToggleBlockList( childrenBlocks ) + setIsEnabledDialogOpen( true ) + } else { + delete newDisabledBlocks[ name ] + } + } else if ( valueInt === BLOCK_STATE.DISABLED ) { // Check if a child is being disabled + // Get the child's enabled parents and confirm if they will also be disabled + const parentBlocks = getParentBlocks( name ).filter( block => ! ( block in disabledBlocks ) ) + if ( parentBlocks.length > 0 ) { + setCurrentToggleBlockList( parentBlocks ) + setIsDisabledDialogOpen( true ) + } else { + newDisabledBlocks = { ...disabledBlocks, [ name ]: valueInt } + } + } else { + newDisabledBlocks = { ...disabledBlocks, [ name ]: valueInt } } - }, [ tabletBreakpoint, mobileBreakpoint, isReady ] ) - - return -
- setTabletBreakpoint( value ) } - placeholder="1024" - > px - setMobileBreakpoint( value ) } - placeholder="768" - > px -
- { isBusy && -
- + handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + const handleDisableDialogConfirm = () => { + setIsDisabledDialogOpen( false ) + const newDisabledBlocks = { ...disabledBlocks, [ currentToggleBlock ]: BLOCK_STATE.DISABLED } + currentToggleBlockList.forEach( block => { + newDisabledBlocks[ block ] = BLOCK_STATE.DISABLED + } ) + handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + const handleEnableDialogConfirm = () => { + setIsEnabledDialogOpen( false ) + const newDisabledBlocks = { ...disabledBlocks } + delete newDisabledBlocks[ currentToggleBlock ] + currentToggleBlockList.forEach( block => { + delete newDisabledBlocks[ block ] + } ) + handleSettingsChange( { stackable_block_states: newDisabledBlocks } ) // eslint-disable-line camelcase + } + + return ( + <> + { isDisabledDialogOpen && currentToggleBlockList && ( + { + setIsDisabledDialogOpen( false ) + } } + /> + ) } + + { isEnabledDialogOpen && currentToggleBlockList && ( + { + setIsEnabledDialogOpen( false ) + } } + /> + ) } + +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( +
+

{ __( 'Blocks', i18n ) }

+

{ __( 'Here you can enable, hide and disable Stackable blocks. Hiding blocks will hide the block from the list of available blocks. Disabling blocks will prevent them from being registered at all. When using block variations or design library patterns, disabled blocks will be substituted with the relevant core blocks.', i18n ) }

+
+ + + +
+ { BLOCK_CATEROGIES.map( ( { + id, label, Icon, + } ) => { + const classes = classnames( [ + 's-box-block__title', + `s-box-block__title--${ id }`, + ] ) + const group = groups.find( group => group.id === id ) + return group.children.length > 0 && ( +
+

+ { Icon && } + { label } +

+
+ { DERIVED_BLOCKS[ id ].map( ( block, i ) => { + const blockState = disabledBlocks[ block.name ] ?? BLOCK_STATE.ENABLED + const availableStates = mapStringStates( block[ 'stk-available-states' ] ) + // If there is only one state, do not render the block + if ( availableStates && availableStates.length <= 1 ) { + return null + } + return ( + { + toggleBlock( block.name, value ) + } } + isSmall={ true } + /> + ) + } ) } +
+
+ ) + } ) } +
+ ) }
- } - + + ) } -const GlobalSettings = () => { - const [ forceTypography, setForceTypography ] = useState( false ) +const Optimizations = props => { + const { + settings, + handleSettingsChange, + filteredSearchTree, + } = props - useEffect( () => { - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - setForceTypography( !! response.stackable_global_force_typography ) - } ) - } ) - }, [] ) + const groups = filteredSearchTree.find( tab => tab.id === 'optimizations' ).groups + const optimizations = groups.find( group => group.id === 'optimizations' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) - const updateForceTypography = value => { - const model = new models.Settings( { stackable_global_force_typography: value } ) // eslint-disable-line camelcase - model.save() - setForceTypography( value ) - } + return ( +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( + <> + { optimizations.children.length > 0 && +
+

{ __( 'Optimizations', i18n ) }

+

{ __( 'Here you can adjust some optimization settings that are performed by Stackable.', i18n ) }

+ { + handleSettingsChange( { stackable_optimize_inline_css: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Optimize inlined CSS styles. If this is enabled, similar selectors will be combined together, helpful if you changed Block Defaults.', i18n ) } + /> + { + handleSettingsChange( { stackable_enable_carousel_lazy_loading: value } ) // eslint-disable-line camelcase + } } + help={ __( 'Disable this if you encounter layout or spacing issues when using images inside carousel-type blocks because of image lazy loading.', i18n ) } + /> +
+ } + + ) } +
+ ) +} - return - - +const GlobalSettings = props => { + const groups = props.filteredSearchTree.find( tab => tab.id === 'global-settings' ).groups + const globalSettings = groups.find( group => group.id === 'global-settings' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) + + return ( +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( + <> + { globalSettings.children.length > 0 && +
+

{ __( 'Global Settings', i18n ) }

+

{ __( 'Here you can tweak Global Settings that affect the styles across your entire site.', i18n ) }

+ { + props.handleSettingsChange( { stackable_global_force_typography: value } ) // eslint-disable-line camelcase + } } + disabled={ __( 'Not forced', i18n ) } + enabled={ __( 'Force styles', i18n ) } + /> +
+ } + + ) } +
+ ) } -const IconSettings = () => { - const [ faVersion, setFaVersion ] = useState( '' ) +const EditorModeSettings = lazy( () => import( '../../pro__premium_only/src/welcome/editor-mode' ) ) - useEffect( () => { - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - setFaVersion( response.stackable_icons_fa_free_version || '6.5.1' ) - } ) - } ) - }, [] ) +const RoleManager = props => { + const groups = props.filteredSearchTree.find( tab => tab.id === 'role-manager' ).groups + props.roleManager = groups.find( group => group.id === 'role-manager' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) - const updateFaVersion = value => { - const model = new models.Settings( { stackable_icons_fa_free_version: value } ) // eslint-disable-line camelcase - model.save() - setFaVersion( value ) - } + return ( +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( + <> + { props.roleManager.children.length > 0 && +
+

{ __( 'Role Manager', i18n ) }

+

+ { __( 'Lock the Block Editor\'s inspector for different user roles, and give clients edit access to only images and content. Content Editing Mode affects all blocks. ', i18n ) } + + { __( 'Learn more', i18n ) } + +

+ { isPro + ? }> +
+ +
+
+ :

+ { __( 'This is only available in Stackable Premium. ', i18n ) } + + { __( 'Go Premium', i18n ) } + +

+ } +
+ } + + ) } +
+ ) +} - return -
- +const CustomFieldsEnableSettings = lazy( () => import( '../../pro__premium_only/src/welcome/custom-fields-toggle' ) ) +const CustomFieldsManagerSettings = lazy( () => import( '../../pro__premium_only/src/welcome/custom-fields-roles' ) ) + +const CustomFields = props => { + const groups = props.filteredSearchTree.find( tab => tab.id === 'custom-fields-settings' ).groups + props.customFields = groups.find( group => group.id === 'custom-fields-settings' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) + + return ( +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( + <> + { props.customFields.children.length > 0 && +
+
+

{ __( 'Custom Fields', i18n ) }

+ { isPro && + }> +
+ +
+
+ } +
+

+ { __( 'Create Custom Fields that you can reference across your entire site. You can assign which roles can manage your Custom Fields. ', i18n ) } + + { __( 'Learn more', i18n ) } + +

+ { isPro + ? }> +
+ +
+
+ :

+ { __( 'This is only available in Stackable Premium. ', i18n ) } + + { __( 'Go Premium', i18n ) } + +

+ } +
+ } + + ) }
-
- + ) +} + +const IconSettings = lazy( () => import( '../../pro__premium_only/src/welcome/icons.js' ) ) + +const Integrations = props => { + const { + settings, + handleSettingsChange, + filteredSearchTree, + } = props + + const groups = filteredSearchTree.find( tab => tab.id === 'integrations' ).groups + props.integrations = groups.find( group => group.id === 'integrations' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) + + return ( +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( + <> + { props.integrations.children.length > 0 && +
+

{ __( 'Integrations', i18n ) }

+

{ __( 'Here are settings for the different integrations available in Stackable.', i18n ) }

+ { + handleSettingsChange( { stackable_google_maps_api_key: value } ) // eslint-disable-line camelcase + } } + help={ + <> + { __( + 'Adding a Google API Key enables additional features of the Stackable Map Block.', + i18n + ) } +   + { __( 'Learn more', i18n ) } + + } + /> + { isPro + ? }> +
+ +
+
+ : <> +
+ +
+ + } +
+
+ +
+
+ { + handleSettingsChange( { stackable_icons_fa_free_version: value } ) // eslint-disable-line camelcase + } } + /> +
+
+
+ } + + ) }
- + ) } const AdditionalOptions = props => { - const [ helpTooltipsDisabled, setHelpTooltipsDisabled ] = useState( false ) - const [ generateNativeGlobalColors, setGenerateNativeGlobalColors ] = useState( false ) - const [ v2EditorBackwardCompatibility, setV2EditorBackwardCompatibility ] = useState( false ) - const [ v2EditorBackwardCompatibilityUsage, setV2EditorBackwardCompatibilityUsage ] = useState( false ) - const [ v2FrontendBackwardCompatibility, setV2FrontendBackwardCompatibility ] = useState( false ) - const [ showPremiumNotices, setShowPremiumNotices ] = useState( false ) - const [ isBusy, setIsBusy ] = useState( false ) + const { + settings, + handleSettingsChange, + filteredSearchTree, + } = props - useEffect( () => { - setIsBusy( true ) - loadPromise.then( () => { - const settings = new models.Settings() - settings.fetch().then( response => { - setHelpTooltipsDisabled( response.stackable_help_tooltip_disabled === '1' ) - setGenerateNativeGlobalColors( !! response.stackable_global_colors_native_compatibility ) - setV2EditorBackwardCompatibility( response.stackable_v2_editor_compatibility === '1' ) - setV2EditorBackwardCompatibilityUsage( response.stackable_v2_editor_compatibility_usage === '1' ) - setV2FrontendBackwardCompatibility( response.stackable_v2_frontend_compatibility === '1' ) - setShowPremiumNotices( response.stackable_show_pro_notices === '1' ) - setIsBusy( false ) - } ) - } ) - }, [] ) + const groups = filteredSearchTree.find( tab => tab.id === 'other-settings' ).groups + const miscellaneous = groups.find( group => group.id === 'miscellaneous' ) + const migrationSettings = groups.find( group => group.id === 'migration-settings' ) + const groupLength = groups.reduce( ( acc, curr ) => acc + curr.children.length, 0 ) - const updateSetting = settings => { - setIsBusy( true ) - const model = new models.Settings( settings ) - model.save().then( () => setIsBusy( false ) ) + const searchClassname = ( label, searchedSettings ) => { + return searchedSettings.children.includes( label ) ? '' : 'ugb-admin-setting--not-highlight' } return ( -
- { props.showProNoticesOption && - { - updateSetting( { stackable_show_pro_notices: checked ? '1' : '' } ) // eslint-disable-line camelcase - setShowPremiumNotices( checked ) - } } - /> - } - { - updateSetting( { stackable_help_tooltip_disabled: checked ? '1' : '' } ) // eslint-disable-line camelcase - setHelpTooltipsDisabled( checked ) - } } - /> - { - updateSetting( { stackable_global_colors_native_compatibility: checked } ) // eslint-disable-line camelcase - setGenerateNativeGlobalColors( checked ) - } } - /> -

{ __( '🏠 Migration Settings', i18n ) }

-

- { __( 'Migrating from version 2 to version 3?', i18n ) } -   - { __( 'Learn more about migration and the settings below', i18n ) } -

- { - const settings = { stackable_v2_editor_compatibility: checked ? '1' : '' } // eslint-disable-line camelcase - if ( checked ) { - settings.stackable_v2_editor_compatibility_usage = '' // eslint-disable-line camelcase - setV2EditorBackwardCompatibilityUsage( false ) +
+ { groupLength <= 0 ? ( +

{ __( 'No matching settings', i18n ) }

+ ) : ( + <> + { miscellaneous.children.length > 0 && +
+

{ __( 'Miscellaneous', i18n ) }

+

{ __( 'Below are other minor settings. Some may be useful when upgrading from older versions of Stackable.', i18n ) }

+ { showProNoticesOption && + { + handleSettingsChange( { stackable_show_pro_notices: checked ? '1' : '' } ) // eslint-disable-line camelcase + } } + /> + } + { + handleSettingsChange( { stackable_global_colors_native_compatibility: checked } ) // eslint-disable-line camelcase + } } + /> +
} - updateSetting( settings ) - setV2EditorBackwardCompatibility( checked ) - } } - /> - { - const settings = { stackable_v2_editor_compatibility_usage: checked ? '1' : '' } // eslint-disable-line camelcase - if ( checked ) { - settings.stackable_v2_editor_compatibility = '' // eslint-disable-line camelcase - setV2EditorBackwardCompatibility( false ) + { migrationSettings.children.length > 0 && +
+

{ __( 'Migration Settings', i18n ) }

+

{ __( 'After enabling the version 2 blocks, please refresh the page to re-fetch the blocks from the server.', i18n ) }

+

+ { __( 'Migrating from version 2 to version 3?', i18n ) } +   + { __( 'Learn more about migration and the settings below', i18n ) } +

+ { + if ( checked ) { + handleSettingsChange( { stackable_v2_editor_compatibility_usage: '' } ) // eslint-disable-line camelcase + } + handleSettingsChange( { stackable_v2_editor_compatibility: checked ? '1' : '' } ) // eslint-disable-line camelcase + } } + /> + { + if ( checked ) { + handleSettingsChange( { stackable_v2_editor_compatibility: '' } ) // eslint-disable-line camelcase + } + handleSettingsChange( { stackable_v2_editor_compatibility_usage: checked ? '1' : '' } ) // eslint-disable-line camelcase + } } + /> + { + handleSettingsChange( { stackable_v2_frontend_compatibility: checked ? '1' : '' } ) // eslint-disable-line camelcase + } } + /> +
} - updateSetting( settings ) - setV2EditorBackwardCompatibilityUsage( checked ) - } } - /> - { - updateSetting( { stackable_v2_frontend_compatibility: checked ? '1' : '' } ) // eslint-disable-line camelcase - setV2FrontendBackwardCompatibility( checked ) - } } - /> - { isBusy && -
- -
- } + + ) }
) } -AdditionalOptions.defaultProps = { - showProNoticesOption: false, +const V2Settings = props => { + const groups = props.filteredSearchTree.find( tab => tab.id === 'v2-settings' ).groups + const optimizations = groups.find( group => group.id === 'optimizations' ) + const blocks = groups.find( group => group.id === 'blocks' ) + + const classes = classnames( [ + 's-v2-settings', + { 's-settings-hide': props.currentTab !== 'v2-settings' }, + ] ) + + return ( +
+ { optimizations.children.length > 0 && +
+

{ __( '🏃‍♂️ Optimization Settings', i18n ) } (V2)

+

+ { __( 'Here are some settings that you can tweak to optimize Stackable.', i18n ) } + { __( 'Learn more.', i18n ) } +
+ { __( 'This only works for version 2 blocks.', i18n ) } +

+ +
+ } + { blocks.children.length > 0 && +
+

{ __( 'Enable & Disable Blocks', i18n ) } (V2)

+ { __( 'This only works for version 2 blocks.', i18n ) } + +
+ } +
+ ) } // Load all the options into the UI. domReady( () => { - // This is for the getting started block list. - if ( document.querySelector( '.s-getting-started__block-list' ) ) { - createRoot( - document.querySelector( '.s-getting-started__block-list' ) - ).render( - - ) - } - - // All these below are for the settings page. - if ( document.querySelector( '.s-settings-wrapper' ) ) { - createRoot( - document.querySelector( '.s-settings-wrapper' ) - ).render( - - ) - } - - if ( document.querySelector( '.s-other-options-wrapper' ) ) { - createRoot( - document.querySelector( '.s-other-options-wrapper' ) - ).render( - - ) - } - - if ( document.querySelector( '.s-settings-notice' ) ) { - createRoot( - document.querySelector( '.s-settings-notice' ) - ).render( - - ) - } - - if ( document.querySelector( '.s-editor-settings' ) ) { + if ( document.querySelector( '.s-getting-started__body' ) ) { createRoot( - document.querySelector( '.s-editor-settings' ) + document.querySelector( '.s-getting-started__body' ) ).render( - + ) } - if ( document.querySelector( '.s-dynamic-breakpoints' ) ) { + // This is for the getting started block list. + if ( document.querySelector( '.s-getting-started__block-list' ) ) { createRoot( - document.querySelector( '.s-dynamic-breakpoints' ) + document.querySelector( '.s-getting-started__block-list' ) ).render( - + ) } - if ( document.querySelector( '.s-global-settings' ) ) { + if ( document.querySelector( '.s-sidenav' ) ) { createRoot( - document.querySelector( '.s-global-settings' ) + document.querySelector( '.s-sidenav' ) ).render( - + ) } - if ( document.querySelector( '.s-icon-settings-fa-version' ) ) { + if ( document.querySelector( '.s-rest-settings-notice' ) ) { createRoot( - document.querySelector( '.s-icon-settings-fa-version' ) + document.querySelector( '.s-rest-settings-notice' ) ).render( - + ) } - if ( document.querySelector( '.s-getting-started__body' ) ) { + if ( document.querySelector( '.s-content' ) ) { createRoot( - document.querySelector( '.s-getting-started__body' ) + document.querySelector( '.s-content' ) ).render( - + ) } } ) diff --git a/src/welcome/admin.scss b/src/welcome/admin.scss index 9586aa79e9..fd66141822 100644 --- a/src/welcome/admin.scss +++ b/src/welcome/admin.scss @@ -8,6 +8,11 @@ --stk-welcome-light-border: #d0d5dd; } +// Make scroll smooth with inline navigation. +html { + scroll-behavior: smooth; +} + // Clear out the margins of the admin page. body[class*="page_stackable"], @@ -18,6 +23,10 @@ body[class*="page_stk-"] { #wpbody-content > .wrap:not(#fs_connect) { // stylelint-disable-line selector-id-pattern margin: 0; padding: 0 50px; + + &.wrap-settings { + padding: 0; + } } } @@ -31,6 +40,10 @@ body[class*="page_stk-"] { border-bottom: 1px solid var(--stk-welcome-light-border); font-weight: 300; + &.s-header-settings { + margin: 0; + } + .s-header { padding: 0px; position: absolute; @@ -152,8 +165,8 @@ body[class*="page_stk-"] { margin-right: -50px; } .s-settings-subtitle { - width: 75%; - margin-bottom: 40px; + width: calc(95% - 250px); + margin: 0.5em 0 40px; } .s-settings-field, .ugb-admin-setting { @@ -172,23 +185,6 @@ body[class*="page_stk-"] { box-sizing: border-box; flex-shrink: 0; } - - .s-settings-field__status { - width: 40px; - display: inline-block; - vertical-align: middle; - svg { - width: 18px; - height: 18px; - margin: 5px 11px 0; - } - .s-settings-field__status--check { - color: #27de6c; - } - .s-settings-field__status--cross { - color: #ff5a7f; - } - } } [type="text"], [type="number"], @@ -209,7 +205,6 @@ body[class*="page_stk-"] { } } p { - margin-left: var(--label-width); font-style: italic; margin-top: 4px; } @@ -228,12 +223,6 @@ body[class*="page_stk-"] { margin: 0; } } -.s-icon-kit-settings-field > p { - margin-left: calc(var(--label-width) + 40px); -} -.s-icon-settings-fa-version .ugb-admin-setting__field { - margin-left: 40px; -} .s-icon-settings-fa-pro-version .ugb-admin-setting__field p { margin-left: 20px; } @@ -352,7 +341,18 @@ body[class*="page_stk-"] { .s-logo-logo + * > .s-tabs { justify-content: center; } - +.ugb-admin-setting.s-block-setting:not(:last-child) { + margin-bottom: 32px; + padding-bottom: 0; +} +body.settings_page_stackable { + #wpwrap { + background: #fff; + } + .s-box { + box-shadow: none !important; + } +} body.settings_page_stackable, body.settings_page_stackable-getting-started { // Hide admin notices to not mess up the welcome screen. @@ -363,6 +363,12 @@ body.settings_page_stackable-getting-started { .s-body-container .updated { display: none !important; } + #wpfooter { + display: none; + } + #wpbody-content { + padding-bottom: 0; + } } body.settings_page_stackable, body.settings_page_stackable-getting-started, @@ -378,11 +384,10 @@ body.toplevel_page_stk-custom-fields { background-color: #fff; border-radius: 0; overflow: hidden; - padding: 60px 35px; + padding: 30px; overflow: visible; box-shadow: 0 18px 35px -15px rgba(18, 63, 82, 0.25); border: 0; - margin-bottom: 30px; transition: all 0.3s ease; position: relative; &.s-box-spaced { @@ -397,6 +402,9 @@ body.toplevel_page_stk-custom-fields { color: #f34957; } } + &.s-box-hidden { + display: none; + } } .s-absolute-spinner { position: absolute; @@ -476,11 +484,171 @@ body.toplevel_page_stk-custom-fields { margin-bottom: 0 !important; } } - .s-body-container-grid { + #settings-content { + position: relative; display: grid; - grid-template-columns: 1fr 350px; - grid-gap: 30px; + grid-template-columns: 250px 1fr; } + .s-sidenav { + min-height: 90vh; + width: 250px; + left: 0; + background-color: #fff; + padding-top: 20px; + display: flex; + flex-direction: column; + justify-content: flex-start; + box-shadow: inset 0 -18px 35px -15px #123f5240; + z-index: 1; + > * { + position: sticky; + height: 100vh; + display: flex; + flex-direction: column; + top: 50px; + } + + .s-box { + padding-bottom: 200px; + } + .s-sidenav-item { + color: #444; + font-size: 14px; + padding: 15px 20px; + text-decoration: none; + display: block; + position: relative; + width: 100%; + text-align: left; + background: none; + border: none; + transition: background-color 0.3s ease; + cursor: pointer; + &:hover { + color: var(--stk-welcome-primary) !important; + } + &.s-active { + font-weight: 600; + color: var(--stk-welcome-primary) !important; + transition: color 0.3s ease; + background: #f1f1f1; + &::after { + content: ""; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 4px; + background-color: var(--stk-welcome-primary); + transition: width 0.3s ease; + } + } + &.s-sidenav-item-highlight { + font-weight: bold; + } + } + // Fade out the non-highlighted items. + .s-sidenav-item:has(~ .s-sidenav-item-highlight):not(.s-active):not(.s-sidenav-item-highlight), + .s-sidenav-item-highlight ~ .s-sidenav-item:not(.s-sidenav-item-highlight):not(.s-active) { + opacity: 0.3; + } + .s-save-changes-wrapper { + bottom: 0; + padding: 20px; + flex: 1 0 auto; + display: flex; + flex-direction: column; + justify-content: end; + } + .s-save-changes-inner-wrapper { + bottom: 20px; + display: inline-block; + position: sticky; + } + // This is a button, render this in the bottom of the side bar + // The side bar is a flex container, so this will be at the bottom. + .s-save-changes { + background: linear-gradient(135deg, #b300be, #f00069); + width: 100%; + margin: 0; + transition: all 0.1s ease-in-out; + text-decoration: none; + border: none; + color: #fff; + padding: 12px 20px; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 14px; + cursor: pointer; + box-sizing: border-box !important; + min-height: 55px; + &:hover { + opacity: 0.85; + box-shadow: none !important; + } + &:not(.s-button-has-unsaved-changes) { + filter: saturate(0); + } + } + .s-save-changes-note { + top: -30px; + position: absolute; + display: block; + right: 0; + left: 0; + text-align: center; + } + } + .s-search-setting { + display: flex; + justify-content: end; + position: absolute; + right: 30px; + .s-search-setting__input { + margin: 0; + padding: 0.5em 1em; + width: 250px; + } + } + + .ugb-admin-setting--not-highlight { + display: none; + } + + .components-base-control--not-highlight { + display: none; + } + + .s-confirm-modal { + .components-button { + --wp-admin-theme-color: #f00069; + --wp-admin-theme-color-darker-10: #c40056; + } + } + + .s-setting-group { + padding-bottom: 32px; + padding-top: 32px; + &:not(:last-child) { + border-bottom: 1px solid #eaecf0; + } + &:first-child { + padding-top: 0; + h2 { + margin-top: 0; + } + } + h2 { + font-size: 1.6em; + margin: 1em 0 0.5em; + } + } + + .s-v2-settings { + &.s-settings-hide { + display: none; + } + } + .s-side { h2, h3 { @@ -509,11 +677,19 @@ body.toplevel_page_stk-custom-fields { } } .stackable_notice_wrapper { - outline: 2px solid #f00069; - margin-bottom: 30px; + // outline: 2px solid #f00069; + margin: 0; + padding: 20px; + background: #e9ebee; } .stackable_notice { margin-bottom: 35px; + > *:last-child { + margin-bottom: 0; + } + } + #editor-settings { + margin-bottom: 0; } .s-settings-header { text-align: right; @@ -527,7 +703,7 @@ body.toplevel_page_stk-custom-fields { } .s-settings-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); grid-gap: 64px; grid-row-gap: 24px; .s-box { @@ -550,7 +726,7 @@ body.toplevel_page_stk-custom-fields { .s-box-block__title { display: flex; align-items: center; - margin: -32px 12px 0 -8px !important; + margin: -32px 12px 32px -8px !important; svg { padding: 20px; border-radius: 100%; @@ -703,8 +879,8 @@ body.toplevel_page_stk-custom-fields { grid-template-columns: 1fr 1fr 1fr; } -// Collapse to a single column for mobile. @media screen and (max-width: 960px) { + // Collapse to a single column for mobile. .s-body-container { grid-template-columns: 1fr !important; } @@ -732,12 +908,8 @@ body.toplevel_page_stk-custom-fields { margin-top: 0; margin-left: 32px; } - h3 { - margin-top: 2em; - } p { margin-bottom: 2em; - margin-top: -0.5em; } } diff --git a/src/welcome/index.php b/src/welcome/index.php index b2abfec3c7..ff2af783a6 100644 --- a/src/welcome/index.php +++ b/src/welcome/index.php @@ -174,81 +174,26 @@ public static function print_premium_button() { ?> public function stackable_settings_content() { ?> -
-
+
+
print_header() ?> print_premium_button() ?> print_tabs() ?>

-
-
- +
+
+
+
+ +
+
-
-
-
-
-

-

-
-
-
-

-

', '' ) ?>

-
-
-
-

-

', '' ) ?>

-
-
-
-

-
-
- can_use_premium_code() ) : ?> -

- -
-
-

- can_use_premium_code() ) : ?> -

' . __( 'Learn more', STACKABLE_I18N ) . '' ?>

- - can_use_premium_code() ) : ?> -

' . __( 'Learn more', STACKABLE_I18N ) . '' ?>

- -
- can_use_premium_code() ) : ?> -

- -
-
-
-

-
-
-

', '' ) ?>

-
- can_use_premium_code() ) : ?> -

- -
-
-

-

- -
-
+ +
- -
-

- -
-
+ + + +
-

+