diff --git a/docs/coding-best-practices.md b/docs/coding-best-practices.md index f61f4e82b..158bd6014 100644 --- a/docs/coding-best-practices.md +++ b/docs/coding-best-practices.md @@ -101,4 +101,6 @@ In this project, we do not have cases when the code must do a lot of things at a The challenge in this project is to extend the code to support more features. The more we have, the harder it will be to maintain the code. Fancy tricks without a good reason are not good. -A piece code that is performant, easy to read and understand, and easy to maintain is the best. [But sometime you can not have it all](https://www.youtube.com/watch?v=hFDcoX7s6rE). So, you need to choose what is more important for your case. \ No newline at end of file +A piece code that is performant, easy to read and understand, and easy to maintain is the best. [But sometime you can not have it all](https://www.youtube.com/watch?v=hFDcoX7s6rE). So, you need to choose what is more important for your case. + +If you are using filters or HOC to add, for example, a Toolbar to all Blocks, it's recommended that you make useSelect or other requests inside child components to avoid them being triggered when they're not needed. An example of what type of issues it can cause: https://github.com/Codeinwp/otter-blocks/pull/1974 \ No newline at end of file diff --git a/plugins/blocks-animation/readme.txt b/plugins/blocks-animation/readme.txt index ec12d1e2a..dd300426d 100644 --- a/plugins/blocks-animation/readme.txt +++ b/plugins/blocks-animation/readme.txt @@ -1,7 +1,7 @@ === Blocks Animation: CSS Animations for Gutenberg Blocks === Contributors: themeisle, hardeepasrani, mariamunteanu1 Tags: gutenberg, block, block editor, editor, animation, animations, animate, styles, block animations -Requires at least: 5.9 +Requires at least: 6.2 Tested up to: 6.4 Requires PHP: 5.4 Stable tag: trunk diff --git a/plugins/blocks-css/readme.txt b/plugins/blocks-css/readme.txt index 403b63c28..9a842ca7b 100644 --- a/plugins/blocks-css/readme.txt +++ b/plugins/blocks-css/readme.txt @@ -1,7 +1,7 @@ === Blocks CSS: CSS Editor for Gutenberg Blocks === Contributors: themeisle, hardeepasrani Tags: gutenberg, block, css, css editor, blocks css -Requires at least: 5.9 +Requires at least: 6.2 Tested up to: 6.4 Requires PHP: 5.4 Stable tag: trunk diff --git a/plugins/blocks-export-import/readme.txt b/plugins/blocks-export-import/readme.txt index b413e7fd8..93797a203 100644 --- a/plugins/blocks-export-import/readme.txt +++ b/plugins/blocks-export-import/readme.txt @@ -1,7 +1,7 @@ === Blocks Export Import === Contributors: themeisle, hardeepasrani Tags: gutenberg, block, blocks, export, import, exporter, importer, block exporter, block export, block import, block importer -Requires at least: 5.9 +Requires at least: 6.2 Tested up to: 6.4 Requires PHP: 5.4 Stable tag: trunk diff --git a/readme.txt b/readme.txt index b87bea6e7..1bf9aefc5 100644 --- a/readme.txt +++ b/readme.txt @@ -1,7 +1,7 @@ === Otter Blocks - Gutenberg Blocks, Page Builder for Gutenberg Editor & FSE === Contributors: themeisle, hardeepasrani, soarerobertdaniel7, mariamunteanu1, arinat, uriahs-victor, john_pixle, wildmisha, irinelenache Tags: block, blocks, gutenberg, gutenberg blocks, wordPress blocks, editor, block Editor, page Builder, post blocks, post grids -Requires at least: 5.9 +Requires at least: 6.2 Tested up to: 6.4 Requires PHP: 5.6 Stable tag: trunk diff --git a/src/blocks/helpers/use-settings.js b/src/blocks/helpers/use-settings.js index 85e89959e..710bda3de 100644 --- a/src/blocks/helpers/use-settings.js +++ b/src/blocks/helpers/use-settings.js @@ -5,15 +5,15 @@ import api from '@wordpress/api'; import { __ } from '@wordpress/i18n'; -import { dispatch } from '@wordpress/data'; - import { - useEffect, - useState -} from '@wordpress/element'; + dispatch, + useSelect +} from '@wordpress/data'; + +import { useState } from '@wordpress/element'; /** - * useSettings Hook. + * useSettings Hook, modifed for Otter usage. * * useSettings hook to get/update WordPress' settings database. * @@ -29,29 +29,28 @@ import { * @returns {[(optionName: string) => any, (option: string, value: any, success?: string, noticeId?: string, onSuccess: Function) => void, 'loading' | 'loaded' | 'error' | 'saving']} [ getOption, updateOption, status ] * */ +let updatedSettings = {}; const useSettings = () => { const { createNotice } = dispatch( 'core/notices' ); - const [ settings, setSettings ] = useState({}); const [ status, setStatus ] = useState( 'loading' ); + const [ settings, setSettings ] = useState({}); - const getSettings = () => { - api.loadPromise.then( async() => { - try { - const settings = new api.models.Settings(); - const response = await settings.fetch(); - setSettings( response ); - } catch ( error ) { - setStatus( 'error' ); - } finally { - setStatus( 'loaded' ); - } - }); - }; + useSelect( select => { + const { getEntityRecord } = select( 'core' ); + + // Bail out if settings are already loaded. + if ( Object.keys( settings ).length ) { + return; + } + + const request = getEntityRecord( 'root', 'site' ); - useEffect( () => { - getSettings(); - }, []); + if ( request ) { + setStatus( 'loaded' ); + setSettings( request ); + } + }, [ settings ]); /** * Get the value of the given option. @@ -60,7 +59,7 @@ const useSettings = () => { * @returns {any} Option value. */ const getOption = option => { - return settings?.[option]; + return updatedSettings?.[option] || settings?.[option]; }; /** @@ -107,7 +106,8 @@ const useSettings = () => { ); } - getSettings(); + updatedSettings = response; + setSettings( response ); onSuccess?.( response ); }); diff --git a/src/blocks/plugins/ai-content/index.tsx b/src/blocks/plugins/ai-content/index.tsx index b4fd6a280..e638cdff9 100644 --- a/src/blocks/plugins/ai-content/index.tsx +++ b/src/blocks/plugins/ai-content/index.tsx @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; -// @ts-ignore + import { DropdownMenu, MenuGroup, @@ -18,12 +18,9 @@ import { createHigherOrderComponent } from '@wordpress/compose'; import { Fragment, useEffect, useState } from '@wordpress/element'; -import { - addFilter, - applyFilters -} from '@wordpress/hooks'; +import { addFilter } from '@wordpress/hooks'; -import { useDispatch, useSelect, dispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { rawHandler, createBlock } from '@wordpress/blocks'; import { BlockControls } from '@wordpress/block-editor'; @@ -74,161 +71,249 @@ const extractContent = ( source: BlockProps | BlockProps[]): s let embeddedPromptsCache: PromptsData|null = null; -const withConditions = createHigherOrderComponent( BlockEdit => { - return props => { - const [ getOption, _, status ] = useSettings(); - const [ hasAPIKey, setHasAPIKey ] = useState( false ); - const [ isProcessing, setIsProcessing ] = useState>({}); - const [ displayError, setDisplayError ] = useState( undefined ); +const AIToolbar = ({ + props, + onClose +}) => { + const [ getOption, _, status ] = useSettings(); + const [ hasAPIKey, setHasAPIKey ] = useState( false ); + const [ isProcessing, setIsProcessing ] = useState>({}); + const [ displayError, setDisplayError ] = useState( undefined ); + + // Get the create notice function from the hooks api. + const { createNotice } = useDispatch( 'core/notices' ); + + const { + isMultipleSelection, + selectedBlocks + } = useSelect( ( select ) => { + const selectedBlocks = select( 'core/block-editor' ).getMultiSelectedBlocks(); + + return { + isMultipleSelection: 1 < selectedBlocks.length, + selectedBlocks + }; + }, []); + + useEffect( () => { + if ( 'loading' === status ) { + return; + } + + if ( 'loaded' === status && ! hasAPIKey ) { + const key = getOption( openAiAPIKeyName ) as string; + setHasAPIKey( Boolean( key ) && 0 < key.length ); + } + }, [ status, getOption ]); + + useEffect( () => { + if ( ! displayError ) { + return; + } + + createNotice( + 'error', + displayError, + { + type: 'snackbar', + isDismissible: true + } + ); - // Get the create notice function from the hooks api. - const { createNotice } = useDispatch( 'core/notices' ); + setDisplayError( undefined ); + }, [ displayError ]); - const { - isMultipleSelection, - areValidBlocks, - selectedBlocks, - isHidden - } = useSelect( ( select ) => { - const selectedBlocks = select( 'core/block-editor' ).getMultiSelectedBlocks(); - const hiddenBlocks = select( 'core/preferences' )?.get( 'core/edit-post', 'hiddenBlockTypes' ) || []; - - return { - isMultipleSelection: 1 < selectedBlocks.length, - areValidBlocks: selectedBlocks.every( ( block ) => isValidBlock( block.name ) ), - selectedBlocks, - isHidden: hiddenBlocks.find( ( blockName: string ) => 'themeisle-blocks/content-generator' === blockName ) ?? false - }; - }, []); + const generateContent = async( content: string, actionKey: string, callback: Function = () =>{}) => { - useEffect( () => { - if ( 'loading' === status ) { - return; - } + if ( ! content ) { + setDisplayError( __( 'No content detected in selected block.', 'otter-blocks' ) ); + return; + } - if ( 'loaded' === status && ! hasAPIKey ) { - const key = getOption( openAiAPIKeyName ) as string; - setHasAPIKey( Boolean( key ) && 0 < key.length ); - } - }, [ status, getOption ]); + if ( ! embeddedPromptsCache ) { + const response = await retrieveEmbeddedPrompt( 'textTransformation' ); + embeddedPromptsCache = response?.prompts ?? []; + } - useEffect( () => { - if ( ! displayError ) { - return; - } + const embeddedPrompt = embeddedPromptsCache?.find( ( prompt ) => 'textTransformation' === prompt.otter_name ); - createNotice( - 'error', - displayError, - { - type: 'snackbar', - isDismissible: true - } - ); + if ( ! embeddedPrompt ) { + setDisplayError( __( 'Something when wrong retrieving the prompts.', 'otter-blocks' ) ); + return; + } - setDisplayError( undefined ); - }, [ displayError ]); + const action: undefined | string = embeddedPrompt?.[actionKey]; - const generateContent = async( content: string, actionKey: string, callback: Function = () =>{}) => { + if ( ! action ) { + setDisplayError( __( 'The action is not longer available.', 'otter-blocks' ) ); + return; + } - if ( ! content ) { - setDisplayError( __( 'No content detected in selected block.', 'otter-blocks' ) ); - return; - } + if ( ! hasAPIKey ) { + setDisplayError( __( 'No Open API key detected. Please add your key.', 'otter-blocks' ) ); + return; + } - if ( ! embeddedPromptsCache ) { - const response = await retrieveEmbeddedPrompt( 'textTransformation' ); - embeddedPromptsCache = response?.prompts ?? []; - } + setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: true }) ); - const embeddedPrompt = embeddedPromptsCache?.find( ( prompt ) => 'textTransformation' === prompt.otter_name ); + window.oTrk?.add({ feature: 'ai-generation', featureComponent: 'ai-toolbar', featureValue: actionKey }, { consent: true }); - if ( ! embeddedPrompt ) { - setDisplayError( __( 'Something when wrong retrieving the prompts.', 'otter-blocks' ) ); + sendPromptToOpenAI( + content, + injectActionIntoPrompt( + embeddedPrompt, + action + ), + { + 'otter_used_action': `textTransformation::${ actionKey }`, + 'otter_user_content': content + } + ).then( ( response ) => { + if ( response.error ) { + setDisplayError( response.error?.message ?? response.error ); return; } - const action: undefined | string = embeddedPrompt?.[actionKey]; + const blockContentRaw = response?.choices?.[0]?.message.content; - if ( ! action ) { - setDisplayError( __( 'The action is not longer available.', 'otter-blocks' ) ); + if ( ! blockContentRaw ) { return; } - if ( ! hasAPIKey ) { - setDisplayError( __( 'No Open API key detected. Please add your key.', 'otter-blocks' ) ); - return; - } + const newBlocks = rawHandler({ + HTML: blockContentRaw + }); - setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: true }) ); + const aiBlock = createBlock( + 'themeisle-blocks/content-generator', + { + promptID: 'textTransformation', + resultHistory: [{ + result: response?.choices?.[0]?.message.content ?? '', + meta: { + usedToken: response?.usage.total_tokens, + prompt: '' + } + }] + }, + newBlocks + ); - window.oTrk?.add({ feature: 'ai-generation', featureComponent: 'ai-toolbar', featureValue: actionKey }, { consent: true }); + insertBlockBelow( props.clientId, aiBlock ); - sendPromptToOpenAI( - content, - injectActionIntoPrompt( - embeddedPrompt, - action - ), - { - 'otter_used_action': `textTransformation::${ actionKey }`, - 'otter_user_content': content - } - ).then( ( response ) => { - if ( response.error ) { - setDisplayError( response.error?.message ?? response.error ); - return; - } - - const blockContentRaw = response?.choices?.[0]?.message.content; - - if ( ! blockContentRaw ) { - return; - } - - const newBlocks = rawHandler({ - HTML: blockContentRaw - }); - - const aiBlock = createBlock( - 'themeisle-blocks/content-generator', - { - promptID: 'textTransformation', - resultHistory: [{ - result: response?.choices?.[0]?.message.content ?? '', - meta: { - usedToken: response?.usage.total_tokens, - prompt: '' + setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: false }) ); + callback?.(); + }).catch( ( error ) => { + setDisplayError( error.message ); + setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: false }) ); + }); + }; + + const ActionMenuItem = ( args: { actionKey: string, children: React.ReactNode, callback: Function }) => { + return ( + x )}> + { + generateContent( extractContent( isMultipleSelection ? selectedBlocks : props ), args.actionKey, () => args.callback?.( args.actionKey ) ); + }} + > + { args.children } + { isProcessing?.[args.actionKey] && } + + + ); + }; + + return ( + + { + ( ! hasAPIKey ) && ( + + + { __( 'Please add your OpenAI API key in Integrations.', 'otter-blocks' ) } + + + { + __( 'Go to Dashboard', 'otter-blocks' ) } - }] - }, - newBlocks - ); - - insertBlockBelow( props.clientId, aiBlock ); - - setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: false }) ); - callback?.(); - }).catch( ( error ) => { - setDisplayError( error.message ); - setIsProcessing( prevState => ({ ...prevState, [ actionKey ]: false }) ); - }); - }; + + + ) + } + + {__( 'Writing', 'otter-blocks' )} + + { __( 'Generate a heading', 'otter-blocks' ) } + + + { __( 'Continue writing', 'otter-blocks' ) } + + + { __( 'Summarize it', 'otter-blocks' ) } + + + { __( 'Make it shorter', 'otter-blocks' ) } + + + { __( 'Make it longer', 'otter-blocks' ) } + + + { __( 'Make it more descriptive', 'otter-blocks' ) } + + + + {__( 'Tone', 'otter-blocks' )} + + { __( 'Professional', 'otter-blocks' ) } + + + { __( 'Friendly', 'otter-blocks' ) } + + + { __( 'Humorous', 'otter-blocks' ) } + + + { __( 'Confident', 'otter-blocks' ) } + + + { __( 'Persuasive', 'otter-blocks' ) } + + + { __( 'Casual', 'otter-blocks' ) } + + + + + { __( 'Use as prompt', 'otter-blocks' ) } + + + + + { + __( 'Go to docs', 'otter-blocks' ) + } + + + + ); +}; - const ActionMenuItem = ( args: { actionKey: string, children: React.ReactNode, callback: Function }) => { - return ( - x )}> - { - generateContent( extractContent( isMultipleSelection ? selectedBlocks : props ), args.actionKey, () => args.callback?.( args.actionKey ) ); - }} - > - { args.children } - { isProcessing?.[args.actionKey] && } - - - ); - }; +const withConditions = createHigherOrderComponent( BlockEdit => { + return props => { + const { + isMultipleSelection, + areValidBlocks, + isHidden + } = useSelect( ( select ) => { + const selectedBlocks = select( 'core/block-editor' ).getMultiSelectedBlocks(); + const hiddenBlocks = select( 'core/preferences' )?.get( 'core/edit-post', 'hiddenBlockTypes' ) || []; + + return { + isMultipleSelection: 1 < selectedBlocks.length, + areValidBlocks: selectedBlocks.every( ( block ) => isValidBlock( block.name ) ), + isHidden: hiddenBlocks.find( ( blockName: string ) => 'themeisle-blocks/content-generator' === blockName ) ?? false + }; + }, []); return ( @@ -244,81 +329,12 @@ const withConditions = createHigherOrderComponent( BlockEdit => { { ({ onClose }) => ( - - { - ( ! hasAPIKey ) && ( - - - { __( 'Please add your OpenAI API key in Integrations.', 'otter-blocks' ) } - - - { - __( 'Go to Dashboard', 'otter-blocks' ) - } - - - ) - } - - {__( 'Writing', 'otter-blocks' )} - - { __( 'Generate a heading', 'otter-blocks' ) } - - - { __( 'Continue writing', 'otter-blocks' ) } - - - { __( 'Summarize it', 'otter-blocks' ) } - - - { __( 'Make it shorter', 'otter-blocks' ) } - - - { __( 'Make it longer', 'otter-blocks' ) } - - - { __( 'Make it more descriptive', 'otter-blocks' ) } - - - - {__( 'Tone', 'otter-blocks' )} - - { __( 'Professional', 'otter-blocks' ) } - - - { __( 'Friendly', 'otter-blocks' ) } - - - { __( 'Humorous', 'otter-blocks' ) } - - - { __( 'Confident', 'otter-blocks' ) } - - - { __( 'Persuasive', 'otter-blocks' ) } - - - { __( 'Casual', 'otter-blocks' ) } - - - - - { __( 'Use as prompt', 'otter-blocks' ) } - - - - - { - __( 'Go to docs', 'otter-blocks' ) // TODO: Add link to docs & CSS styling - } - - - + ) } diff --git a/src/blocks/plugins/options/index.js b/src/blocks/plugins/options/index.js index 8d20a02a0..3cb12cd26 100644 --- a/src/blocks/plugins/options/index.js +++ b/src/blocks/plugins/options/index.js @@ -89,7 +89,7 @@ export let NavigatorButton = ({ ); }; -const Options = () => { +const Sidebar = () => { const { isOnboardingVisible, get } = useSelect( select => { const { isOnboardingVisible } = select( 'themeisle-gutenberg/data' ); const get = select( 'core/preferences' )?.get; @@ -273,14 +273,198 @@ const Options = () => { Controls = globalControls.find( item => item.name === selectedBlock ).control; } - const navigator = useNavigator(); - const enabledModules = { 'css': Boolean( getOption( 'themeisle_blocks_settings_css_module' ) ), 'animation': Boolean( getOption( 'themeisle_blocks_settings_blocks_animation' ) ), 'condition': Boolean( getOption( 'themeisle_blocks_settings_block_conditions' ) ) }; + return ( + + + + + + + + + { + canUser && ( + + +

+ { + __( 'Make those features to be shown by default in Block Tools.', 'otter-blocks' ) + } +

+ + { + 'loading' === status && ( +

+ + { __( 'Checking optional module...', 'otter-blocks' ) } +

+ ) + } + + { + enabledModules?.css && ( + + updatedWithStatus( dispatch( 'core/preferences' )?.set( 'themeisle/otter-blocks', 'show-custom-css', value ) )} + /> + + ) + } + + { + enabledModules?.animation && ( + + updatedWithStatus( dispatch( 'core/preferences' )?.set( 'themeisle/otter-blocks', 'show-animations', value ) )} + /> + + ) + } + + { + enabledModules?.condition && ( + + updatedWithStatus( dispatch( 'core/preferences' )?.set( 'themeisle/otter-blocks', 'show-block-conditions', value ) )} + /> + + ) + } +
+ + + { __( 'Block Settings', 'otter-blocks' ) } + +
+ ) + } + + { applyFilters( 'otter.feedback', '', 'otter-menu-editor', __( 'Help us improve Otter Blocks', 'otter-blocks' ) ) } +
+ + + + { __( 'Settings', 'otter-blocks' ) } + + + + + + + + { __( 'Blocks', 'otter-blocks' ) } + + + { selectedBlock && ( + + + +
+ + + +
+
+ )} +
+
+ ); +}; + +const Options = () => { + useEffect( () => { + let isMounted = true; + + const fetchData = async() => { + const data = await apiFetch({ path: 'wp/v2/users/me?context=edit' }); + + if ( data.capabilities.manage_options && isMounted ) { + setCanUser( true ); + } else { + setCanUser( false ); + setAPILoaded( true ); + } + }; + + fetchData(); + + return () => { + isMounted = false; + }; + }, []); + + const [ canUser, setCanUser ] = useState( false ); + return ( { ( canUser ) && ( @@ -295,164 +479,7 @@ const Options = () => { title={ __( 'Otter Options', 'otter-blocks' ) } name="otter-options" > - - - - - - - - - { - canUser && ( - - -

- { - __( 'Make those features to be shown by default in Block Tools.', 'otter-blocks' ) - } -

- - { - 'loading' === status && ( -

- - { __( 'Checking optional module...', 'otter-blocks' ) } -

- ) - } - - { - enabledModules?.css && ( - - updatedWithStatus( dispatch( 'core/preferences' )?.set( 'themeisle/otter-blocks', 'show-custom-css', value ) )} - /> - - ) - } - - { - enabledModules?.animation && ( - - updatedWithStatus( dispatch( 'core/preferences' )?.set( 'themeisle/otter-blocks', 'show-animations', value ) )} - /> - - ) - } - - { - enabledModules?.condition && ( - - updatedWithStatus( dispatch( 'core/preferences' )?.set( 'themeisle/otter-blocks', 'show-block-conditions', value ) )} - /> - - ) - } -
- - - { __( 'Block Settings', 'otter-blocks' ) } - -
- ) - } - - { applyFilters( 'otter.feedback', '', 'otter-menu-editor', __( 'Help us improve Otter Blocks', 'otter-blocks' ) ) } -
- - - - { __( 'Settings', 'otter-blocks' ) } - - - - - - - - { __( 'Blocks', 'otter-blocks' ) } - - - { selectedBlock && ( - - - -
- - - -
-
- )} -
-
+
);