From afccadc87ff3301b57fc063f07d978e4cda3e72f Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Mon, 12 Jun 2023 17:23:37 +0300 Subject: [PATCH 001/107] feat: add prompt prototype --- src/blocks/blocks/form/edit.js | 40 ++++++- src/blocks/components/index.js | 2 + src/blocks/components/prompt/editor.scss | 6 + src/blocks/components/prompt/index.tsx | 45 ++++++++ src/blocks/helpers/prompt.ts | 137 +++++++++++++++++++++++ 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/blocks/components/prompt/editor.scss create mode 100644 src/blocks/components/prompt/index.tsx create mode 100644 src/blocks/helpers/prompt.ts diff --git a/src/blocks/blocks/form/edit.js b/src/blocks/blocks/form/edit.js index 08710f29e..5d3f33c4b 100644 --- a/src/blocks/blocks/form/edit.js +++ b/src/blocks/blocks/form/edit.js @@ -17,7 +17,7 @@ import api from '@wordpress/api'; import apiFetch from '@wordpress/api-fetch'; import { - __experimentalBlockVariationPicker as VariationPicker, + __experimentalBlockVariationPicker as VariationPicker, BlockControls, InnerBlocks, RichText, useBlockProps @@ -53,7 +53,9 @@ import Inspector from './inspector.js'; import Placeholder from './placeholder.js'; import { useResponsiveAttributes } from '../../helpers/utility-hooks'; import { renderBoxOrNumWithUnit, _cssBlock, _px, findInnerBlocks } from '../../helpers/helper-functions'; -import { Notice } from '@wordpress/components'; +import { Button, Notice, ToolbarGroup } from '@wordpress/components'; +import PromptPlaceholder from '../../components/prompt'; +import { parseFormPromptResponse, sendPromptToOpenAI } from '../../helpers/prompt'; const { attributes: defaultAttributes } = metadata; @@ -874,6 +876,10 @@ const Edit = ({ } }; + const [ showPrompt, setShowPrompt ] = useState( false ); + const [ prompt, setPrompt ] = useState( '' ); + const [ generationStatus, setGenerationStatus ] = useState( 'default' ); + return ( + + + + + + +
+ { + ( showPrompt ) && ( + { + sendPromptToOpenAI( prompt ).then ( ({ result, error }) => { + replaceInnerBlocks( clientId, parseFormPromptResponse( result ) ); + setGenerationStatus( 'default' ); + }); + setGenerationStatus( 'loading' ); + }} + /> + ) + } + { ( hasInnerBlocks ) ? (
void + onSubmit: () => void + status?: 'default' | 'loading' | 'success' | 'error' +}; + +const PromptPlaceholder = ( props: PromptPlaceholderProps ) => { + const { title, description, value, onValueChange, onSubmit, status } = props; + + return ( + + + +
+ +
+
+ ); +}; + +export default PromptPlaceholder; diff --git a/src/blocks/helpers/prompt.ts b/src/blocks/helpers/prompt.ts new file mode 100644 index 000000000..36d823154 --- /dev/null +++ b/src/blocks/helpers/prompt.ts @@ -0,0 +1,137 @@ +import { createBlock } from '@wordpress/blocks'; + +const OPENAI_API_KEY = 'ADD_YOUR_KEY'; + +const embeddedPrompt = 'Add instruction '; + +type PromptResponse = { + result: string + error?: string +} + +type ChatResponse = { + choices: { + finish_reason: string, + index: number, + message: { + content: string, + role: string + } + }[] + created: number + id: string + model: string + object: string + usage: { + completion_tokens: number, + prompt_tokens: number, + total_tokens: number + } +} + +type FormResponse = { + label: string + type: string + placeholder?: string + helpText?: string + choices?: string[] + allowedFileTypes?: string[] + required?: boolean +}[] + +export async function sendPromptToOpenAI( prompt: string ) { + + // Make a request to the OpenAI API using fetch then parse the response + + const response = await fetch( 'https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + + // The Authorization header contains your API key + Authorization: `Bearer ${OPENAI_API_KEY}` + }, + body: JSON.stringify({ + model: 'gpt-3.5-turbo', + messages: [{ + role: 'user', + content: embeddedPrompt + prompt + }], + temperature: 0.2, + // eslint-disable-next-line camelcase + top_p: 1, + stream: false + }) + }); + + if ( ! response.ok ) { + return { + result: '', + error: `Error ${response.status}: ${response.statusText}` + }; + } + + const data = await response.json() as ChatResponse; + + return { + result: data.choices?.[0]?.message.content ?? '', + error: undefined + }; +} + +const fieldMapping = { + 'text': 'themeisle-blocks/form-input', + 'email': 'themeisle-blocks/form-input', + 'password': 'themeisle-blocks/form-input', + 'number': 'themeisle-blocks/form-input', + 'tel': 'themeisle-blocks/form-input', + 'url': 'themeisle-blocks/form-input', + 'date': 'themeisle-blocks/form-input', + 'time': 'themeisle-blocks/form-input', + 'select': 'themeisle-blocks/form-multiple-choice', + 'checkbox': 'themeisle-blocks/form-multiple-choice', + 'radio': 'themeisle-blocks/form-multiple-choice', + 'file': 'themeisle-blocks/form-file', + 'textarea': 'themeisle-blocks/form-textarea' + +}; + +export function parseFormPromptResponse( promptResponse: string ) { + if ( ! promptResponse ) { + return []; + } + + const formResponse = JSON.parse( promptResponse ) as FormResponse; + + return formResponse.map( ( field ) => { + return createBlock( fieldMapping[field.type], { + label: field.label, + placeholder: field.placeholder, + helpText: field.helpText, + options: field.choices?.join( '\n' ), + allowedFileTypes: field.allowedFileTypes + }); + }).filter( Boolean ); +} + +export function replaceInnerBlockWithPrompt( blockClientId: string, promptResponse: string ) { + + console.log( '[Call] replaceInnerBlockWithPrompt', blockClientId, promptResponse ); + + if ( blockClientId === undefined ) { + return; + } + + const formFields = parseFormPromptResponse( promptResponse ); + + if ( ! formFields.length ) { + return; + } + + console.log( 'replaceInnerBlockWithPrompt', blockClientId, formFields ); + + const { replaceInnerBlocks } = window.wp.data.dispatch( 'core/block-editor' ); + + replaceInnerBlocks( blockClientId, formFields ); +} + From c7fe27ca8005d7721d316dfe6a6d90541ef04c66 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Thu, 15 Jun 2023 16:24:46 +0300 Subject: [PATCH 002/107] feat: add initial demo for Form AI --- inc/class-main.php | 1 + inc/plugins/class-options-settings.php | 12 + inc/server/class-prompt-server.php | 248 +++++++++++++++++++++ src/blocks/blocks/form/edit.js | 15 +- src/blocks/components/prompt/editor.scss | 32 +++ src/blocks/components/prompt/index.tsx | 265 ++++++++++++++++++++++- src/blocks/helpers/prompt.ts | 48 +++- 7 files changed, 595 insertions(+), 26 deletions(-) create mode 100644 inc/server/class-prompt-server.php diff --git a/inc/class-main.php b/inc/class-main.php index 02cc5ab8a..f2518a4eb 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -74,6 +74,7 @@ public function autoload_classes() { '\ThemeIsle\GutenbergBlocks\Integration\Form_Providers', '\ThemeIsle\GutenbergBlocks\Integration\Form_Email', '\ThemeIsle\GutenbergBlocks\Server\Form_Server', + '\ThemeIsle\GutenbergBlocks\Server\Prompt_Server', ); $classnames = apply_filters( 'otter_blocks_autoloader', $classnames ); diff --git a/inc/plugins/class-options-settings.php b/inc/plugins/class-options-settings.php index 5cb795b00..62eb48d33 100644 --- a/inc/plugins/class-options-settings.php +++ b/inc/plugins/class-options-settings.php @@ -511,6 +511,18 @@ function ( $item ) { ), ) ); + + register_setting( + 'themeisle_blocks_settings', + 'themeisle_open_ai_api_key', + array( + 'type' => 'string', + 'description' => __( 'The OpenAI API Key required for usage of Otter AI features.', 'otter-blocks' ), + 'sanitize_callback' => 'sanitize_text_field', + 'show_in_rest' => true, + 'default' => '', + ) + ); } /** diff --git a/inc/server/class-prompt-server.php b/inc/server/class-prompt-server.php new file mode 100644 index 000000000..6168de1ac --- /dev/null +++ b/inc/server/class-prompt-server.php @@ -0,0 +1,248 @@ +namespace . $this->version; + + register_rest_route( + $namespace, + '/prompt', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_prompts' ), + 'permission_callback' => function () { + return current_user_can( 'edit_posts' ); + }, + ), + ) + ); + + } + + /** + * Get prompts. + * + * @param \WP_REST_Request $request Request object. + * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response + */ + public function get_prompts( $request ) { + $response = array( + 'prompts' => array(), + 'code' => '0', + 'error' => '', + ); + + // Get the saved prompts. + $prompts = get_transient( $this->transient_prompts ); + + if ( false === $prompts ) { + /** + * If we don't have the prompts saved, we need to retrieve them from the server. Once retrieved, we save them in a transient and return them. + */ + $response = $this->retrieve_prompts_from_server(); + } + + if ( '0' === $response['code'] ) { + if ( $request->get_param( 'name' ) !== null ) { + $prompts = false !== $prompts ? $prompts : $response['prompts']; + // Prompt can be filtered by name. By filtering by name, we can get only the prompt we need and save some bandwidth. + $response['prompts'] = array_filter( + $prompts, + function ( $prompt ) use ( $request ) { + return $prompt['name'] === $request->get_param( 'name' ); + } + ); + + if ( empty( $response['prompts'] ) ) { + $response['prompts'] = array(); + $response['code'] = '1'; + $response['error'] = __( 'Something went wrong when preparing the data for this feature.', 'otter-blocks' ); + } + } else { + $response['prompts'] = $prompts; + } + } + + + return rest_ensure_response( $response ); + } + + /** + * + * Retrieve prompts from server. + * + * @return array + */ + public function retrieve_prompts_from_server() { + + if ( false !== get_transient( $this->timeout_transient ) ) { + return array( + 'response' => array(), + 'code' => '3', + 'error' => __( 'Fetching from central server has failed. Please try again later.', 'otter-blocks' ), + ); + } + + $url = add_query_arg( + array( + 'site_url' => get_site_url(), + 'license_id' => apply_filters( 'product_otter_license_key', 'free' ), + 'cache' => gmdate( 'u' ), + ), + 'http://localhost:3000/prompts' + ); + + $response = ''; + + if ( function_exists( 'vip_safe_wp_remote_get' ) ) { + $response = vip_safe_wp_remote_get( esc_url_raw( $url ) ); + } else { + $response = wp_remote_get( esc_url_raw( $url ) ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get + } + + $response = wp_remote_retrieve_body( $response ); + $response = json_decode( $response, true ); + + if ( ! is_array( $response ) || 0 === count( $response ) || ! $this->check_prompt_structure( $response ) ) { + set_transient( $this->timeout_transient, '1', HOUR_IN_SECONDS ); + return array( + 'response' => array(), + 'code' => '2', + 'error' => __( 'Could not fetch the data from the central server.', 'otter-blocks' ), + ); + } + + set_transient( $this->transient_prompts, $response, WEEK_IN_SECONDS ); + + return array( + 'response' => $response, + 'code' => '0', + 'error' => '', + ); + } + + /** + * Check if the prompt structure is valid. + * + * @param mixed $response Response from the server. + * @return bool + */ + public function check_prompt_structure( $response ) { + if ( ! isset( $response ) ) { + return false; + } + + if ( ! is_array( $response ) ) { + return false; + } + + if ( 0 === count( $response ) ) { + return false; + } + + return true; + } + + + /** + * The instance method for the static class. + * Defines and returns the instance of the static class. + * + * @static + * @since 1.7.0 + * @access public + * @return Prompt_Server + */ + public static function instance() { + if ( is_null( self::$instance ) ) { + self::$instance = new self(); + self::$instance->init(); + } + + return self::$instance; + } + + /** + * Throw error on object clone + * + * The whole idea of the singleton design pattern is that there is a single + * object therefore, we don't want the object to be cloned. + * + * @access public + * @since 1.7.0 + * @return void + */ + public function __clone() { + // Cloning instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'otter-blocks' ), '1.0.0' ); + } + + /** + * Disable unserializing of the class + * + * @access public + * @since 1.7.0 + * @return void + */ + public function __wakeup() { + // Unserializing instances of the class is forbidden. + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'otter-blocks' ), '1.0.0' ); + } +} diff --git a/src/blocks/blocks/form/edit.js b/src/blocks/blocks/form/edit.js index 3085aa41c..ce1f45bcc 100644 --- a/src/blocks/blocks/form/edit.js +++ b/src/blocks/blocks/form/edit.js @@ -55,7 +55,7 @@ import { useResponsiveAttributes } from '../../helpers/utility-hooks'; import { renderBoxOrNumWithUnit, _cssBlock, _px, findInnerBlocks } from '../../helpers/helper-functions'; import { Button, Notice, ToolbarGroup } from '@wordpress/components'; import PromptPlaceholder from '../../components/prompt'; -import { parseFormPromptResponse, sendPromptToOpenAI } from '../../helpers/prompt'; +import { parseFormPromptResponseToBlocks, sendPromptToOpenAI } from '../../helpers/prompt'; const { attributes: defaultAttributes } = metadata; @@ -877,9 +877,8 @@ const Edit = ({ } }; - const [ showPrompt, setShowPrompt ] = useState( false ); + const [ showPrompt, setShowPrompt ] = useState( true ); const [ prompt, setPrompt ] = useState( '' ); - const [ generationStatus, setGenerationStatus ] = useState( 'default' ); return ( @@ -925,15 +924,11 @@ const Edit = ({ { ( showPrompt ) && ( { - sendPromptToOpenAI( prompt ).then ( ({ result, error }) => { - replaceInnerBlocks( clientId, parseFormPromptResponse( result ) ); - setGenerationStatus( 'default' ); - }); - setGenerationStatus( 'loading' ); + onSuccess={( result )=>{ + replaceInnerBlocks( clientId, parseFormPromptResponseToBlocks( result ) ); }} /> ) diff --git a/src/blocks/components/prompt/editor.scss b/src/blocks/components/prompt/editor.scss index dd7b98084..897cd73aa 100644 --- a/src/blocks/components/prompt/editor.scss +++ b/src/blocks/components/prompt/editor.scss @@ -3,4 +3,36 @@ display: flex; flex-direction: column; } + + .prompt-placeholder__block-generation__container { + margin: 10px; + display: flex; + flex-direction: column; + padding: 10px; + border-radius: 10px; + gap: 10px; + font-size: 1.2em; + + background-color: #FAA26A; + + .prompt-placeholder__block-generation__field { + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; + } + + .prompt-placeholder__block-generation__field__type { + padding: 1px 5px; + background-color: azure; + border-radius: 5px; + min-width: 100px; + text-align: center; + } + } + + .history-display { + padding: 0px 20px; + font-size: 1.2em; + } } diff --git a/src/blocks/components/prompt/index.tsx b/src/blocks/components/prompt/index.tsx index 37e750871..97ac7a129 100644 --- a/src/blocks/components/prompt/index.tsx +++ b/src/blocks/components/prompt/index.tsx @@ -1,19 +1,209 @@ import { __ } from '@wordpress/i18n'; -import { Button, Placeholder, Spinner, TextControl } from '@wordpress/components'; +import { Button, ExternalLink, Placeholder, Spinner, TextControl } from '@wordpress/components'; import './editor.scss'; -import { Fragment } from '@wordpress/element'; +import { Fragment, useEffect, useState } from '@wordpress/element'; +import useSettings from '../../helpers/use-settings'; +import { + parseFormPromptResponseToBlocks, + parseToDisplayPromptResponse, + PromptsData, + retrieveEmbeddedPrompt, + sendPromptToOpenAI +} from '../../helpers/prompt'; type PromptPlaceholderProps = { + promptName?: string title?: string description?: string value: string onValueChange: ( text: string ) => void - onSubmit: () => void - status?: 'default' | 'loading' | 'success' | 'error' + onSuccess?: ( result: string ) => void +}; + +export const apiKeyName = 'themeisle_open_ai_api_key'; + +const BlockGenerationArea = ( props: { result?: string }) => { + return ( +
+ { props.result?.length && parseToDisplayPromptResponse( props.result ).map( field => { + return ( +
+
+ { field.type } +
+
+ { field.label } +
+
+ ); + }) } +
+ ); }; const PromptPlaceholder = ( props: PromptPlaceholderProps ) => { - const { title, description, value, onValueChange, onSubmit, status } = props; + const { title, description, value, onValueChange, onSuccess, promptName } = props; + + const [ getOption, updateOption, status ] = useSettings(); + const [ apiKey, setApiKey ] = useState( null ); + + const [ generationStatus, setGenerationStatus ] = useState<'loading' | 'loaded' | 'error'>( 'loaded' ); + + const [ apiKeyStatus, setApiKeyStatus ] = useState<'checking' | 'missing' | 'present' | 'error'>( 'checking' ); + const [ embeddedPrompts, setEmbeddedPrompts ] = useState([]); + const [ result, setResult ] = useState( undefined ); + + const [ resultHistory, setResultHistory ] = useState([]); + const [ resultHistoryIndex, setResultHistoryIndex ] = useState( 0 ); + + useEffect( () => { + const getEmbeddedPrompt = async() => { + retrieveEmbeddedPrompt( promptName ).then( ( promptServer ) => { + setEmbeddedPrompts( promptServer.prompts ); + console.log( promptServer ); + }); + }; + + getEmbeddedPrompt(); + }, []); + + useEffect( () => { + if ( 'loading' === status ) { + return; + } + + if ( 'loaded' === status ) { + if ( getOption( apiKeyName ) ) { + setApiKeyStatus( 'present' ); + setApiKey( getOption( apiKeyName ) ); + } else { + setApiKeyStatus( 'missing' ); + } + } + + if ( 'error' === status ) { + setApiKeyStatus( 'error' ); + } + }, [ status, getOption ]); + + useEffect( () => { + setResultHistoryIndex( resultHistory.length - 1 ); + }, [ resultHistory ]); + + useEffect( () => { + + if ( ! result ) { + return; + } + + if ( 0 > resultHistoryIndex ) { + return; + } + + if ( resultHistoryIndex > resultHistory.length - 1 ) { + return; + } + + setResult( resultHistory[ resultHistoryIndex ]); + + }, [ resultHistoryIndex, resultHistory ]); + + function onSubmit() { + + const embeddedPrompt = embeddedPrompts?.find( ( prompt ) => prompt.name === promptName ); + + if ( ! embeddedPrompt ) { + console.warn( 'Prompt not found' ); + return; + } + + if ( ! apiKey ) { + console.warn( 'API Key not found' ); + return; + } + + setGenerationStatus( 'loading' ); + + sendPromptToOpenAI( value, apiKey, embeddedPrompt.prompt ).then ( ({ result, error }) => { + if ( error ) { + console.error( error ); + setGenerationStatus( 'error' ); + return; + } + + setGenerationStatus( 'loaded' ); + + setResult( result ); + setResultHistory([ ...resultHistory, result ]); + }); + } + + if ( 'present' !== apiKeyStatus ) { + return ( + + { + 'checking' === apiKeyStatus && ( +
+ + { __( 'Checking the api key...', 'otter-blocks' ) } +
+ ) + } + + { + 'missing' === apiKeyStatus && ( + + { __( 'API Key not found. Please introduce the API Key', 'otter-blocks' ) } + + { + setApiKey( text ); + }} + /> +
+ +
+

+ + + { __( 'Get your API Key', 'otter-blocks' ) } + + + ) + } + + ); + + } return ( { + { + 0 < resultHistory.length && ( + + { + 1 < resultHistory.length && ( + + { `${resultHistoryIndex + 1}/${resultHistory.length}` } + + ) + } + + { + 0 < resultHistoryIndex && ( + + ) + + } + + { + resultHistoryIndex < resultHistory.length - 1 && ( + + ) + } + + + ) + }

+ + + + { + result && ( +
+ + +
+ ) + } ); }; diff --git a/src/blocks/helpers/prompt.ts b/src/blocks/helpers/prompt.ts index 36d823154..71710d6bd 100644 --- a/src/blocks/helpers/prompt.ts +++ b/src/blocks/helpers/prompt.ts @@ -1,8 +1,6 @@ import { createBlock } from '@wordpress/blocks'; - -const OPENAI_API_KEY = 'ADD_YOUR_KEY'; - -const embeddedPrompt = 'Add instruction '; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; type PromptResponse = { result: string @@ -39,7 +37,18 @@ type FormResponse = { required?: boolean }[] -export async function sendPromptToOpenAI( prompt: string ) { +export type PromptsData = { + name: string + prompt: string +}[] + +type PromptServerResponse = { + code: string + error: string + prompts: PromptsData +} + +export async function sendPromptToOpenAI( prompt: string, apiKey: string, embeddedPrompt: string ) { // Make a request to the OpenAI API using fetch then parse the response @@ -49,7 +58,7 @@ export async function sendPromptToOpenAI( prompt: string ) { 'Content-Type': 'application/json', // The Authorization header contains your API key - Authorization: `Bearer ${OPENAI_API_KEY}` + Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'gpt-3.5-turbo', @@ -96,7 +105,22 @@ const fieldMapping = { }; -export function parseFormPromptResponse( promptResponse: string ) { +export function parseToDisplayPromptResponse( promptResponse: string ) { + const response = JSON.parse( promptResponse ) as FormResponse; + + return response.map( ( field ) => { + return { + label: field.label, + type: field.type, + placeholder: field.placeholder, + helpText: field.helpText, + options: field.choices?.join( '\n' ), + allowedFileTypes: field.allowedFileTypes + }; + }).filter( Boolean ); +} + +export function parseFormPromptResponseToBlocks( promptResponse: string ) { if ( ! promptResponse ) { return []; } @@ -122,7 +146,7 @@ export function replaceInnerBlockWithPrompt( blockClientId: string, promptRespon return; } - const formFields = parseFormPromptResponse( promptResponse ); + const formFields = parseFormPromptResponseToBlocks( promptResponse ); if ( ! formFields.length ) { return; @@ -135,3 +159,11 @@ export function replaceInnerBlockWithPrompt( blockClientId: string, promptRespon replaceInnerBlocks( blockClientId, formFields ); } +export function retrieveEmbeddedPrompt( promptName ?: string ) { + return apiFetch({ + path: addQueryArgs( '/otter/v1/prompt', { + name: promptName + }), + method: 'GET' + }); +} From a81399a4fb71d30824f749a70a0e24e0abfc6904 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Tue, 20 Jun 2023 18:35:40 +0300 Subject: [PATCH 003/107] feat: add AI Block --- blocks.json | 3 + .../blocks/content-generator/block.json | 24 ++++ src/blocks/blocks/content-generator/edit.js | 126 ++++++++++++++++++ .../blocks/content-generator/editor.scss | 1 + src/blocks/blocks/content-generator/index.js | 27 ++++ .../blocks/content-generator/inspector.js | 46 +++++++ .../blocks/content-generator/types.d.ts | 10 ++ src/blocks/blocks/form/edit.js | 32 ++--- src/blocks/blocks/index.js | 1 + src/blocks/components/prompt/index.tsx | 20 ++- src/blocks/helpers/prompt.ts | 32 +++-- 11 files changed, 293 insertions(+), 29 deletions(-) create mode 100644 src/blocks/blocks/content-generator/block.json create mode 100644 src/blocks/blocks/content-generator/edit.js create mode 100644 src/blocks/blocks/content-generator/editor.scss create mode 100644 src/blocks/blocks/content-generator/index.js create mode 100644 src/blocks/blocks/content-generator/inspector.js create mode 100644 src/blocks/blocks/content-generator/types.d.ts diff --git a/blocks.json b/blocks.json index e740e559d..9e1e90334 100644 --- a/blocks.json +++ b/blocks.json @@ -272,5 +272,8 @@ "product-upsells": { "isPro": true, "block": "pro/woocommerce/upsells/block.json" + }, + "content-generator": { + "block": "blocks/blocks/content-generator/block.json" } } diff --git a/src/blocks/blocks/content-generator/block.json b/src/blocks/blocks/content-generator/block.json new file mode 100644 index 000000000..d3e573caf --- /dev/null +++ b/src/blocks/blocks/content-generator/block.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "themeisle-blocks/content-generator", + "title": "AI Content Generator", + "category": "themeisle-blocks", + "description": "Generate content for your block with AI.", + "keywords": [ + "content", + "generator", + "ai", + "layout" + ], + "textdomain": "otter-blocks", + "attributes": { + "blockToReplace": { + "type": "string" + }, + "generationType": { + "type": "string", + "default": "form" + } + } +} diff --git a/src/blocks/blocks/content-generator/edit.js b/src/blocks/blocks/content-generator/edit.js new file mode 100644 index 000000000..f4239571d --- /dev/null +++ b/src/blocks/blocks/content-generator/edit.js @@ -0,0 +1,126 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Button, ResizableBox } from '@wordpress/components'; + +import { + InnerBlocks, + RichText, + useBlockProps +} from '@wordpress/block-editor'; + +import { + Fragment, + useEffect, + useRef, + useState +} from '@wordpress/element'; + +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import Inspector from './inspector.js'; +import PromptPlaceholder from '../../components/prompt'; +import { parseFormPromptResponseToBlocks } from '../../helpers/prompt'; +import { useDispatch } from '@wordpress/data'; + +const { attributes: defaultAttributes } = metadata; + +const PRESETS = { + form: { + title: 'Form Generator', + description: 'Write what type of form do you want to have.' + } +}; + +/** + * Progress Bar Block + * @param {import('./types').ContentGeneratorProps} props + */ +const ContentGenerator = ({ + attributes, + setAttributes, + isSelected, + clientId, + toggleSelection +}) => { + + const blockProps = useBlockProps(); + + const [ prompt, setPrompt ] = useState( '' ); + + const { + insertBlock, + removeBlock, + replaceInnerBlocks, + selectBlock, + moveBlockToPosition + } = useDispatch( 'core/block-editor' ); + + /** + * On success callback + * + * @type {import('../../components/prompt').PromptOnSuccess} + */ + const onSuccess = ( result, actions ) => { + if ( 'form' === attributes.generationType ) { + + const formFields = parseFormPromptResponseToBlocks( result ); + + const form = createBlock( 'themeisle-blocks/form', {}, formFields ); + + console.log( form ); + + actions.clearHistory(); + + replaceInnerBlocks( clientId, [ form ]); + + + } + }; + + return ( + + + +
+ + + +
+ +
+ +
+ +
+ ); +}; + +export default ContentGenerator; diff --git a/src/blocks/blocks/content-generator/editor.scss b/src/blocks/blocks/content-generator/editor.scss new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/blocks/blocks/content-generator/editor.scss @@ -0,0 +1 @@ + diff --git a/src/blocks/blocks/content-generator/index.js b/src/blocks/blocks/content-generator/index.js new file mode 100644 index 000000000..e7dc50cfa --- /dev/null +++ b/src/blocks/blocks/content-generator/index.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { progressIcon as icon } from '../../helpers/icons.js'; +import edit from './edit.js'; + +const { name } = metadata; + +registerBlockType( name, { + ...metadata, + icon, + keywords: [ + 'content', + 'ai', + 'layout' + ], + edit, + save: () => null +}); diff --git a/src/blocks/blocks/content-generator/inspector.js b/src/blocks/blocks/content-generator/inspector.js new file mode 100644 index 000000000..cd908fb60 --- /dev/null +++ b/src/blocks/blocks/content-generator/inspector.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { clamp } from 'lodash'; + +import { + InspectorControls, + PanelColorSettings +} from '@wordpress/block-editor'; + +import { + BaseControl, + PanelBody, + RangeControl, + SelectControl, + TextControl, + FontSizePicker +} from '@wordpress/components'; + + +/** + * + * @param {import('./types').ContentGeneratorInspectorProps} props + * @returns + */ +const Inspector = ({ + attributes, + setAttributes +}) => { + + + return ( + + + + + + + ); +}; + +export default Inspector; diff --git a/src/blocks/blocks/content-generator/types.d.ts b/src/blocks/blocks/content-generator/types.d.ts new file mode 100644 index 000000000..f0c92b8ca --- /dev/null +++ b/src/blocks/blocks/content-generator/types.d.ts @@ -0,0 +1,10 @@ +import { BlockProps, InspectorProps } from '../../helpers/blocks'; + +type Attributes = { + blockToReplace: string; + generationType: string; +} + +export type ContentGeneratorAttrs = Partial +export type ContentGeneratorProps = BlockProps +export interface ContentGeneratorInspectorProps extends InspectorProps {} diff --git a/src/blocks/blocks/form/edit.js b/src/blocks/blocks/form/edit.js index ce1f45bcc..b092f0c18 100644 --- a/src/blocks/blocks/form/edit.js +++ b/src/blocks/blocks/form/edit.js @@ -190,12 +190,17 @@ const Edit = ({ const [ hasEmailField, setHasEmailField ] = useState( false ); - const { children, hasProtection } = useSelect( select => { + const { children, hasProtection, currentBlockPosition } = useSelect( select => { const { - getBlock + getBlock, + getBlockOrder } = select( 'core/block-editor' ); const children = getBlock( clientId ).innerBlocks; + + const currentBlockPosition = getBlockOrder().indexOf( clientId ); + return { + currentBlockPosition, children, hasProtection: 0 < children?.filter( ({ name }) => 'themeisle-blocks/form-nonce' === name )?.length }; @@ -877,9 +882,6 @@ const Edit = ({ } }; - const [ showPrompt, setShowPrompt ] = useState( true ); - const [ prompt, setPrompt ] = useState( '' ); - return ( + ({ + label: block.name, + value: block.clientId + }) ) ?? []) + ]} + onChange={value => { + console.log( 'Replace original block with new one' ); + setAttributes({ + blockToReplace: value + }); + }} + /> + + ) + } -
-
-
); }; diff --git a/src/blocks/blocks/content-generator/editor.scss b/src/blocks/blocks/content-generator/editor.scss index 8b1378917..3732e9839 100644 --- a/src/blocks/blocks/content-generator/editor.scss +++ b/src/blocks/blocks/content-generator/editor.scss @@ -1 +1,19 @@ +.wp-block-themeisle-blocks-content-generator { + padding: 10px; + border-radius: 10px; + background-color: #1ee5de; + .block-editor-inner-blocks { + padding: 10px; + border-radius: 5px; + background-color: #fff; + margin: 10px 0px + } + + .o-actions { + display: flex; + flex-direction: row; + gap: 10px; + align-items: baseline; + } +} diff --git a/src/blocks/blocks/content-generator/index.js b/src/blocks/blocks/content-generator/index.js index e7dc50cfa..26e03a8f9 100644 --- a/src/blocks/blocks/content-generator/index.js +++ b/src/blocks/blocks/content-generator/index.js @@ -11,6 +11,7 @@ import { registerBlockType } from '@wordpress/blocks'; import metadata from './block.json'; import { progressIcon as icon } from '../../helpers/icons.js'; import edit from './edit.js'; +import './editor.scss'; const { name } = metadata; diff --git a/src/blocks/components/prompt/index.tsx b/src/blocks/components/prompt/index.tsx index 3acc291ce..b47d31f6f 100644 --- a/src/blocks/components/prompt/index.tsx +++ b/src/blocks/components/prompt/index.tsx @@ -295,7 +295,7 @@ const PromptPlaceholder = ( props: PromptPlaceholderProps ) => { onSuccess?.( result, onSuccessActions ); }} > - { __( 'Accept', 'otter-blocks' ) } + { __( 'Preview', 'otter-blocks' ) } From 6325546aac9de0ea4bfd77afdbddf0e6d3117339 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Wed, 21 Jun 2023 19:05:58 +0300 Subject: [PATCH 005/107] feat: form submissions dashboard --- inc/plugins/class-dashboard.php | 309 ++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index 532bf1123..d695e2272 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -28,6 +28,7 @@ public function init() { add_action( 'admin_menu', array( $this, 'register_menu_page' ) ); add_action( 'admin_init', array( $this, 'maybe_redirect' ) ); add_action( 'admin_notices', array( $this, 'maybe_add_otter_banner' ), 30 ); + add_action( 'wp_dashboard_setup', array( $this, 'form_submissions_widget' ) ); } /** @@ -266,6 +267,314 @@ private function the_otter_banner() { 'otter_form_record', + 'posts_per_page' => -1, + ); + + if ( 'all' !== $posts_filter ) { + $query_args['post_status'] = $posts_filter; + } + + $query = new \WP_Query( $query_args ); + $entries = array(); + $display_size = 5; + $display_index = 0; + + $count = $query->found_posts; + + if ( $query->have_posts() ) { + + while ( $query->have_posts() ) { + + $display_index ++; + + if ( $display_index > $display_size ) { + break; + } + + $query->the_post(); + + $meta = get_post_meta( get_the_ID(), 'otter_form_record_meta', true ); + + $title = null; + $post = null; + + if ( isset( $meta['post_id'] ) ) { + $post = get_the_title( $meta['post_id'] ); + } + + if ( isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ) { + // Find the first email field and use that as the title. + foreach ( $meta['inputs'] as $input ) { + if ( isset( $input['type'] ) && 'email' === $input['type'] ) { + $title = $input['value']; + break; + } + } + } + + + if ( ! $title ) { + + if ( isset( $meta['post_id']['value'] ) ) { + $title = 'Submission #' . get_the_ID(); + } else { + $title = __( 'No title', 'otter-blocks' ); + } + } + + $entries[] = array( + 'title' => $title, + 'post' => $post, + ); + } + } + + ?> + + + + +
+ + +
+
+ Otter Logo +
+ +
+

+ +
+
+ + +
+
+
+ + + + : + + + + +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ Date: Thu, 22 Jun 2023 10:22:42 +0300 Subject: [PATCH 006/107] chore: phpcs --- inc/plugins/class-dashboard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index d695e2272..2cf39b5f7 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -496,7 +496,7 @@ public function form_submissions_widget_content() { // change the url param based on the value const url = new URL(window.location.href); url.searchParams.set('otter_form_widget_filter', value); - url.searchParams.set('otter_nonce', '') + url.searchParams.set('otter_nonce', '') // go to the new url using the href window.location.href = url.href; From edf4f2b24ad4180d3b912499e6c8a90bbe9560b0 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Thu, 22 Jun 2023 11:30:28 +0300 Subject: [PATCH 007/107] chore: review --- inc/plugins/class-dashboard.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index 2cf39b5f7..ce7a4b987 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -322,16 +322,16 @@ public function form_submissions_widget_content() { $meta = get_post_meta( get_the_ID(), 'otter_form_record_meta', true ); $title = null; - $post = null; + $date = null; - if ( isset( $meta['post_id'] ) ) { - $post = get_the_title( $meta['post_id'] ); + if ( isset( $meta['post_id']['value'] ) ) { + $date = get_the_date( 'F j, H:i', $meta['post_id']['value'] ); } if ( isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ) { // Find the first email field and use that as the title. foreach ( $meta['inputs'] as $input ) { - if ( isset( $input['type'] ) && 'email' === $input['type'] ) { + if ( isset( $input['type'] ) && 'email' === $input['type'] && ! empty( $input['value'] ) ) { $title = $input['value']; break; } @@ -350,7 +350,7 @@ public function form_submissions_widget_content() { $entries[] = array( 'title' => $title, - 'post' => $post, + 'date' => $date, ); } } @@ -362,7 +362,7 @@ public function form_submissions_widget_content() { justify-content: center; align-items: center; width: 100%; - margin-bottom: 10px; + margin-bottom: 8px; } .o-upsell-banner { @@ -374,8 +374,7 @@ public function form_submissions_widget_content() { gap: 12px; isolation: isolate; - width: 445px; - height: 273.4px; + width: fit-content; background: #FFFFFF; box-shadow: 0px 2px 25px 10px rgba(0, 0, 0, 0.08); @@ -387,6 +386,7 @@ public function form_submissions_widget_content() { order: 0; align-self: stretch; flex-grow: 0; + } .o-upsell-banner .o-banner-tile { @@ -402,6 +402,7 @@ public function form_submissions_widget_content() { display: flex; align-items: center; text-align: center; + margin: 0px; } .o-upsell-banner a { @@ -424,7 +425,7 @@ public function form_submissions_widget_content() { } .otter-form-submissions-widget { - padding: 24px 12px 0px 12px; + padding: 6px 3px 0px 3px; } .otter-form-submissions-widget a { @@ -446,6 +447,7 @@ public function form_submissions_widget_content() { flex-direction: row; justify-content: space-between; align-items: center; + font-size: 14px; } @@ -465,7 +467,7 @@ public function form_submissions_widget_content() { } .o-entry:not(:last-child) { - padding-bottom: 5px; + padding-bottom: 6px; border-bottom: 1px solid #eee; } @@ -514,8 +516,8 @@ public function form_submissions_widget_content() {
-

- +

+ @@ -547,10 +549,10 @@ public function form_submissions_widget_content() {
- +
@@ -559,7 +561,8 @@ public function form_submissions_widget_content() { From 0a9718c77944a7930c38ca6cc1f22511c0f21bb5 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Thu, 22 Jun 2023 16:04:54 +0300 Subject: [PATCH 008/107] feat: add hidden form field --- blocks.json | 3 + plugins/otter-pro/inc/class-main.php | 2 + .../inc/plugins/class-form-emails-storing.php | 12 +++ .../render/class-form-hidden-block.pnp.php | 54 +++++++++++ src/blocks/blocks/form/edit.js | 1 - src/blocks/blocks/form/editor.scss | 8 ++ .../blocks/form/hidden-field/block.json | 26 ++++++ src/blocks/blocks/form/hidden-field/index.js | 41 +++++++++ .../blocks/form/hidden-field/inspector.js | 71 ++++++++++++++ .../blocks/form/hidden-field/types.d.ts | 12 +++ src/blocks/blocks/form/index.js | 1 + src/blocks/frontend/form/index.js | 14 ++- src/pro/blocks/form-hidden-field/block.json | 26 ++++++ src/pro/blocks/form-hidden-field/edit.js | 92 +++++++++++++++++++ src/pro/blocks/form-hidden-field/index.js | 41 +++++++++ src/pro/blocks/form-hidden-field/inspector.js | 91 ++++++++++++++++++ src/pro/blocks/form-hidden-field/types.d.ts | 12 +++ src/pro/blocks/index.js | 1 + 18 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 plugins/otter-pro/inc/render/class-form-hidden-block.pnp.php create mode 100644 src/blocks/blocks/form/hidden-field/block.json create mode 100644 src/blocks/blocks/form/hidden-field/index.js create mode 100644 src/blocks/blocks/form/hidden-field/inspector.js create mode 100644 src/blocks/blocks/form/hidden-field/types.d.ts create mode 100644 src/pro/blocks/form-hidden-field/block.json create mode 100644 src/pro/blocks/form-hidden-field/edit.js create mode 100644 src/pro/blocks/form-hidden-field/index.js create mode 100644 src/pro/blocks/form-hidden-field/inspector.js create mode 100644 src/pro/blocks/form-hidden-field/types.d.ts diff --git a/blocks.json b/blocks.json index e740e559d..fdddd4e0b 100644 --- a/blocks.json +++ b/blocks.json @@ -99,6 +99,9 @@ "form-file": { "block": "blocks/blocks/form/file/block.json" }, + "form-hidden-field": { + "block": "blocks/blocks/form/hidden-field/block.json" + }, "google-map": { "block": "blocks/blocks/google-map/block.json", "assets": { diff --git a/plugins/otter-pro/inc/class-main.php b/plugins/otter-pro/inc/class-main.php index 5c0a50d29..3068e1667 100644 --- a/plugins/otter-pro/inc/class-main.php +++ b/plugins/otter-pro/inc/class-main.php @@ -104,6 +104,7 @@ public function register_blocks( $blocks ) { 'product-upsells', 'review-comparison', 'form-file', + 'form-hidden-field', ); $blocks = array_merge( $blocks, $pro_blocks ); @@ -135,6 +136,7 @@ public function register_dynamic_blocks( $dynamic_blocks ) { 'product-upsells' => '\ThemeIsle\OtterPro\Render\WooCommerce\Product_Upsells_Block', 'review-comparison' => '\ThemeIsle\OtterPro\Render\Review_Comparison_Block', 'form-file' => '\ThemeIsle\OtterPro\Render\Form_File_Block', + 'form-hidden-field' => '\ThemeIsle\OtterPro\Render\Form_Hidden_Block', ); $dynamic_blocks = array_merge( $dynamic_blocks, $blocks ); diff --git a/plugins/otter-pro/inc/plugins/class-form-emails-storing.php b/plugins/otter-pro/inc/plugins/class-form-emails-storing.php index 7efdf83fe..efef2ce4e 100644 --- a/plugins/otter-pro/inc/plugins/class-form-emails-storing.php +++ b/plugins/otter-pro/inc/plugins/class-form-emails-storing.php @@ -789,6 +789,18 @@ class="otter_form_record_meta__value" + + '; + + $output .= ''; + + $output .= ''; + + $output .= ''; + + + return $output; + } + + +} diff --git a/src/blocks/blocks/form/edit.js b/src/blocks/blocks/form/edit.js index 3a9029201..0f896c446 100644 --- a/src/blocks/blocks/form/edit.js +++ b/src/blocks/blocks/form/edit.js @@ -53,7 +53,6 @@ import Inspector from './inspector.js'; import Placeholder from './placeholder.js'; import { useResponsiveAttributes } from '../../helpers/utility-hooks'; import { renderBoxOrNumWithUnit, _cssBlock, _px, findInnerBlocks } from '../../helpers/helper-functions'; -import { Notice } from '@wordpress/components'; const { attributes: defaultAttributes } = metadata; diff --git a/src/blocks/blocks/form/editor.scss b/src/blocks/blocks/form/editor.scss index 19bfc062c..e964ae585 100644 --- a/src/blocks/blocks/form/editor.scss +++ b/src/blocks/blocks/form/editor.scss @@ -49,3 +49,11 @@ border-top: 1px solid #ededed; } } + +.o-hidden-field-mark { + padding: 5px 10px; + background-color: var(--label-color, #2B2784); + color: white; + border-radius: 5px; + margin-right: 5px; +} diff --git a/src/blocks/blocks/form/hidden-field/block.json b/src/blocks/blocks/form/hidden-field/block.json new file mode 100644 index 000000000..34960f21e --- /dev/null +++ b/src/blocks/blocks/form/hidden-field/block.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "themeisle-blocks/form-hidden-field", + "title": "Hidden Field", + "category": "themeisle-blocks", + "description": "A field used for adding extra metadata to the Form via URL params.", + "keywords": [ "metadata", "hidden", "field" ], + "textdomain": "otter-blocks", + "ancestor": [ "themeisle-blocks/form" ], + "attributes": { + "id": { + "type": "string" + }, + "label": { + "type": "string", + "default": "Hidden Field" + }, + "paramName": { + "type": "string" + } + }, + "supports": { + "align": [ "wide", "full" ] + } +} diff --git a/src/blocks/blocks/form/hidden-field/index.js b/src/blocks/blocks/form/hidden-field/index.js new file mode 100644 index 000000000..a8fbed33b --- /dev/null +++ b/src/blocks/blocks/form/hidden-field/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { formFieldIcon as icon } from '../../../helpers/icons.js'; +import Inspector from '../file/inspector'; + +const { name } = metadata; + +if ( ! window.themeisleGutenberg.isAncestorTypeAvailable ) { + metadata.parent = [ 'themeisle-blocks/form' ]; +} + +if ( ! Boolean( window.themeisleGutenberg.hasPro ) ) { + + registerBlockType( name, { + ...metadata, + title: __( 'Nonce Field', 'otter-blocks' ), + description: __( 'Protect the form from CSRF.', 'otter-blocks' ), + icon, + keywords: [ + 'protection', + 'csrf', + 'field' + ], + edit: ( props ) => { + return ( + + ); + }, + save: () => null + }); + +} diff --git a/src/blocks/blocks/form/hidden-field/inspector.js b/src/blocks/blocks/form/hidden-field/inspector.js new file mode 100644 index 000000000..4b5a09372 --- /dev/null +++ b/src/blocks/blocks/form/hidden-field/inspector.js @@ -0,0 +1,71 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + InspectorControls, + PanelColorSettings +} from '@wordpress/block-editor'; + +import { + Button, + PanelBody, + SelectControl, + TextControl, + ToggleControl +} from '@wordpress/components'; +import { useContext } from '@wordpress/element'; +import { FormContext } from '../edit'; + + +/** + * + * @param {import('./types').FormHiddenFieldInspectorPros} props + * @returns {JSX.Element} + */ +const Inspector = ({ + attributes, + setAttributes +}) => { + + const { + selectForm + } = useContext( FormContext ); + + return ( + + + + + + + + + + + ); +}; + +export default Inspector; diff --git a/src/blocks/blocks/form/hidden-field/types.d.ts b/src/blocks/blocks/form/hidden-field/types.d.ts new file mode 100644 index 000000000..16e9732c7 --- /dev/null +++ b/src/blocks/blocks/form/hidden-field/types.d.ts @@ -0,0 +1,12 @@ +import { BlockProps, InspectorProps } from '../../../helpers/blocks'; + + +type Attributes = { + id: string + formId: string + label: string + paramName: string +} + +export type FormHiddenFieldProps = BlockProps +export type FormHiddenFieldInspectorPros = InspectorProps diff --git a/src/blocks/blocks/form/index.js b/src/blocks/blocks/form/index.js index 15fd42e73..04afed768 100644 --- a/src/blocks/blocks/form/index.js +++ b/src/blocks/blocks/form/index.js @@ -18,6 +18,7 @@ import './nonce/index.js'; import './textarea/index.js'; import './multiple-choice/index.js'; import './file/index.js'; +import './hidden-field/index.js'; const { name } = metadata; diff --git a/src/blocks/frontend/form/index.js b/src/blocks/frontend/form/index.js index 3c2ec262a..7456dc933 100644 --- a/src/blocks/frontend/form/index.js +++ b/src/blocks/frontend/form/index.js @@ -23,7 +23,7 @@ const getFormFieldInputs = ( form ) => { * * @type {Array.} */ - return [ ...form?.querySelectorAll( ':scope > .otter-form__container .wp-block-themeisle-blocks-form-input, :scope > .otter-form__container .wp-block-themeisle-blocks-form-textarea, :scope > .otter-form__container .wp-block-themeisle-blocks-form-multiple-choice, :scope > .otter-form__container .wp-block-themeisle-blocks-form-file' ) ].filter( input => { + return [ ...form?.querySelectorAll( ':scope > .otter-form__container .wp-block-themeisle-blocks-form-input, :scope > .otter-form__container .wp-block-themeisle-blocks-form-textarea, :scope > .otter-form__container .wp-block-themeisle-blocks-form-multiple-choice, :scope > .otter-form__container .wp-block-themeisle-blocks-form-file, :scope > .otter-form__container > .wp-block-themeisle-blocks-form-hidden-field ' ) ].filter( input => { return ! innerForms?.some( innerForm => innerForm?.contains( input ) ); }); }; @@ -57,7 +57,7 @@ const extractFormFields = async( form ) => { let fieldType = undefined; const { id } = input; - const valueElem = input.querySelector( '.otter-form-input:not([type="checkbox"], [type="radio"], [type="file"]), .otter-form-textarea-input' ); + const valueElem = input.querySelector( '.otter-form-input:not([type="checkbox"], [type="radio"], [type="file"], [type="hidden"]), .otter-form-textarea-input' ); if ( null !== valueElem ) { value = valueElem?.value; fieldType = valueElem?.type; @@ -67,6 +67,8 @@ const extractFormFields = async( form ) => { /** @type{HTMLInputElement} */ const fileInput = input.querySelector( 'input[type="file"]' ); + const hiddenInput = input.querySelector( 'input[type="hidden"]' ); + if ( fileInput ) { const files = fileInput?.files; @@ -89,6 +91,14 @@ const extractFormFields = async( form ) => { } else if ( select ) { value = [ ...select.selectedOptions ].map( o => o?.label )?.filter( l => Boolean( l ) ).join( ', ' ); fieldType = 'multiple-choice'; + } else if ( hiddenInput ) { + const paramName = hiddenInput?.dataset?.paramName; + + if ( paramName ) { + const urlParams = new URLSearchParams( window.location.search ); + value = urlParams.get( paramName ); + fieldType = 'hidden'; + } } else { const labels = input.querySelectorAll( '.o-form-multiple-choice-field > label' ); const valuesElem = input.querySelectorAll( '.o-form-multiple-choice-field > input' ); diff --git a/src/pro/blocks/form-hidden-field/block.json b/src/pro/blocks/form-hidden-field/block.json new file mode 100644 index 000000000..8c25c5d7b --- /dev/null +++ b/src/pro/blocks/form-hidden-field/block.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "themeisle-blocks/form-hidden-field", + "title": "Hidden Field", + "category": "themeisle-blocks", + "description": "A field used for adding extra metadata to the Form via URL params.", + "keywords": [ "metadata", "hidden", "field" ], + "textdomain": "otter-blocks", + "ancestor": [ "themeisle-blocks/form" ], + "attributes": { + "id": { + "type": "string" + }, + "label": { + "type": "string", + "default": "Metadata" + }, + "paramName": { + "type": "string" + } + }, + "supports": { + "align": [ "wide", "full" ] + } +} diff --git a/src/pro/blocks/form-hidden-field/edit.js b/src/pro/blocks/form-hidden-field/edit.js new file mode 100644 index 000000000..53bc94c28 --- /dev/null +++ b/src/pro/blocks/form-hidden-field/edit.js @@ -0,0 +1,92 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +import { Fragment, useEffect } from '@wordpress/element'; +import { RichText, useBlockProps } from '@wordpress/block-editor'; +import { _cssBlock } from '../../../blocks/helpers/helper-functions'; +import { __ } from '@wordpress/i18n'; +import { blockInit } from '../../../blocks/helpers/block-utility'; +import metadata from '../../../blocks/blocks/form/block.json'; +const { attributes: defaultAttributes } = metadata; +import Inspector from './inspector'; + +/** + * Form Nonce component + * @param {import('./types').FormHiddenFieldProps} props + * @returns + */ +const Edit = ({ + attributes, + setAttributes, + clientId +}) => { + + useEffect( () => { + const unsubscribe = blockInit( clientId, defaultAttributes ); + return () => unsubscribe( attributes.id ); + }, [ attributes.id ]); + + const blockProps = useBlockProps({ + className: 'wp-block wp-block-themeisle-blocks-form-input' + }); + + const placeholder = attributes.paramName ? __( 'Get the value of the URL param: ', 'otter-blocks' ) + attributes.paramName : ''; + + return ( + + + +
+ + + + + { + attributes.helpText && ( + + { attributes.helpText } + + ) + } +
+
+ ); +}; + +export default Edit; diff --git a/src/pro/blocks/form-hidden-field/index.js b/src/pro/blocks/form-hidden-field/index.js new file mode 100644 index 000000000..bb1f7b0f5 --- /dev/null +++ b/src/pro/blocks/form-hidden-field/index.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { formFieldIcon as icon } from '../../../blocks/helpers/icons.js'; +import edit from './edit.js'; + +import { useBlockProps } from '@wordpress/block-editor'; +import Inactive from '../../components/inactive'; + + +const { name } = metadata; + +console.log( 'Hidden Field Added' ); + +if ( ! window.themeisleGutenberg.isAncestorTypeAvailable ) { + metadata.parent = [ 'themeisle-blocks/form' ]; +} + +// if ( ! ( Boolean( window.otterPro.isActive ) && ! Boolean( window.otterPro.isExpired ) ) ) { +// edit = () => ; +// } + + +registerBlockType( name, { + ...metadata, + icon, + edit, + save: () => null +}); diff --git a/src/pro/blocks/form-hidden-field/inspector.js b/src/pro/blocks/form-hidden-field/inspector.js new file mode 100644 index 000000000..ac12f7500 --- /dev/null +++ b/src/pro/blocks/form-hidden-field/inspector.js @@ -0,0 +1,91 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + InspectorControls, + PanelColorSettings +} from '@wordpress/block-editor'; + +import { + Button, + PanelBody, + SelectControl, + TextControl, + ToggleControl +} from '@wordpress/components'; +import { applyFilters } from '@wordpress/hooks'; +import { Notice as OtterNotice } from '../../../blocks/components'; +import { Fragment } from '@wordpress/element'; + + +/** + * + * @param {import('./types').FormHiddenFieldInspectorPros} props + * @returns {JSX.Element} + */ +const Inspector = ({ + attributes, + setAttributes, + clientId +}) => { + + // FormContext is not available here. This is a workaround. + const selectForm = () => { + const formParentId = Array.from( document.querySelectorAll( `.wp-block-themeisle-blocks-form:has(#block-${clientId})` ) )?.pop()?.dataset?.block; + dispatch( 'core/block-editor' ).selectBlock( formParentId ); + }; + + return ( + + + + + setAttributes({ label }) } + help={ __( 'The label will be used as the field name.', 'otter-blocks' ) } + disabled={! Boolean( window?.otterPro?.isActive )} + /> + + setAttributes({ paramName }) } + help={ __( 'The query parameter name that is used in URL. If the param is present, its value will be extracted and send with the Form.', 'otter-blocks' ) } + placeholder={ __( 'e.g. utm_source', 'otter-blocks' ) } + disabled={! Boolean( window?.otterPro?.isActive )} + /> + + { ! Boolean( window?.otterPro?.isActive ) && ( + + + + ) + + } + +
+ { applyFilters( 'otter.feedback', '', 'sticky' ) } + { applyFilters( 'otter.poweredBy', '' ) } +
+
+ +
+ ); +}; + +export default Inspector; diff --git a/src/pro/blocks/form-hidden-field/types.d.ts b/src/pro/blocks/form-hidden-field/types.d.ts new file mode 100644 index 000000000..9c3aeb4d8 --- /dev/null +++ b/src/pro/blocks/form-hidden-field/types.d.ts @@ -0,0 +1,12 @@ +import { BlockProps, InspectorProps } from '../../helpers/blocks'; + + +type Attributes = { + id: string + formId: string + label: string + paramName: string +} + +export type FormHiddenFieldProps = BlockProps +export type FormHiddenFieldInspectorPros = InspectorProps diff --git a/src/pro/blocks/index.js b/src/pro/blocks/index.js index 853c2acdf..c5f5e0b4f 100644 --- a/src/pro/blocks/index.js +++ b/src/pro/blocks/index.js @@ -6,3 +6,4 @@ import './business-hours/index.js'; import './review-comparison/index.js'; import './woo-comparison/index.js'; import './file/index.js'; +import './form-hidden-field/index.js'; From b5535cefedcad7ef741d87872bdc1b88a0a60f4b Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Thu, 22 Jun 2023 16:25:08 +0300 Subject: [PATCH 009/107] chore: add upsell in free --- .../blocks/form/hidden-field/inspector.js | 22 ++++++++++++++----- src/pro/blocks/form-hidden-field/edit.js | 8 +++++-- src/pro/blocks/form-hidden-field/index.js | 4 ---- src/pro/blocks/form-hidden-field/inspector.js | 12 +++++----- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/blocks/blocks/form/hidden-field/inspector.js b/src/blocks/blocks/form/hidden-field/inspector.js index 4b5a09372..e36748d80 100644 --- a/src/blocks/blocks/form/hidden-field/inspector.js +++ b/src/blocks/blocks/form/hidden-field/inspector.js @@ -4,19 +4,24 @@ import { __ } from '@wordpress/i18n'; import { - InspectorControls, - PanelColorSettings + InspectorControls } from '@wordpress/block-editor'; import { - Button, + Button, ExternalLink, PanelBody, - SelectControl, - TextControl, - ToggleControl + TextControl } from '@wordpress/components'; + import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ + import { FormContext } from '../edit'; +import { Notice } from '../../../components'; +import { setUtm } from '../../../helpers/helper-functions'; /** @@ -62,6 +67,11 @@ const Inspector = ({ placeholder={ __( 'e.g. utm_source', 'otter-blocks' ) } disabled={true} /> + + { __( 'Get more options with Otter Pro. ', 'otter-blocks' ) } } + variant="upsell" + /> ) diff --git a/src/pro/blocks/form-hidden-field/edit.js b/src/pro/blocks/form-hidden-field/edit.js index 53bc94c28..aa2d1ee5c 100644 --- a/src/pro/blocks/form-hidden-field/edit.js +++ b/src/pro/blocks/form-hidden-field/edit.js @@ -1,12 +1,16 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; import { Fragment, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; import { RichText, useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ + import { _cssBlock } from '../../../blocks/helpers/helper-functions'; -import { __ } from '@wordpress/i18n'; import { blockInit } from '../../../blocks/helpers/block-utility'; import metadata from '../../../blocks/blocks/form/block.json'; const { attributes: defaultAttributes } = metadata; diff --git a/src/pro/blocks/form-hidden-field/index.js b/src/pro/blocks/form-hidden-field/index.js index bb1f7b0f5..c5804be58 100644 --- a/src/pro/blocks/form-hidden-field/index.js +++ b/src/pro/blocks/form-hidden-field/index.js @@ -12,10 +12,6 @@ import metadata from './block.json'; import { formFieldIcon as icon } from '../../../blocks/helpers/icons.js'; import edit from './edit.js'; -import { useBlockProps } from '@wordpress/block-editor'; -import Inactive from '../../components/inactive'; - - const { name } = metadata; console.log( 'Hidden Field Added' ); diff --git a/src/pro/blocks/form-hidden-field/inspector.js b/src/pro/blocks/form-hidden-field/inspector.js index ac12f7500..42487134e 100644 --- a/src/pro/blocks/form-hidden-field/inspector.js +++ b/src/pro/blocks/form-hidden-field/inspector.js @@ -4,18 +4,20 @@ import { __ } from '@wordpress/i18n'; import { - InspectorControls, - PanelColorSettings + InspectorControls } from '@wordpress/block-editor'; import { Button, PanelBody, - SelectControl, - TextControl, - ToggleControl + TextControl } from '@wordpress/components'; import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ + import { Notice as OtterNotice } from '../../../blocks/components'; import { Fragment } from '@wordpress/element'; From 9c1bde19925b77ba71a222281e42cd1d3f5da129 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Thu, 22 Jun 2023 16:48:54 +0300 Subject: [PATCH 010/107] chore: update form e2e test with hidden field --- src/blocks/test/e2e/blocks/form.spec.js | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/blocks/test/e2e/blocks/form.spec.js b/src/blocks/test/e2e/blocks/form.spec.js index c4e77bd1f..cd1687ed4 100644 --- a/src/blocks/test/e2e/blocks/form.spec.js +++ b/src/blocks/test/e2e/blocks/form.spec.js @@ -198,4 +198,42 @@ test.describe( 'Form Block', () => { // TODO: load a file and check if it is uploaded }); + + test( 'insert a hidden field and check if it renders in frontend', async({ page, editor }) => { + + await page.waitForTimeout( 1000 ); + await editor.insertBlock({ name: 'themeisle-blocks/form', innerBlocks: [ + { + name: 'themeisle-blocks/form-hidden-field', + attributes: { + label: 'Hidden Field Test', + paramName: 'test' + } + } + ] }); + + const blocks = await editor.getBlocks(); + + const formBlock = blocks.find( ( block ) => 'themeisle-blocks/form' === block.name ); + expect( formBlock ).toBeTruthy(); + + const fileHiddenBlock = formBlock.innerBlocks.find( ( block ) => 'themeisle-blocks/form-hidden-field' === block.name ); + + expect( fileHiddenBlock ).toBeTruthy(); + + const { attributes } = fileHiddenBlock; + + expect( attributes.id ).toBeTruthy(); + + const postId = await editor.publishPost(); + + await page.goto( `/?p=${postId}&test=123` ); + + const hiddenInput = await page.locator( `#${attributes.id} input[type="hidden"]` ); + + expect( hiddenInput ).toBeTruthy(); + + await expect( hiddenInput ).toHaveAttribute( 'data-param-name', 'test' ); + + }); }); From ad1be75a3a9b86853b7760780d9a2d7a89379ddb Mon Sep 17 00:00:00 2001 From: Hardeep Asrani Date: Fri, 23 Jun 2023 08:59:41 +0530 Subject: [PATCH 011/107] Uppercase label --- src/blocks/blocks/form/common.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/blocks/form/common.tsx b/src/blocks/blocks/form/common.tsx index f6b9bbe48..e23662dc1 100644 --- a/src/blocks/blocks/form/common.tsx +++ b/src/blocks/blocks/form/common.tsx @@ -115,7 +115,7 @@ export const fieldTypesOptions = () => ([ value: 'textarea' }, { - label: __( 'Url', 'otter-blocks' ), + label: __( 'URL', 'otter-blocks' ), value: 'url' } ]); From e8c386dfccb02d074bac10adf165df8826892a49 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Fri, 23 Jun 2023 11:56:08 +0300 Subject: [PATCH 012/107] chore: review --- inc/plugins/class-dashboard.php | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index ce7a4b987..20fd950ed 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -28,7 +28,11 @@ public function init() { add_action( 'admin_menu', array( $this, 'register_menu_page' ) ); add_action( 'admin_init', array( $this, 'maybe_redirect' ) ); add_action( 'admin_notices', array( $this, 'maybe_add_otter_banner' ), 30 ); - add_action( 'wp_dashboard_setup', array( $this, 'form_submissions_widget' ) ); + + $form_options = get_option( 'themeisle_blocks_form_emails' ); + if ( ! empty( $form_options ) ) { + add_action( 'wp_dashboard_setup', array( $this, 'form_submissions_widget' ) ); + } } /** @@ -293,30 +297,29 @@ public function form_submissions_widget_content() { $query_args = array( 'post_type' => 'otter_form_record', - 'posts_per_page' => -1, + 'posts_per_page' => 5, ); if ( 'all' !== $posts_filter ) { $query_args['post_status'] = $posts_filter; } - $query = new \WP_Query( $query_args ); - $entries = array(); - $display_size = 5; - $display_index = 0; + $query = new \WP_Query( $query_args ); + $entries = array(); - $count = $query->found_posts; + $records_count = wp_count_posts( 'otter_form_record' ); - if ( $query->have_posts() ) { - - while ( $query->have_posts() ) { + $count = $records_count->read + $records_count->unread; - $display_index ++; + if ( 'read' === $posts_filter ) { + $count = $records_count->read; + } elseif ( 'unread' === $posts_filter ) { + $count = $records_count->unread; + } - if ( $display_index > $display_size ) { - break; - } + if ( $query->have_posts() ) { + while ( $query->have_posts() ) { $query->the_post(); $meta = get_post_meta( get_the_ID(), 'otter_form_record_meta', true ); @@ -342,7 +345,7 @@ public function form_submissions_widget_content() { if ( ! $title ) { if ( isset( $meta['post_id']['value'] ) ) { - $title = 'Submission #' . get_the_ID(); + $title = __( 'Submission', 'otter-blocks' ) . ' #' . get_the_ID(); } else { $title = __( 'No title', 'otter-blocks' ); } From f418c9f2a12ff632ff88a5cd09b5ae9a34652868 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Fri, 23 Jun 2023 13:33:48 +0300 Subject: [PATCH 013/107] chore: field switching for hidden field --- src/blocks/blocks/form/common.tsx | 5 +++++ src/blocks/blocks/form/hidden-field/inspector.js | 16 +++++++++++++++- src/pro/blocks/form-hidden-field/edit.js | 4 +++- src/pro/blocks/form-hidden-field/inspector.js | 13 +++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/blocks/blocks/form/common.tsx b/src/blocks/blocks/form/common.tsx index e23662dc1..3f625e04a 100644 --- a/src/blocks/blocks/form/common.tsx +++ b/src/blocks/blocks/form/common.tsx @@ -94,6 +94,10 @@ export const fieldTypesOptions = () => ([ label: ( Boolean( window.otterPro?.isActive ) && ! Boolean( window.otterPro?.isExpired ) ) ? __( 'File', 'otter-blocks' ) : __( 'File (Pro)', 'otter-blocks' ), value: 'file' }, + { + label: ( Boolean( window.otterPro?.isActive ) && ! Boolean( window.otterPro?.isExpired ) ) ? __( 'Hidden', 'otter-blocks' ) : __( 'Hidden (Pro)', 'otter-blocks' ), + value: 'hidden' + }, { label: __( 'Number', 'otter-blocks' ), value: 'number' @@ -132,6 +136,7 @@ export const switchFormFieldTo = ( type?: string, clientId ?:string, attributes? [ 'textarea' === type, 'form-textarea' ], [ 'select' === type || 'checkbox' === type || 'radio' === type, 'form-multiple-choice' ], [ 'file' === type, 'form-file' ], + [ 'hidden' === type, 'form-hidden-field' ], [ 'form-input' ] ]); diff --git a/src/blocks/blocks/form/hidden-field/inspector.js b/src/blocks/blocks/form/hidden-field/inspector.js index e36748d80..ab98ddf32 100644 --- a/src/blocks/blocks/form/hidden-field/inspector.js +++ b/src/blocks/blocks/form/hidden-field/inspector.js @@ -8,8 +8,10 @@ import { } from '@wordpress/block-editor'; import { - Button, ExternalLink, + Button, + ExternalLink, PanelBody, + SelectControl, TextControl } from '@wordpress/components'; @@ -22,6 +24,7 @@ import { useContext } from '@wordpress/element'; import { FormContext } from '../edit'; import { Notice } from '../../../components'; import { setUtm } from '../../../helpers/helper-functions'; +import { fieldTypesOptions, switchFormFieldTo } from '../common'; /** @@ -51,6 +54,17 @@ const Inspector = ({ { __( 'Back to the Form', 'otter-blocks' ) } + { + if ( 'hidden' !== type ) { + switchFormFieldTo( type, clientId, attributes ); + } + }} + /> + unsubscribe( attributes.id ); }, [ attributes.id ]); + const blockProps = useBlockProps({ className: 'wp-block wp-block-themeisle-blocks-form-input' }); diff --git a/src/pro/blocks/form-hidden-field/inspector.js b/src/pro/blocks/form-hidden-field/inspector.js index 42487134e..7337eb6b8 100644 --- a/src/pro/blocks/form-hidden-field/inspector.js +++ b/src/pro/blocks/form-hidden-field/inspector.js @@ -10,6 +10,7 @@ import { import { Button, PanelBody, + SelectControl, TextControl } from '@wordpress/components'; import { applyFilters } from '@wordpress/hooks'; @@ -20,6 +21,7 @@ import { applyFilters } from '@wordpress/hooks'; import { Notice as OtterNotice } from '../../../blocks/components'; import { Fragment } from '@wordpress/element'; +import { fieldTypesOptions, switchFormFieldTo } from '../../../blocks/blocks/form/common'; /** @@ -52,6 +54,17 @@ const Inspector = ({ { __( 'Back to the Form', 'otter-blocks' ) } + { + if ( 'hidden' !== type ) { + switchFormFieldTo( type, clientId, attributes ); + } + }} + /> + Date: Fri, 23 Jun 2023 13:38:41 +0300 Subject: [PATCH 014/107] chore: tweaks --- src/pro/blocks/form-hidden-field/index.js | 16 +++++++--------- src/pro/blocks/form-hidden-field/inspector.js | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pro/blocks/form-hidden-field/index.js b/src/pro/blocks/form-hidden-field/index.js index c5804be58..e8a22b86f 100644 --- a/src/pro/blocks/form-hidden-field/index.js +++ b/src/pro/blocks/form-hidden-field/index.js @@ -14,19 +14,17 @@ import edit from './edit.js'; const { name } = metadata; -console.log( 'Hidden Field Added' ); - if ( ! window.themeisleGutenberg.isAncestorTypeAvailable ) { metadata.parent = [ 'themeisle-blocks/form' ]; } -// if ( ! ( Boolean( window.otterPro.isActive ) && ! Boolean( window.otterPro.isExpired ) ) ) { -// edit = () => ; -// } +if ( ! ( Boolean( window.otterPro.isActive ) && ! Boolean( window.otterPro.isExpired ) ) ) { + edit = () => ; +} registerBlockType( name, { diff --git a/src/pro/blocks/form-hidden-field/inspector.js b/src/pro/blocks/form-hidden-field/inspector.js index 7337eb6b8..0615ccd2b 100644 --- a/src/pro/blocks/form-hidden-field/inspector.js +++ b/src/pro/blocks/form-hidden-field/inspector.js @@ -22,6 +22,7 @@ import { applyFilters } from '@wordpress/hooks'; import { Notice as OtterNotice } from '../../../blocks/components'; import { Fragment } from '@wordpress/element'; import { fieldTypesOptions, switchFormFieldTo } from '../../../blocks/blocks/form/common'; +import { dispatch } from '@wordpress/data'; /** From d386e53fc9f2cb08ad118a729f1c2371ea93bca3 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Fri, 23 Jun 2023 17:21:34 +0300 Subject: [PATCH 015/107] chore: change prompts format --- inc/server/class-prompt-server.php | 2 +- src/blocks/components/prompt/index.tsx | 10 +-- src/blocks/helpers/prompt.ts | 89 ++++++++++++++++++-------- 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/inc/server/class-prompt-server.php b/inc/server/class-prompt-server.php index 6168de1ac..31877a071 100644 --- a/inc/server/class-prompt-server.php +++ b/inc/server/class-prompt-server.php @@ -106,7 +106,7 @@ public function get_prompts( $request ) { $response['prompts'] = array_filter( $prompts, function ( $prompt ) use ( $request ) { - return $prompt['name'] === $request->get_param( 'name' ); + return $prompt['otter_name'] === $request->get_param( 'name' ); } ); diff --git a/src/blocks/components/prompt/index.tsx b/src/blocks/components/prompt/index.tsx index b47d31f6f..a5f7e45e4 100644 --- a/src/blocks/components/prompt/index.tsx +++ b/src/blocks/components/prompt/index.tsx @@ -124,7 +124,7 @@ const PromptPlaceholder = ( props: PromptPlaceholderProps ) => { function onSubmit() { - const embeddedPrompt = embeddedPrompts?.find( ( prompt ) => prompt.name === promptName ); + const embeddedPrompt = embeddedPrompts?.find( ( prompt ) => prompt.otter_name === promptName ); if ( ! embeddedPrompt ) { console.warn( 'Prompt not found' ); @@ -138,13 +138,15 @@ const PromptPlaceholder = ( props: PromptPlaceholderProps ) => { setGenerationStatus( 'loading' ); - sendPromptToOpenAI( value, apiKey, embeddedPrompt.prompt ).then ( ({ result, error }) => { - if ( error ) { - console.error( error ); + sendPromptToOpenAI( value, apiKey, embeddedPrompt ).then ( ( data ) => { + if ( data?.error ) { + console.error( data?.error ); setGenerationStatus( 'error' ); return; } + const result = data?.choices?.[0]?.message?.function_call?.arguments; + setGenerationStatus( 'loaded' ); setResult( result ); diff --git a/src/blocks/helpers/prompt.ts b/src/blocks/helpers/prompt.ts index 20b75e4d3..d25be9566 100644 --- a/src/blocks/helpers/prompt.ts +++ b/src/blocks/helpers/prompt.ts @@ -14,6 +14,10 @@ type ChatResponse = { message: { content: string, role: string + function_call?: { + name: string + arguments: string + } } }[] created: number @@ -28,19 +32,34 @@ type ChatResponse = { } type FormResponse = { - label: string - type: string - placeholder?: string - helpText?: string - choices?: string[] - allowedFileTypes?: string[] - required?: boolean -}[] - -export type PromptsData = { - name: string - prompt: string -}[] + fields: { + label: string + type: string + placeholder?: string + helpText?: string + choices?: string[] + allowedFileTypes?: string[] + required?: boolean + }[] +} +export type PromptData = { + otter_name: string + model: string + messages: { + role: string + content: string + }[] + functions: { + name: string + description: string + parameters: any + } + function_call: { + [key: string]: string + } +} + +export type PromptsData = PromptData[] type PromptServerResponse = { code: string @@ -48,9 +67,32 @@ type PromptServerResponse = { prompts: PromptsData } -export async function sendPromptToOpenAI( prompt: string, apiKey: string, embeddedPrompt: string ) { +export async function sendPromptToOpenAI( prompt: string, apiKey: string, embeddedPrompt: PromptData ) { + + const body = { + ...embeddedPrompt, + messages: embeddedPrompt.messages.map( ( message ) => { + if ( 'user' === message.role && message.content.includes( '{INSERT_TASK}' ) ) { + return { + role: 'user', + content: message.content.replace( '{INSERT_TASK}', prompt ) + }; + } + + return message; + }) + }; + + + function removeOtterKeys( obj ) { + for ( let key in obj ) { + if ( key.startsWith( 'otter_' ) ) { + delete obj[key]; + } + } + return obj; + } - // Make a request to the OpenAI API using fetch then parse the response const response = await fetch( 'https://api.openai.com/v1/chat/completions', { method: 'POST', @@ -61,11 +103,7 @@ export async function sendPromptToOpenAI( prompt: string, apiKey: string, embedd Authorization: `Bearer ${apiKey}` }, body: JSON.stringify({ - model: 'gpt-3.5-turbo', - messages: [{ - role: 'user', - content: embeddedPrompt + prompt - }], + ...( removeOtterKeys( body ) ), temperature: 0.2, // eslint-disable-next-line camelcase top_p: 1, @@ -80,12 +118,7 @@ export async function sendPromptToOpenAI( prompt: string, apiKey: string, embedd }; } - const data = await response.json() as ChatResponse; - - return { - result: data.choices?.[0]?.message.content ?? '', - error: undefined - }; + return await response.json() as ChatResponse; } const fieldMapping = { @@ -112,7 +145,7 @@ export function parseToDisplayPromptResponse( promptResponse: string ) { return []; } - return response.map( ( field ) => { + return response?.fields.map( ( field ) => { return { label: field?.label, type: field?.type, @@ -143,7 +176,7 @@ export function parseFormPromptResponseToBlocks( promptResponse: string ) { return []; } - return formResponse.map( ( field ) => { + return formResponse?.fields?.map( ( field ) => { return createBlock( fieldMapping[field.type], { label: field.label, placeholder: field.placeholder, From 32a0e9f027e2250f28f4af6c7437a66c6b281eef Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Wed, 28 Jun 2023 16:50:36 +0300 Subject: [PATCH 016/107] feat: add webhook to form --- inc/integrations/api/form-response-data.php | 2 + inc/integrations/class-form-settings-data.php | 32 ++ inc/plugins/class-options-settings.php | 75 ++++ .../inc/plugins/class-form-pro-features.php | 88 +++++ src/blocks/blocks/form/edit.js | 6 +- src/blocks/blocks/form/inspector.js | 38 ++ src/blocks/blocks/form/type.d.ts | 3 +- src/blocks/editor.scss | 35 ++ src/blocks/helpers/use-settings.js | 2 +- src/pro/components/webhook-editor/index.tsx | 362 ++++++++++++++++++ src/pro/plugins/form/index.js | 42 +- 11 files changed, 680 insertions(+), 5 deletions(-) create mode 100644 src/pro/components/webhook-editor/index.tsx diff --git a/inc/integrations/api/form-response-data.php b/inc/integrations/api/form-response-data.php index 1881e3a83..531fc44ef 100644 --- a/inc/integrations/api/form-response-data.php +++ b/inc/integrations/api/form-response-data.php @@ -57,6 +57,7 @@ class Form_Data_Response { const ERROR_PROVIDER_INVALID_EMAIL = '207'; const ERROR_PROVIDER_DUPLICATED_EMAIL = '208'; const ERROR_PROVIDER_CREDENTIAL_ERROR = '209'; + const ERROR_WEBHOOK_COULD_NOT_TRIGGER = '210'; /** @@ -332,6 +333,7 @@ public static function get_error_code_message( $error_code ) { self::ERROR_AUTORESPONDER_MISSING_EMAIL_FIELD => __( 'The email field is missing from the Form Block with Autoresponder activated.', 'otter-blocks' ), self::ERROR_AUTORESPONDER_COULD_NOT_SEND => __( 'The email from Autoresponder could not be sent.', 'otter-blocks' ), self::ERROR_FILE_MISSING_BINARY => __( 'The file data is missing.', 'otter-blocks' ), + self::ERROR_WEBHOOK_COULD_NOT_TRIGGER => __( 'The webhook could not be triggered.', 'otter-blocks' ), ); if ( ! isset( $error_messages[ $error_code ] ) ) { diff --git a/inc/integrations/class-form-settings-data.php b/inc/integrations/class-form-settings-data.php index 21c27cb1e..c8cee7e76 100644 --- a/inc/integrations/class-form-settings-data.php +++ b/inc/integrations/class-form-settings-data.php @@ -121,6 +121,13 @@ class Form_Settings_Data { */ private $submissions_save_location = ''; + /** + * The webhook ID. + * + * @var string + */ + private $webhook_id = ''; + /** * The default constructor. * @@ -241,6 +248,9 @@ public static function get_form_setting_from_wordpress_options( $form_option ) { $integration->set_submissions_save_location( 'database-email' ); } $integration->set_meta( $form ); + if ( isset( $form['webhookId'] ) ) { + $integration->set_webhook_id( $form['webhookId'] ); + } } } return $integration; @@ -644,6 +654,15 @@ public function get_autoresponder() { return $this->autoresponder; } + /** + * Get the webhook id. + * + * @return string + */ + public function get_webhook_id() { + return $this->webhook_id; + } + /** * Set the autoresponder. * @@ -675,4 +694,17 @@ public function set_submissions_save_location( $submissions_save_location ) { $this->submissions_save_location = $submissions_save_location; return $this; } + + /** + * Set the webhook ID. + * + * @param string $webhook_id The webhook ID. + * @return $this + */ + private function set_webhook_id( $webhook_id ) { + if ( ! empty( $webhook_id ) ) { + $this->webhook_id = $webhook_id; + } + return $this; + } } diff --git a/inc/plugins/class-options-settings.php b/inc/plugins/class-options-settings.php index 5cb795b00..4416b0eaf 100644 --- a/inc/plugins/class-options-settings.php +++ b/inc/plugins/class-options-settings.php @@ -394,6 +394,9 @@ function ( $item ) { 'submissionsSaveLocation' => array( 'type' => 'string', ), + 'webhookId' => array( + 'type' => 'string', + ), ), ), ), @@ -511,6 +514,78 @@ function ( $item ) { ), ) ); + + register_setting( + 'themeisle_blocks_settings', + 'themeisle_webhooks_options', + array( + 'type' => 'array', + 'description' => __( 'Otter Registered Webhooks.', 'otter-blocks' ), + 'sanitize_callback' => function ( $array ) { + return array_map( + function ( $item ) { + if ( isset( $item['id'] ) ) { + $item['id'] = sanitize_text_field( $item['id'] ); + } + if ( isset( $item['url'] ) ) { + $item['url'] = esc_url_raw( $item['url'] ); + } + if ( isset( $item['name'] ) ) { + $item['name'] = sanitize_text_field( $item['name'] ); + } + if ( isset( $item['method'] ) ) { + $item['method'] = sanitize_text_field( $item['method'] ); + } + if ( isset( $item['headers'] ) && is_array( $item['headers'] ) && count( array_keys( $item['headers'] ) ) > 0 ) { + foreach ( $item['headers'] as $key => $value ) { + $item['headers'][ $key ] = sanitize_text_field( $value ); + } + } else { + $item['headers'] = array(); + } + return $item; + }, + $array + ); + }, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + ), + 'headers' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'type' => 'string', + ), + 'value' => array( + 'type' => 'string', + ), + ), + ), + ), + 'name' => array( + 'type' => 'string', + ), + 'method' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ) + ); } /** diff --git a/plugins/otter-pro/inc/plugins/class-form-pro-features.php b/plugins/otter-pro/inc/plugins/class-form-pro-features.php index 8b4394b4d..976e1dabc 100644 --- a/plugins/otter-pro/inc/plugins/class-form-pro-features.php +++ b/plugins/otter-pro/inc/plugins/class-form-pro-features.php @@ -34,6 +34,7 @@ public function init() { add_filter( 'otter_form_data_preparation', array( $this, 'load_files_to_media_library' ) ); add_action( 'otter_form_after_submit', array( $this, 'clean_files_from_uploads' ) ); add_action( 'otter_form_after_submit', array( $this, 'send_autoresponder' ), 99 ); + add_action( 'otter_form_after_submit', array( $this, 'trigger_webhook' ) ); } } @@ -347,6 +348,93 @@ public function send_autoresponder( $form_data ) { } } + /** + * Send autoresponder email to the subscriber. + * + * @param Form_Data_Request|null $form_data The files to load. + * @since 2.4 + */ + public function trigger_webhook( $form_data ) { + + if ( ! isset( $form_data ) ) { + return $form_data; + } + + if ( + ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || + ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || + $form_data->has_error() || + empty( $form_data->get_form_options()->get_webhook_id() ) + ) { + return $form_data; + } + + try { + $form_webhook_id = $form_data->get_form_options()->get_webhook_id(); + + $webhooks = get_option( 'themeisle_webhooks_options', array() ); + + $webhook = null; + + foreach ( $webhooks as $hook ) { + if ( $hook['id'] === $form_webhook_id ) { + $webhook = $hook; + break; + } + } + + if ( ! empty( $webhook ) ) { + $method = $webhook['method']; + $url = $webhook['url']; + $headers_pairs = $webhook['headers']; + $headers = array(); + + foreach ( $headers_pairs as $pair ) { + if ( empty( $pair['key'] ) || empty( $pair['value'] ) ) { + continue; + } + $headers[ $pair['key'] ] = $pair['value']; + } + + $payload = array(); + $inputs = $form_data->get_form_inputs(); + + foreach ( $inputs as $input ) { + if ( isset( $input['id'] ) && isset( $input['value'] ) ) { + $input['id'] = str_replace( 'wp-block-themeisle-blocks-form-', '', $input['id'] ); + + $payload[ $input['id'] ] = $input['value']; + } + } + + $payload = wp_json_encode( $payload ); + + $response = wp_remote_request( + $url, + array( + 'method' => $method, + 'headers' => $headers, + 'body' => $payload, + ) + ); + + if ( is_wp_error( $response ) ) { + $form_data->add_warning( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_WEBHOOK_COULD_NOT_TRIGGER, $response->get_error_message() ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + // TODO: use logger. + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + error_log( __( '[Otter Webhook]', 'otter-blocks' ) . $response->get_error_message() ); + } + } + } + } catch ( \Exception $e ) { + $form_data->add_warning( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_RUNTIME_ERROR, $e->getMessage() ); + } finally { + return $form_data; + } + } + /** * Replace magic tags with the values from the form inputs. * diff --git a/src/blocks/blocks/form/edit.js b/src/blocks/blocks/form/edit.js index 3a9029201..67e156d77 100644 --- a/src/blocks/blocks/form/edit.js +++ b/src/blocks/blocks/form/edit.js @@ -69,7 +69,8 @@ const formOptionsMap = { cc: 'cc', bcc: 'bcc', autoresponder: 'autoresponder', - submissionsSaveLocation: 'submissionsSaveLocation' + submissionsSaveLocation: 'submissionsSaveLocation', + webhookId: 'webhookId' }; /** @@ -303,7 +304,8 @@ const Edit = ({ hasCaptcha: wpOptions?.hasCaptcha, autoresponder: wpOptions?.autoresponder, autoresponderSubject: wpOptions?.autoresponderSubject, - submissionsSaveLocation: wpOptions?.submissionsSaveLocation + submissionsSaveLocation: wpOptions?.submissionsSaveLocation, + webhookId: wpOptions?.webhookId }); }; diff --git a/src/blocks/blocks/form/inspector.js b/src/blocks/blocks/form/inspector.js index 68e91900a..a0437ebab 100644 --- a/src/blocks/blocks/form/inspector.js +++ b/src/blocks/blocks/form/inspector.js @@ -294,6 +294,44 @@ const FormOptions = ({ formOptions, setFormOption, attributes, setAttributes }) + undefined !== formOptions.webhookId } + label={ __( 'Webhook', 'otter-blocks' ) } + onDeselect={ () => setFormOption({ webhookId: undefined }) } + isShownByDefault={ true } + > + + < br /> + + +
+ { __( 'Unlock this with Otter Pro.', 'otter-blocks' ) } } + variant="upsell" + /> +

{ __( 'Trigger webhooks on Form submit action.', 'otter-blocks' ) }

+
+
) } diff --git a/src/blocks/blocks/form/type.d.ts b/src/blocks/blocks/form/type.d.ts index b8e4534a5..1a178e459 100644 --- a/src/blocks/blocks/form/type.d.ts +++ b/src/blocks/blocks/form/type.d.ts @@ -63,7 +63,8 @@ export type FormOptions = { subject?: string body?: string } - submissionSaveLocation?: string + submissionsSaveLocation?: string + webhookId?: string } export type FormAttrs = Partial diff --git a/src/blocks/editor.scss b/src/blocks/editor.scss index cf0af68f8..3bb27ea0b 100644 --- a/src/blocks/editor.scss +++ b/src/blocks/editor.scss @@ -145,3 +145,38 @@ svg.o-block-icon { } } } + +.o-webhook-headers { + display: flex; + flex-direction: column; + gap: 8px; + + .o-webhook-header { + display: grid; + grid-template-columns: 1fr 1fr 35px; + gap: 10px; + align-items: center; + + .components-base-control__field { + margin: 0; + } + } + + button { + display: flex; + justify-content: center; + } +} + +.o-webhook-actions { + display: flex; + flex-direction: row; + justify-content: space-between; + + .o-webhook-actions__left { + display: flex; + flex-direction: row; + gap: 10px; + } +} + diff --git a/src/blocks/helpers/use-settings.js b/src/blocks/helpers/use-settings.js index fe0e1aca8..5fec66137 100644 --- a/src/blocks/helpers/use-settings.js +++ b/src/blocks/helpers/use-settings.js @@ -105,8 +105,8 @@ const useSettings = () => { } ); } - onSuccess?.(); getSettings(); + onSuccess?.( response ); }); save.error( ( response ) => { diff --git a/src/pro/components/webhook-editor/index.tsx b/src/pro/components/webhook-editor/index.tsx new file mode 100644 index 000000000..9936540de --- /dev/null +++ b/src/pro/components/webhook-editor/index.tsx @@ -0,0 +1,362 @@ +import { Fragment } from 'react'; +import { + BaseControl, + Button, + Icon, + Modal, + Notice, + SelectControl, + Spinner, + TabPanel, + TextControl +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; +import { arrowRight, closeSmall, trash } from '@wordpress/icons'; +import { v4 as uuidv4 } from 'uuid'; +import useSettings from '../../../blocks/helpers/use-settings'; + + +type WebhookEditorProps = { + webhookId: string, + onChange: ( webhookId: string ) => void, +} + +type Webhook = { + id: string, + name: string, + url: string, + method: string, + headers: {key?: string, value?: string}[], +} + +const WebhookEditor = ( props: WebhookEditorProps ) => { + + const [ isOpen, setOpen ] = useState( false ); + const [ error, setError ] = useState( '' ); + + const [ id, setId ] = useState( '' ); + + const [ url, setUrl ] = useState( '' ); + const [ name, setName ] = useState( '' ); + const [ method, setMethod ] = useState( 'GET' ); + const [ headers, setHeaders ] = useState<{key?: string, value?: string}[]>([]); + + const [ getOption, setOption, status ] = useSettings(); + + const fetchWebhook = () => { + const hooksOptions = getOption?.( 'themeisle_webhooks_options' ); + console.log( hooksOptions ); + + if ( hooksOptions ) { + setWebhooks( hooksOptions ); + } + }; + + const [ webhooks, setWebhooks ] = useState([]); + + + const [ initWebhooks, setInitWebhooks ] = useState( true ); + useEffect( () => { + if ( 'loaded' === status && initWebhooks ) { + fetchWebhook(); + setInitWebhooks( false ); + } + }, [ status, initWebhooks ]); + + const checkWebhook = ( webhook: Webhook ) => { + if ( ! webhook.name ) { + return __( 'Please enter a webhook name.', 'otter-blocks' ); + } + + if ( ! webhook.url ) { + return __( 'Please enter a webhook URL.', 'otter-blocks' ); + } + + if ( 0 < webhook.headers.length ) { + for ( const header of webhook.headers ) { + if ( ! header.key || ! header.value ) { + return __( 'Please enter a key and value for all headers.', 'otter-blocks' ); + } + } + } + + return true; + }; + + const saveWebhooks = ( webhooksToSave: Webhook[]) => { + for ( const webhook of webhooksToSave ) { + const check = checkWebhook( webhook ); + if ( true !== check ) { + const msg = __( 'There was an error saving the webhook: ', 'otter-blocks' ) + webhook?.name + '\n'; + setError( msg + check ); + return; + } + } + + // Save to wp options + setOption?.( 'themeisle_webhooks_options', [ ...webhooksToSave ], __( 'Webhooks saved!', 'otter-blocks' ), 'webhook', ( response ) => { + setWebhooks( response?.['themeisle_webhooks_options'] ?? []); + }); + }; + + return ( + + { isOpen && ( + setOpen( false )} + shouldCloseOnClickOutside={ false } + > + { + id ? ( + + + + + +
+ { + headers?.map( ( header, index ) => { + return ( +
+ { + const newHeaders = [ ...headers ]; + newHeaders[ index ] = { + ...newHeaders[ index ], + key: value + }; + setHeaders( newHeaders ); + }} + /> + { + const newHeaders = [ ...headers ]; + newHeaders[ index ] = { + ...newHeaders[ index ], + value + }; + setHeaders( newHeaders ); + }} + /> +
+ ); + }) + } + +
+
+
+
+ + +
+ + +
+
+ ) : ( + +
+ { + webhooks?.map( ( webhook ) => { + return ( +
+ +
+ { webhook.name } +
+
+ ); + }) + } + +
+ +
+ +
+
+ ) + } + + { + error && ( + { + setError( '' ); + }}> + { error } + + ) + } +
+ ) } + + { + 'loading' === status && ( + +
+ + + { __( 'Loading Webhooks', 'otter-blocks' ) } + +
+ ) + } + + { + return { + value: webhook.id, + label: webhook.name + }; + }) ?? [] + ) + ] + + } + onChange={ props.onChange } + /> + < br /> + +
+ ); +}; + +export default WebhookEditor; diff --git a/src/pro/plugins/form/index.js b/src/pro/plugins/form/index.js index ec79243a0..54b0ae885 100644 --- a/src/pro/plugins/form/index.js +++ b/src/pro/plugins/form/index.js @@ -21,6 +21,7 @@ import { Notice as OtterNotice } from '../../../blocks/components'; import { RichTextEditor } from '../../../blocks/components'; import { FieldInputWidth, HideFieldLabelToggle } from '../../../blocks/blocks/form/common'; import { setSavedState } from '../../../blocks/helpers/helper-functions'; +import WebhookEditor from '../../components/webhook-editor'; // +-------------- Autoresponder --------------+ @@ -63,7 +64,15 @@ const helpMessages = { 'database-email': __( 'Save the submissions to the database and notify also via email.', 'otter-blocks' ) }; - +/** + * Form Options + * + * @param {React.ReactNode} Options The children of the FormOptions component. + * @param {import('../../../blocks/blocks/form/type').FormOptions} formOptions The form options. + * @param { (options: import('../../../blocks/blocks/form/type').FormOptions) => void } setFormOption The function to set the form options. + * @param {any} config The form config. + * @returns {JSX.Element} + */ const FormOptions = ( Options, formOptions, setFormOption, config ) => { return ( @@ -167,6 +176,37 @@ const FormOptions = ( Options, formOptions, setFormOption, config ) => { )} + formOptions.webhookId } + label={__( 'Webhook', 'otter-blocks' )} + onDeselect={() => setFormOption({ webhookId: undefined })} + > + {Boolean( window.otterPro.isActive ) ? ( + <> + { + setFormOption({ + webhookId: webhookId + }); + }} + /> + + ) : ( +
+ +
+ )} +
); }; From 48a2bab7a04b04bc51a2f8835c6e66805a08d557 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Thu, 29 Jun 2023 18:00:51 +0300 Subject: [PATCH 017/107] feat: add mapped name to form field & webhook improvements --- inc/render/class-form-multiple-choice.php | 12 +- .../inc/plugins/class-form-pro-features.php | 48 ++++- .../inc/render/class-form-file-block.php | 9 +- src/blocks/blocks/form/file/inspector.js | 87 +++++---- src/blocks/blocks/form/input/inspector.js | 141 ++++++++------ .../blocks/form/multiple-choice/inspector.js | 183 ++++++++++-------- src/blocks/blocks/form/textarea/inspector.js | 110 ++++++----- src/blocks/frontend/form/index.js | 12 +- src/pro/components/webhook-editor/index.tsx | 10 +- src/pro/plugins/form/index.js | 8 + 10 files changed, 372 insertions(+), 248 deletions(-) diff --git a/inc/render/class-form-multiple-choice.php b/inc/render/class-form-multiple-choice.php index 70a96299c..46809f755 100644 --- a/inc/render/class-form-multiple-choice.php +++ b/inc/render/class-form-multiple-choice.php @@ -32,11 +32,12 @@ public function render( $attributes ) { $options_array = explode( "\n", $options ); $is_required = isset( $attributes['isRequired'] ) ? boolval( $attributes['isRequired'] ) : false; $has_multiple_selection = isset( $attributes['multipleSelection'] ) ? boolval( $attributes['multipleSelection'] ) : false; + $mapped_name = isset( $attributes['mappedName'] ) ? $attributes['mappedName'] : $id; $output = '
'; if ( 'select' === $field_type ) { - $output .= $this->render_select_field( $label, $options_array, $id, $has_multiple_selection, $is_required ); + $output .= $this->render_select_field( $label, $options_array, $id, $mapped_name, $has_multiple_selection, $is_required ); } else { $output .= ''; @@ -50,7 +51,7 @@ public function render( $attributes ) { $field_value = implode( '_', explode( ' ', sanitize_title( $field_label ) ) ); $field_id = 'field-' . $field_value; - $output .= $this->render_field( $field_type, $field_label, $field_value, $id, $field_id, $is_required ); + $output .= $this->render_field( $field_type, $field_label, $field_value, $mapped_name, $field_id, $is_required ); } $output .= '
'; @@ -90,13 +91,14 @@ public function render_field( $type, $label, $value, $name, $id, $is_required = * @param string $label The label of the field. * @param array $options_array The options of the field. * @param string $id The id of the field. + * @param string $name The name of the field. * @param bool $is_multiple The multiple status of the field. * @param bool $is_required The required status of the field. * @return string */ - public function render_select_field( $label, $options_array, $id, $is_multiple, $is_required ) { + public function render_select_field( $label, $options_array, $id, $name, $is_multiple, $is_required ) { $output = ''; - $output .= ''; foreach ( $options_array as $field_label ) { @@ -114,7 +116,7 @@ public function render_select_field( $label, $options_array, $id, $is_multiple, /** * Render the required sign. - * + * * @param bool $is_required The required status of the field. * @return string */ diff --git a/plugins/otter-pro/inc/plugins/class-form-pro-features.php b/plugins/otter-pro/inc/plugins/class-form-pro-features.php index 976e1dabc..aa9a91eb3 100644 --- a/plugins/otter-pro/inc/plugins/class-form-pro-features.php +++ b/plugins/otter-pro/inc/plugins/class-form-pro-features.php @@ -397,13 +397,55 @@ public function trigger_webhook( $form_data ) { } $payload = array(); - $inputs = $form_data->get_form_inputs(); + + $inputs = $form_data->get_form_inputs(); + $uploaded_files = $form_data->get_uploaded_files_path(); + $media_files = $form_data->get_files_loaded_to_media_library(); foreach ( $inputs as $input ) { if ( isset( $input['id'] ) && isset( $input['value'] ) ) { - $input['id'] = str_replace( 'wp-block-themeisle-blocks-form-', '', $input['id'] ); + $key = str_replace( 'wp-block-themeisle-blocks-form-', '', $input['id'] ); + $value = $input['value']; + + if ( ! empty( $input['metadata']['mappedName'] ) ) { + $key = $input['metadata']['mappedName']; + } + + $is_file_field = ! empty( $input['type'] ) && 'file' === $input['type']; + + if ( $is_file_field && ! empty( $input['metadata']['data'] ) ) { + $file_data_key = $input['metadata']['data']; + + if ( ! empty( $uploaded_files[ $file_data_key ] ) ) { + $value = $uploaded_files[ $file_data_key ]['file_path']; - $payload[ $input['id'] ] = $input['value']; + /** + * If the file was uploaded to the media library, we use the URL instead of the path. + */ + if ( ! empty( $uploaded_files[ $file_data_key ]['file_url'] ) ) { + $value = $uploaded_files[ $file_data_key ]['file_url']; + } + } + } + + + if ( array_key_exists( $key, $payload ) ) { + if ( is_array( $payload[ $key ] ) ) { + $payload[ $key ][] = $value; + } else { + /** + * Overwrite the value if it's not an array. + */ + $payload[ $key ] = $value; + } + } elseif ( $is_file_field ) { + /** + * If the field is a file field, we need to make sure the value is an array. + */ + $payload[ $key ] = array( $value ); + } else { + $payload[ $key ] = $value; + } } } diff --git a/plugins/otter-pro/inc/render/class-form-file-block.php b/plugins/otter-pro/inc/render/class-form-file-block.php index 6f7416aea..446a5ffb5 100644 --- a/plugins/otter-pro/inc/render/class-form-file-block.php +++ b/plugins/otter-pro/inc/render/class-form-file-block.php @@ -37,12 +37,13 @@ public function render( $attributes ) { $has_multiple_files = isset( $attributes['multipleFiles'] ) && boolval( $attributes['multipleFiles'] ) && ( ! isset( $attributes['maxFilesNumber'] ) || intval( $attributes['maxFilesNumber'] ) > 1 ); $allowed_files = isset( $attributes['allowedFileTypes'] ) ? implode( ',', $attributes['allowedFileTypes'] ) : ''; - $output = '
'; + $output = '
'; + $mapped_name = isset( $attributes['mappedName'] ) ? $attributes['mappedName'] : 'field-' . $id; - $output .= ''; + $output .= ''; - $output .= ' { @@ -68,6 +68,14 @@ const ProPreview = ({ attributes }) => { onChange={ () => {} } /> + {} } + placeholder={ __( 'photos', 'otter-blocks' ) } + /> + - - - - { - if ( 'file' !== type ) { - switchFormFieldTo( type, clientId, attributes ); + + + { + if ( 'file' !== type ) { + switchFormFieldTo( type, clientId, attributes ); + } + }} + /> + + + + + + {}, + label: __( 'Label Color', 'otter-blocks' ) } - }} + ] } /> - - - - - - {}, - label: __( 'Label Color', 'otter-blocks' ) - } - ] } + + setAttributes({ id: value }) } /> - + + ); }; diff --git a/src/blocks/blocks/form/input/inspector.js b/src/blocks/blocks/form/input/inspector.js index bfd1a4dc7..b3aa42a47 100644 --- a/src/blocks/blocks/form/input/inspector.js +++ b/src/blocks/blocks/form/input/inspector.js @@ -16,8 +16,9 @@ import { ToggleControl } from '@wordpress/components'; import { FieldInputWidth, fieldTypesOptions, HideFieldLabelToggle, switchFormFieldTo } from '../common'; -import { useContext } from '@wordpress/element'; +import { Fragment, useContext } from '@wordpress/element'; import { FormContext } from '../edit'; +import { HTMLAnchorControl } from '../../../components'; /** * @@ -35,78 +36,92 @@ const Inspector = ({ } = useContext( FormContext ); return ( - - - - - { - if ( 'textarea' === type || 'radio' === type || 'checkbox' === type || 'select' === type || 'file' === type ) { - switchFormFieldTo( type, clientId, attributes ); - return; - } - - setAttributes({ type }); - }} - /> + - setAttributes({ label }) } - /> + { + if ( 'textarea' === type || 'radio' === type || 'checkbox' === type || 'select' === type || 'file' === type ) { + switchFormFieldTo( type, clientId, attributes ); + return; + } - + setAttributes({ type }); + }} + /> - + setAttributes({ label }) } + /> - { - ( 'date' !== attributes.type || undefined === attributes.type ) && ( - setAttributes({ placeholder }) } - /> - ) - } + - setAttributes({ helpText }) } - /> - - setAttributes({ isRequired }) } - /> - + - setAttributes({ labelColor }), - label: __( 'Label Color', 'otter-blocks' ) + ( 'date' !== attributes.type || undefined === attributes.type ) && ( + setAttributes({ placeholder }) } + /> + ) } - ] } + + setAttributes({ helpText }) } + /> + + setAttributes({ isRequired }) } + /> + + setAttributes({ mappedName }) } + placeholder={ __( 'first_name', 'otter-blocks' ) } + /> + + + setAttributes({ labelColor }), + label: __( 'Label Color', 'otter-blocks' ) + } + ] } + /> + + setAttributes({ id: value }) } /> - + ); }; diff --git a/src/blocks/blocks/form/multiple-choice/inspector.js b/src/blocks/blocks/form/multiple-choice/inspector.js index b7d130482..7118c2eac 100644 --- a/src/blocks/blocks/form/multiple-choice/inspector.js +++ b/src/blocks/blocks/form/multiple-choice/inspector.js @@ -19,8 +19,9 @@ import { import { getActiveStyle, changeActiveStyle } from '../../../helpers/helper-functions.js'; import { fieldTypesOptions, HideFieldLabelToggle, switchFormFieldTo } from '../common'; -import { useContext } from '@wordpress/element'; +import { Fragment, useContext } from '@wordpress/element'; import { FormContext } from '../edit.js'; +import { HTMLAnchorControl } from '../../../components'; const styles = [ { @@ -45,95 +46,109 @@ const Inspector = ({ } = useContext( FormContext ); return ( - - - - - { - if ( 'radio' === type || 'checkbox' === type || 'select' === type ) { - setAttributes({ type }); - return; - } - switchFormFieldTo( type, clientId, attributes ); - }} - /> - - setAttributes({ label }) } - /> + + + { + if ( 'radio' === type || 'checkbox' === type || 'select' === type ) { + setAttributes({ type }); + return; + } + switchFormFieldTo( type, clientId, attributes ); + }} + /> + + setAttributes({ label }) } + /> + + + + setAttributes({ options }) } + /> + + setAttributes({ helpText }) } + /> - - - setAttributes({ options }) } - /> - - setAttributes({ helpText }) } - /> - - { - 'select' !== attributes?.type && ( - { - const classes = changeActiveStyle( attributes.className, styles, value ? 'inline-list' : undefined ); - setAttributes({ className: classes }); - } } - /> - ) - } - - { - 'select' === attributes?.type && ( - setAttributes({ multipleSelection }) } - /> - ) - } - - setAttributes({ isRequired }) } - /> - + { + 'select' !== attributes?.type && ( + { + const classes = changeActiveStyle( attributes.className, styles, value ? 'inline-list' : undefined ); + setAttributes({ className: classes }); + } } + /> + ) + } - setAttributes({ labelColor }), - label: __( 'Label Color', 'otter-blocks' ) + 'select' === attributes?.type && ( + setAttributes({ multipleSelection }) } + /> + ) } - ] } + + setAttributes({ isRequired }) } + /> + + setAttributes({ mappedName }) } + placeholder={ __( 'car_type', 'otter-blocks' ) } + /> + + + {}, + label: __( 'Label Color', 'otter-blocks' ) + } + ] } + /> + + setAttributes({ id: value }) } /> - + ); }; diff --git a/src/blocks/blocks/form/textarea/inspector.js b/src/blocks/blocks/form/textarea/inspector.js index a324c7da9..a14e6b7ca 100644 --- a/src/blocks/blocks/form/textarea/inspector.js +++ b/src/blocks/blocks/form/textarea/inspector.js @@ -14,7 +14,8 @@ import { } from '@wordpress/components'; import { FieldInputWidth, fieldTypesOptions, HideFieldLabelToggle, switchFormFieldTo } from '../common'; import { FormContext } from '../edit'; -import { useContext } from '@wordpress/element'; +import { Fragment, useContext } from '@wordpress/element'; +import { HTMLAnchorControl } from '../../../components'; const Inspector = ({ attributes, @@ -27,60 +28,75 @@ const Inspector = ({ } = useContext( FormContext ); return ( - - - + - { - if ( 'textarea' === type ) { - return; - } - switchFormFieldTo( type, clientId, attributes ); - }} - /> + { + if ( 'textarea' === type ) { + return; + } + switchFormFieldTo( type, clientId, attributes ); + }} + /> - setAttributes({ label }) } - /> + setAttributes({ label }) } + /> - + - + - setAttributes({ placeholder }) } - /> + setAttributes({ placeholder }) } + /> - setAttributes({ helpText }) } - /> + setAttributes({ helpText }) } + /> + + setAttributes({ isRequired }) } + /> + + setAttributes({ mappedName }) } + placeholder={ __( 'message', 'otter-blocks' ) } + /> + + + setAttributes({ id: value }) } + /> + - setAttributes({ isRequired }) } - /> - - ); }; diff --git a/src/blocks/frontend/form/index.js b/src/blocks/frontend/form/index.js index 3c2ec262a..7a8cf9b51 100644 --- a/src/blocks/frontend/form/index.js +++ b/src/blocks/frontend/form/index.js @@ -55,20 +55,25 @@ const extractFormFields = async( form ) => { let value = undefined; let fieldType = undefined; + let mappedName = undefined; const { id } = input; const valueElem = input.querySelector( '.otter-form-input:not([type="checkbox"], [type="radio"], [type="file"]), .otter-form-textarea-input' ); if ( null !== valueElem ) { value = valueElem?.value; fieldType = valueElem?.type; + mappedName = valueElem?.name; } else { const select = input.querySelector( 'select' ); + mappedName = select?.name; /** @type{HTMLInputElement} */ const fileInput = input.querySelector( 'input[type="file"]' ); + if ( fileInput ) { const files = fileInput?.files; + const mappedName = fileInput?.name; for ( let i = 0; i < files.length; i++ ) { formFieldsData.push({ @@ -82,7 +87,8 @@ const extractFormFields = async( form ) => { size: files[i].size, file: files[i], fieldOptionName: fileInput?.dataset?.fieldOptionName, - position: index + 1 + position: index + 1, + mappedName: mappedName } }); } @@ -92,6 +98,7 @@ const extractFormFields = async( form ) => { } else { const labels = input.querySelectorAll( '.o-form-multiple-choice-field > label' ); const valuesElem = input.querySelectorAll( '.o-form-multiple-choice-field > input' ); + mappedName = valuesElem[0]?.name; value = [ ...labels ].filter( ( label, index ) => valuesElem[index]?.checked ).map( label => label.innerHTML ).join( ', ' ); fieldType = 'multiple-choice'; } @@ -105,7 +112,8 @@ const extractFormFields = async( form ) => { id: id, metadata: { version: METADATA_VERSION, - position: index + 1 + position: index + 1, + mappedName: mappedName } }); } diff --git a/src/pro/components/webhook-editor/index.tsx b/src/pro/components/webhook-editor/index.tsx index 9936540de..4597251ae 100644 --- a/src/pro/components/webhook-editor/index.tsx +++ b/src/pro/components/webhook-editor/index.tsx @@ -116,12 +116,14 @@ const WebhookEditor = ( props: WebhookEditorProps ) => { help={ __( 'Enter the URL to send the data to.', 'otter-blocks' )} value={name} onChange={setName} + placeholder={ __( 'My Webhook', 'otter-blocks' )} /> {
-
+
) : ( - -
+
+
{ webhooks?.map( ( webhook ) => { return (
-
+
{ webhook.name }
-
+
- +
) } @@ -354,6 +354,7 @@ const WebhookEditor = ( props: WebhookEditorProps ) => { From 3dac914fec626d3496500418c8ae6cdd47a01444 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Fri, 30 Jun 2023 10:36:15 +0300 Subject: [PATCH 019/107] chore: better seperation between free and pro --- inc/plugins/class-dashboard.php | 94 +++++++++++++++++---------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index 20fd950ed..95b2c525b 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -291,70 +291,75 @@ public function form_submissions_widget() { */ public function form_submissions_widget_content() { - $is_active = Pro::is_pro_active(); + $is_active = Pro::is_pro_active(); + $entries = array(); + $count = 0; + $posts_filter = 'all'; - $posts_filter = isset( $_GET['otter_nonce'] ) && wp_verify_nonce( sanitize_key( $_GET['otter_nonce'] ), 'otter_widget_nonce' ) && isset( $_GET['otter_form_widget_filter'] ) ? sanitize_key( $_GET['otter_form_widget_filter'] ) : 'all'; + if ( $is_active ) { + $posts_filter = isset( $_GET['otter_nonce'] ) && wp_verify_nonce( sanitize_key( $_GET['otter_nonce'] ), 'otter_widget_nonce' ) && isset( $_GET['otter_form_widget_filter'] ) ? sanitize_key( $_GET['otter_form_widget_filter'] ) : 'all'; - $query_args = array( - 'post_type' => 'otter_form_record', - 'posts_per_page' => 5, - ); + $query_args = array( + 'post_type' => 'otter_form_record', + 'posts_per_page' => 5, + ); - if ( 'all' !== $posts_filter ) { - $query_args['post_status'] = $posts_filter; - } + if ( 'all' !== $posts_filter ) { + $query_args['post_status'] = $posts_filter; + } - $query = new \WP_Query( $query_args ); - $entries = array(); + $query = new \WP_Query( $query_args ); - $records_count = wp_count_posts( 'otter_form_record' ); - $count = $records_count->read + $records_count->unread; + $records_count = wp_count_posts( 'otter_form_record' ); - if ( 'read' === $posts_filter ) { - $count = $records_count->read; - } elseif ( 'unread' === $posts_filter ) { - $count = $records_count->unread; - } + $count = $records_count->read + $records_count->unread; - if ( $query->have_posts() ) { + if ( 'read' === $posts_filter ) { + $count = $records_count->read; + } elseif ( 'unread' === $posts_filter ) { + $count = $records_count->unread; + } - while ( $query->have_posts() ) { - $query->the_post(); + if ( $query->have_posts() ) { - $meta = get_post_meta( get_the_ID(), 'otter_form_record_meta', true ); + while ( $query->have_posts() ) { + $query->the_post(); - $title = null; - $date = null; + $meta = get_post_meta( get_the_ID(), 'otter_form_record_meta', true ); - if ( isset( $meta['post_id']['value'] ) ) { - $date = get_the_date( 'F j, H:i', $meta['post_id']['value'] ); - } + $title = null; + $date = null; + + if ( isset( $meta['post_id']['value'] ) ) { + $date = get_the_date( 'F j, H:i', $meta['post_id']['value'] ); + } - if ( isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ) { - // Find the first email field and use that as the title. - foreach ( $meta['inputs'] as $input ) { - if ( isset( $input['type'] ) && 'email' === $input['type'] && ! empty( $input['value'] ) ) { - $title = $input['value']; - break; + if ( isset( $meta['inputs'] ) && is_array( $meta['inputs'] ) ) { + // Find the first email field and use that as the title. + foreach ( $meta['inputs'] as $input ) { + if ( isset( $input['type'] ) && 'email' === $input['type'] && ! empty( $input['value'] ) ) { + $title = $input['value']; + break; + } } } - } - if ( ! $title ) { + if ( ! $title ) { - if ( isset( $meta['post_id']['value'] ) ) { - $title = __( 'Submission', 'otter-blocks' ) . ' #' . get_the_ID(); - } else { - $title = __( 'No title', 'otter-blocks' ); + if ( isset( $meta['post_id']['value'] ) ) { + $title = __( 'Submission', 'otter-blocks' ) . ' #' . get_the_ID(); + } else { + $title = __( 'No title', 'otter-blocks' ); + } } - } - $entries[] = array( - 'title' => $title, - 'date' => $date, - ); + $entries[] = array( + 'title' => $title, + 'date' => $date, + ); + } } } @@ -496,7 +501,6 @@ public function form_submissions_widget_content() { if (select && entriesContainer) { select.addEventListener('change', (e) => { const value = e.target.value; - console.log(value); // change the url param based on the value const url = new URL(window.location.href); From fa11e01d33acec7255d1b877536174ce9b14f31c Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Fri, 30 Jun 2023 10:40:12 +0300 Subject: [PATCH 020/107] chore: title centering --- inc/plugins/class-dashboard.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index 95b2c525b..d92288d6a 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -401,6 +401,8 @@ public function form_submissions_widget_content() { font-weight: 600; font-size: 16px; line-height: 150%; + display: flex; + justify-content: center; } .o-upsell-banner p { From 0f30d27627ca19ae44cef13da8891f057fd062e9 Mon Sep 17 00:00:00 2001 From: "Soare Robert Daniel (Mac 2023)" Date: Fri, 30 Jun 2023 12:35:42 +0300 Subject: [PATCH 021/107] chore: add block switching & styling --- .../render/class-form-hidden-block.pnp.php | 7 +-- src/blocks/blocks/form/editor.scss | 12 ++++- src/blocks/blocks/form/file/index.js | 10 ++++ .../blocks/form/hidden-field/block.json | 6 +++ src/blocks/blocks/form/hidden-field/index.js | 49 ++++++++++++++++++- .../blocks/form/hidden-field/types.d.ts | 2 + src/blocks/blocks/form/input/index.js | 10 ++++ .../blocks/form/multiple-choice/index.js | 10 ++++ src/blocks/blocks/form/textarea/index.js | 10 ++++ src/pro/blocks/file/index.js | 10 ++++ src/pro/blocks/form-hidden-field/block.json | 10 ++++ src/pro/blocks/form-hidden-field/edit.js | 12 ++--- src/pro/blocks/form-hidden-field/index.js | 49 ++++++++++++++++++- src/pro/blocks/form-hidden-field/inspector.js | 4 +- src/pro/blocks/form-hidden-field/types.d.ts | 2 + 15 files changed, 186 insertions(+), 17 deletions(-) diff --git a/plugins/otter-pro/inc/render/class-form-hidden-block.pnp.php b/plugins/otter-pro/inc/render/class-form-hidden-block.pnp.php index c4c32394d..ecb27849a 100644 --- a/plugins/otter-pro/inc/render/class-form-hidden-block.pnp.php +++ b/plugins/otter-pro/inc/render/class-form-hidden-block.pnp.php @@ -33,14 +33,15 @@ public function render( $attributes ) { $id = isset( $attributes['id'] ) ? $attributes['id'] : ''; $label = isset( $attributes['label'] ) ? $attributes['label'] : __( 'Hidden Field', 'otter-blocks' ); $param_name = isset( $attributes['paramName'] ) ? $attributes['paramName'] : ''; + $mapped_name = isset( $attributes['mappedName'] ) ? $attributes['mappedName'] : 'field-' . $id; $output = '