diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 64399dd5a..a35d6377a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -30,4 +30,5 @@ new QueryQA().select('blocks').run() - [ ] In case of deprecation, old blocks are safely migrated. - [ ] It is usable in Widgets and FSE. - [ ] Copy/Paste is working if the attributes are modified. +- [ ] PR is following [the best practices]() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e948936d5..2c9152b1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,14 @@ ## Setup +More details about the setup can be found in [docs/environment-installation.md](docs/environment-installation.md). + This projects requires you to have Node.js (with npm) and Composer. - You can run `npm ci` & `composer install` to install dependencies. - Once done, you can run `npm run build` to generate build files. - You can also use `npm run start` to generate dev build if you are working on the files. -The project also ships with a `docker-compose.yml` file, you can run `docker compose up-d` to bring the instance up. +The project also ships with a `docker-compose.yml` file, you can run `docker compose up -d` to bring the instance up. ## Project Structure diff --git a/assets/images/form-submissions-upsell.svg b/assets/images/form-submissions-upsell.svg new file mode 100644 index 000000000..a547f7393 --- /dev/null +++ b/assets/images/form-submissions-upsell.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blocks.json b/blocks.json index c9fa30204..e740e559d 100644 --- a/blocks.json +++ b/blocks.json @@ -93,6 +93,12 @@ "form-textarea": { "block": "blocks/blocks/form/textarea/block.json" }, + "form-multiple-choice": { + "block": "blocks/blocks/form/multiple-choice/block.json" + }, + "form-file": { + "block": "blocks/blocks/form/file/block.json" + }, "google-map": { "block": "blocks/blocks/google-map/block.json", "assets": { @@ -267,4 +273,4 @@ "isPro": true, "block": "pro/woocommerce/upsells/block.json" } -} \ No newline at end of file +} diff --git a/development.php b/development.php index 19f03f618..124573658 100644 --- a/development.php +++ b/development.php @@ -16,25 +16,25 @@ 'themeisle_sdk_products', function ( $products ) { $products[] = dirname( __FILE__ ) . '/plugins/otter-pro/otter-pro.php'; - + return $products; } ); - + add_filter( 'themesle_sdk_namespace_' . md5( dirname( __FILE__ ) . '/plugins/otter-pro/otter-pro.php' ), function () { return 'otter'; } ); - + add_filter( 'otter_pro_lc_no_valid_string', function ( $message ) { return str_replace( '', '', $message ); } ); - + add_filter( 'otter_pro_hide_license_field', '__return_true' ); \ThemeIsle\OtterPro\Main::instance(); diff --git a/docs/adding-patterns.md b/docs/adding-patterns.md new file mode 100644 index 000000000..286dc184b --- /dev/null +++ b/docs/adding-patterns.md @@ -0,0 +1,19 @@ +# Patterns + +Patterns are big part of Gutenberg ecosystem. In the old days, plugin developer made their own mechanism to add them, but things are now more organized. + +Adding a new pattern for Otter is straight forward. + +- All the patterns are located in `./src/patterns` folder. +- Every pattern is just an array with keys described by the [Gutenberg documentation](https://developer.wordpress.org/block-editor/developers/block-api/block-patterns/). +- After creating a file for it, register it on `./inc/patterns.php` file in `$block_patterns` array. + +## Mentions + +- The pattern name should be unique. +- Do not use specific theme CSS vars as values for attributes. Like `"color": "var(--neve-custom-color)"`. This will make the pattern unusable on other themes. Patterns should be theme agnostic. Exceptions can be made, but only if it is intended. +- Pay attention to image links. The images should the accessible from internet. If use the image that are in a private network, they can not used by users. +- Always test the pattern. There is chance that you might have doing a small modification without thinking it will affect the code. +- Pay attention to the blocks that you use. You accidentally might use a block that is not available in Otter or Gutenberg. Of course, you can add any block, but if it is external, it must intended. + + diff --git a/docs/coding-best-practices.md b/docs/coding-best-practices.md new file mode 100644 index 000000000..f61f4e82b --- /dev/null +++ b/docs/coding-best-practices.md @@ -0,0 +1,104 @@ +# Coding Best Practices + +Coding practices meme from xkcd + +## Introduction + +Best practices are a set of guidelines that help you write code that is easy to read, understand, and maintain. They are a set of rules that you should follow when writing code. They are not rules that you must follow, but they are recommended. + +We follow the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/). For PHP, you can inspire from https://github.com/piotrplenik/clean-code-php + +But this is not enough... + +## Code for the Team + +> When you write code, you are not writing it for yourself. You are writing it for the team. You are writing it for the future you. You are writing it for the future team members. You are writing it for the future clients. -- Copilot, April 2023 + +The sign of code quality are: +- Easy to read and understand. +- Easy to maintain. +- Easy to extend. +- Easy to debug. + +To general, let's make an example for it: + +Suppose you have this task: `Given a list of numbers, sum only the even numbers.` + +A simple book-like solution would be: + +```javascript +const numbers = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]; + +let sum = 0; + +for ( let i = 0; i < numbers.length; i++ ) { + if ( numbers[ i ] % 2 === 0 ) { + sum += numbers[ i ]; + } +} + +console.log( sum ); +``` + +This is a simple solution. But it can be better. Let's refactor it: + +```javascript +const numbers = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]; + +const sum = numbers + .filter( ( number ) => number % 2 === 0 ) + .reduce( ( sum, number ) => sum + number, 0 ); + +console.log( sum ); +``` + +Why is this better? + +- It is easier to read and understand. When you have a classic `for` loop, you need to read the whole loop to understand what it does. With the `filter` and `reduce` functions, you can understand what it does by reading only the first line. `filter` and `reduce` are specialized loops, they have a specific purpose. +- Easy to maintain. It it easier to spot where to make a change. +- Easy to extend. If the numbers are send as `string` instead of `number`, you can easily change the code to convert them to `number` before filtering and reducing. Example: Add `.map( ( number ) => parseInt( number, 10 ) )` before filter. +- Easy to debug. The functionality is modular, so you can remove things one at the time and check them. One neat trick you can do is to create an inspection function like this: + +```javascript +const inspect = ( value ) => { + console.log( value ); + return value; +}; +``` + +Then, you can add it in the chain like this: + +```javascript +const sum = numbers + .filter( ( number ) => number % 2 === 0 ) + .map( inspect ) + .reduce( ( sum, number ) => sum + number, 0 ); +``` + +This will out the result at each step of the chain. This is very useful when you need to debug a chain of functions. + +The first version might me more faster, but if the code is running only at a press of a button or an event, it is not a big deal. + +## Elegant Code + +In era of generated code, we still need to have elegant code. At the end of the day, we are still writing code for humans (this might be obsolete in feature). We need to write code that is easy to read and understand. + +Since the beginning of coding, people made a lot of articles and principles about how to write elegant code. Here are some of them: + +- DRY (Don't Repeat Yourself) - [Wikipedia](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) +- KISS (Keep It Simple, Stupid) - [Wikipedia](https://en.wikipedia.org/wiki/KISS_principle) +- YAGNI (You Ain't Gonna Need It) - [Wikipedia](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it) +- SOLID - [Wikipedia](https://en.wikipedia.org/wiki/SOLID) +- Design Patterns - [Wikipedia](https://en.wikipedia.org/wiki/Software_design_pattern) + +## Performance + +If you come from a background where performance is a big deal, you might be tempted to write code that is optimized for performance. Example: game development, HPC, etc. + +In this project, we do not have cases when the code must do a lot of things at a high rate (like rendering a scene in a game). So, we can write code that is easy to read and understand, and we can optimize it later if needed. + +> Early optimization is the root of all evil. -- Donald Knuth + +The challenge in this project is to extend the code to support more features. The more we have, the harder it will be to maintain the code. Fancy tricks without a good reason are not good. + +A piece code that is performant, easy to read and understand, and easy to maintain is the best. [But sometime you can not have it all](https://www.youtube.com/watch?v=hFDcoX7s6rE). So, you need to choose what is more important for your case. \ No newline at end of file diff --git a/docs/creating-blocks.md b/docs/creating-blocks.md new file mode 100644 index 000000000..a87a25a7b --- /dev/null +++ b/docs/creating-blocks.md @@ -0,0 +1,77 @@ +# Creating a new block for Otter + +A list of steps to follow so that your block is added correctly. This offer a general guideline on what to do and check when creating a new block. Usually you will copy the settings from an existing block and change the values. + +## Registration + +Suppose you want to create a new block called `my-card`. + +So, you make a new folder in `./src/blocks/blocks/` called `my-card`. + +Every block needs a `block.json`. This file contains the block's metadata, such as its name, description, category, icon, and other attributes definition. +Then you create `index.js` and where you call the registration function `registerBlockType`. + +You need to import the `index.js` in the `./src/blocks/blocks/index.js` file (this file is one that make the initiation, you can check this on the `webpack.config.js` entry)(The block will not be registered if you don't do this.) + +In the root folder, at `./blocks.json`, add another entry with the name of your block and the location of the `block.json` file. (You can also have the option for pro block and their assets, add them based on your needs). + +Back to the `./src/blocks/blocks/my-card`. You can create the other files like: `edit.js`, `save.js`, `inspector.js`, `style.scss`, `editor.scss`. + +If your block has functionality for end user (aka frontend), you need to add the script in new folder at `./src/blocks/frontend/`. Then you need to add as an entry point in `./webpack.config.json` file. Then building the project, you will that the script is added independently in `./build/blocks/` (you need to run `npm run build` or `npm run start` if you do not see the folder `./build`). Then register that independent script in `./inc/class-registration.php` on `enqueue_dependencies` function (this function load the script for the end user if the block is present in page). (Tip: Add a `console.log('Loaded')` at the beginning of your script to check if it is loaded). + +## Adding content + +To add content you will usually have those files: +- `edit.js` - This file contains the code for the Editor. What user see in the editor. +- `save.js` - This file contains the code for the Frontend. What the end user see in the final page. +- `inspector.js` - This file contains the code for the Inspector. What user see in the right sidebar. This is imported in `edit.js` file. Make sure to consult [Gutenberg Component Story Book](https://wordpress.github.io/gutenberg/?path=%2Fstory%2Fdocs-introduction--page) to see what components are available. +- `style.scss` - The styling on what the end user see in the final page. +- `editor.scss` - The styling on see in the editor. You can inherit the content from `style.scss` file. Thus this file will contains only changes for the Editor quirks (extra padding on some elements, different nesting on core components, etc.). +- `controls.js` - This file will contains the controls component for the block (The bar that appears in the writing area when you select a block). This is imported in `edit.js` file. + +`save.js` can be omitted if the block is rendered dynamically on the frontend. This is usually when the block is very complex or its specification are not standard it will require changes in the future. You need to a create a PHP rendering class in `./inc/render/`, then register the class in `./inc/class-registration.php` on `register_blocks` function in `$dynamic_blocks` array. + +> Do not forget to run `composer dumpautoload -o` after you create a new PHP class. + +## Pro + +Pro feature need to be separated from the free version if it is possible. This is to make sure that the free version is not bloated with unnecessary code. The pro version will be loaded only if the user has the pro version of the plugin. + +Pro block are added in `./src/blocks/pro/`. The folder structure is the same as the free version. + +Some blocks are hybrid, free to use but with limited functionality (e.g.: Sticky, Popup). The pro features are added via JS WP hooks (in a style similar to the one from PHP). Learn more about them [here](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-hooks/). + +You need to be careful with using `useSelect` or `useDispatch` hooks inside a function that is a part of the filter. As mentioned in [Handbook](./handbook.md), use `console.count` to check how many times the function is called. Sometime we had unpleasant surprise when we found out that the function is called multiple times: [#799](https://github.com/Codeinwp/otter-blocks/issues/799). + +## Testing/Quality + +A good block has the following qualities: +- It works as expected. +- It does not break the editor. +- It does not create performance issues. +- Works well with other blocks if it is the case. + +To assert the quality of the block, have in sight those things: +- It works with the minimum supported version of WordPress. (Sometimes, the supported version will be raised just to enable some new approach for an easy implementation of a feature, make sure to talk with the manager about this to be sure, otherwise you will probably re-implement the block if you find it to late). +- It looks OK with other themes. We target Neve and WP default theme, but you should check with other popular themes since they may add some extra CSS property that might break the layout. +- It works on environments that it is supposed to work: Full Side Editing Editor, Site Editor, Widgets Editor, Custom Neve Layouts, etc. + +Users want simple things from the block: +- It should be easy to use. +- It should be easy to understand. +- It should to the job that is supposed to do (this will be very subjective if the indent of the block is not crystal clear). +- It should not break their work. (Suppose you are a writer, you work 3 hours on a post and you add a block that breaks the layout the content is not saved, you will be very angry -- your are suppose to ease/add value to their work.). + +## Future Proof Block + +A future proof block is a block that will not break if the user updates the plugin. This is important because we want to make sure that the user will not lose the content if he updates the plugin. The same goes in reverse, if the user rolls back to an older version of the plugin, the block should not break. + +The pain point of this is the definition of the attributes, if you have an attributes that is initially a string and later you need an array, you will need to migrate the data. But it will create a problem if the user rolls back to an older version of the plugin since the old version might delete the new format of the data. + +For this to happen, you need to discuss with the manager about the attributes and how they will be used. If you are not sure, you can always ask for help. Sometime not even the team or manager can not be sure about it. + +The following question can help you to decide if the block is future proof: If tomorrow I will add more on this attribute, that will be the least painful data structure to work with it? + +Example: Suppose you have a simple boolean attribute called `save`. If true, the data will be saved on the database. But you think that data can be saved on multiple location: on disk, on cloud, etc. So instead of boolean, you can make it to a string that can have the following values: `database`, `disk`, `cloud`. You can also think that the data can be saved on multiple location at the same time. So you can make it to an array of strings. But this all can be done with a special string like `database,disk,cloud` or `database-disk-cloud`. You can propose those to the manager and team to decide which one is the best. But make sure that the decision is also best for you since you will be the one that will implement it and fix it if something goes wrong (some folks call this _ownership_). + +In a nutshell, the block is nice to be (backward compatible)[https://en.wikipedia.org/wiki/Backward_compatibility] with as many version possible since its creation. \ No newline at end of file diff --git a/docs/dynamic-conditions-and-content.md b/docs/dynamic-conditions-and-content.md new file mode 100644 index 000000000..36f35a204 --- /dev/null +++ b/docs/dynamic-conditions-and-content.md @@ -0,0 +1,58 @@ +# Dynamic Block Conditions and Content + +One of the most powerful feature of Otter is the ability to dynamically change the content of page with blocks using conditions. This is a very powerful feature that allows you to create a single page that can be used for multiple purposes. + +E.g.: Showing a different content for logged in users and for guests. + +Dynamic content allow the user to easy change the content of the page without the need to update page for each case. + +E.g.: Switching images, switching links (imagine manually updating affiliate links for 100 posts). + +## Structure + +They are split into two parts: Free and Pro. + +The Pro part acts like an extension to the Free part. + +| Feature | Editor | Backend | +| :-- | :-- | :-- | +| Dynamic Block Conditions | `./src/blocks/plugins/conditions` | `./inc/plugins/class-block-conditions.php` | +| Dynamic Content | `./src/blocks/plugins/dynamic-content` | `./inc/plugins/class-dynamic-content.php` | + +Extensions (Pro) + +| Feature | Editor | Backend | +| :-- | :-- | :-- | +| Dynamic Block Conditions | `./src/blocks/plugins/conditions` | `./plugins/otter-pro/class-block-conditions.php` | +| Dynamic Content | `./src/blocks/pro/plugins/conditions` | `./plugins/otter-pro/class-dynamic-content.php` | + + +## How it works + +### Dynamic Block Conditions + +Block Condition can be added via `Block Tools` in Inspector. It allows you to add conditions for the block to be displayed. There many options available, like: author, post type, logged in status, user role, etc. And even fancy integration with Stripe and WooCommerce. + +Once you set a condition, they are saved in the block attributes and are used to check if the block should be displayed or not using PHP. + +When the blocks are rendered, we parse the condition from the attribute `'otterConditions'`, then we check if their rule is true. You can check `render_blocks` function in `./inc/plugins/class-block-conditions.php` to see the workflow. + +The concept and workflow is simplistic. The hard part of this is making the evaluation function for each condition. When making a condition you need to have in sight: +- performance. This function is called for each block on the page with the given condition, so it needs to be as fast as possible. +- stability. You need to handle the errors and do not let the page crash. + +To add a new condition outside Otter context (e.g.: making a custom plugin for extending functionality), you need to add a new function to filter `otter_blocks_evaluate_condition`. You can see an example in `./plugins/otter-pro/class-block-conditions.php`. + +### Dynamic Content + +Dynamic Content is a feature that allows you to change the content of the block using conditions. It works similar to Dynamic Block Conditions, but instead of hiding the block, it changes the content of the block. + +In a page, you can active this by inserting the trigger `%` and then select what content you want to add. + +The workflow is similar with Block Conditions, but instead of checking if the block has an established attribute, we look for a _magic tag_ like `add_item( + array( + 'properties' => array( + array( + 'property' => '--width', + 'value' => 'width', + 'unit' => 'px', + ), + ) + ) +); + +$css->add_item( + array( + 'selector' => ' .wp-block-button__link:not(:hover)', + 'properties' => array( + array( + 'property' => 'border-color', + 'value' => 'border', + 'hasSync' => 'gr-btn-border-color', + ), + array( + 'property' => 'border-width', + 'value' => 'borderSize', + 'format' => function( $value, $attrs ) { + return CSS_Utility::box_values( + $value, + array( + 'left' => '1px', + 'right' => '1px', + 'top' => '1px', + 'bottom' => '1px', + ) + ); + }, + 'condition' => function( $attrs ) { + return isset( $attrs['borderSize'] ) && is_array( $attrs['borderSize'] ); + }, + 'hasSync' => 'gr-btn-border-size', + ), + array( + 'property' => 'box-shadow', + 'pattern' => 'horizontal vertical blur spread color', + 'pattern_values' => array( + 'horizontal' => array( + 'value' => 'boxShadowHorizontal', + 'unit' => 'px', + 'default' => 0, + ), + 'vertical' => array( + 'value' => 'boxShadowVertical', + 'unit' => 'px', + 'default' => 0, + ), + 'blur' => array( + 'value' => 'boxShadowBlur', + 'unit' => 'px', + 'default' => 5, + ), + 'spread' => array( + 'value' => 'boxShadowSpread', + 'unit' => 'px', + 'default' => 1, + ), + 'color' => array( + 'value' => 'boxShadowColor', + 'default' => '#000', + 'format' => function( $value, $attrs ) { + $opacity = ( isset( $attrs['boxShadowColorOpacity'] ) ? $attrs['boxShadowColorOpacity'] : 50 ) / 100; + return Base_CSS::hex2rgba( $value, $opacity ); + }, + ), + ), + 'condition' => function( $attrs ) { + return isset( $attrs['boxShadow'] ) && true === $attrs['boxShadow']; + }, + 'hasSync' => 'gr-btn-shadow', + ), + ), + ) +); +``` + +Let's break it down: + +- `$css= new CSS_Utility( $block_metadata );` - we create a new instance of `CSS_Utility` with the block metadata. +- `$css->add_item` - add a new CSS declaration to the CSS file. A CSS declaration has a selector and some properties. The selector is used to identify the HTML element in the page. The properties are the CSS properties that we want to set. Those two are packed in an array as argument. *At the beginning of each declaration, the id of the block is appended, if you don't any selector, by default the CSS properties will be attributed to the whole block.* -- Global Style don't have an ID, so no ID will be appended making the selector to act in a more general way. We use selector for targeting specific elements in the block. Since we use CSS vars, most of the declaration do not need a selector since we want to set the vars at the block level. +- `properties` - is an array that include a list of CSS properties. Each property has a `property` key, which is the CSS property name, and a `value` key, which is the name of the attribute in the block metadata. The `value` key is the name of the block attribute from which we want to extract the value. `unit` is appending a unit string to the value (`px, rem, %`) - use this the value is has no unit by itself. `default` is used to set a default value, `format` is a function that is used to transform the value to the desired output (sometime is the value an attribute is a complex type like `array`, or `object` in which you need to convert them to a `string`), `condition` is a function that is used to check if the property should be added to the CSS file. `hasSync` is used to identify the attribute that is used for syncing the Global Styles (it must have the same name with the one declared in `render_global_css` but without `--` prefix). +- `pattern` and `pattern_values` is mechanism for creating complex CSS values like `box-shadow`. In `pattern` you put a list of tokens name, and in `pattern_values` you specify what values should be used for each token. The form is similar to the `properties` array. + +*When you are working with this, it is good to take inspiration from the existing code, and remember that at the end of day this is just a string with CSS declarations*. At the end of each `render_css` & `render_global_css` function, the `$style = $css->generate();` is present. This will generate the CSS string. + +You can always put any string in `$style` and it will be added to the CSS file; like the old days. + +Also, some code for the example at the begging: + +```php + +// For `render_css` + +$css->add_item( + array( + 'properties' => array( + array( + 'property' => '--color', + 'value' => 'color', + 'hasSync' => 'global-color', + ), + array( + 'property' => '--font-size', + 'value' => 'fontSizeText', + 'hasSync' => 'global-font-size', + ), + ) + ) +); + +// For `render_global_css` +// We assume the value keys are the same as the one in block metadata since the same structure is used. You can think Global Style as being a block globally available from which you can inherit the values. + +$css->add_item( + array( + 'selector' => ' .themeisle-block-test', + 'properties' => array( + array( + 'property' => '--global-color', + 'value' => 'color', + ), + array( + 'property' => '--global-font-size', + 'value' => 'fontSizeText', + ), + ) + ) +); +``` \ No newline at end of file diff --git a/docs/environment-installation.md b/docs/environment-installation.md new file mode 100644 index 000000000..5141449eb --- /dev/null +++ b/docs/environment-installation.md @@ -0,0 +1,184 @@ +# Environment for Development + +You need a WordPress environment to work on this project. + +Tools what you need to have: +- MySQL +- WordPress +- CLI tools: `wp-cli`, `composer`, `node`, `php` +- XDebug (optional) for debugging with Visual Studio Code or PHPStorm the PHP code. + +## Options + +### Local by Flywheel (The Best) + +This is an app that spin VM instances of WordPress with various tools for development. All of them in a friendly interface. + +![local by flywheel interface overview](./images/local-by-flywheel-overview.png) + +Battery included development environment: + - Cross-Platform: Windows, MacOS, Linux + - Easily crate WordPress instances. + - Easily switch between PHP versions. + - XDebug included with Add-On for Visual Studio Code and PHPStorm. + - Database management + - MailHog included + - Option to open a terminal instance with all tools included: `wp-cli, composer, php, mysql` + +Resources: + - Website: https://localwp.com + - Installation guide: https://localwp.com/help-docs/getting-started/installing-local/ + - Docs: https://localwp.com/help-docs/ + +Mentions: + - Working with composer should be done by using the terminal instance included in the app. Click on `Open Site Shell` in the app. + - Use PHP 7.3.X. This is required for composer commands (`composer run lint`, `composer run format`) to work. + - NodeJS is not included. You need to install it separately. + - This is a WordPress only environment. + - Making Visual Studio Code and PHPStorm to detect the PHP install file is tricky. In the terminal instance of the app, run `which php` and to see where is the PHP install file. Then in the IDE, set the PHP executable to that file. It looks something like this: `/Users/robert/Library/Application Support/Local/lightning-services/php-7.3.5+14/bin/darwin/bin/php` + +### Docker + +This is a containerized environment. You need to install Docker and Docker Compose. More about docker: https://www.docker.com/ + +The repo include a `docker-compose.yml` file that you can use to spin up a WordPress instance. + +In the root of the project run: + +```bash +docker-compose up -d +``` + +If you are using Docker for Windows or Docker for Mac, you see will in the interface that the containers are running. + +Unlike Local by Flywheel, this is not a friendly option if you do not know how to use Docker. The power of this option is that you can heavily customize the environment. +Also this work everywhere where Docker is supported. If you some exotic setup like Arch Linux, this is the best option. + +Tools included: + - WordPress Instance & `composer, wp-cli` + - PHP + - MySQL + - NodeJS + +Mentions: + - You can directly connect to container with Visual Studio Code. Making it very easy to work inside the container. + - PHPStorm have the same options but requires some setup. + +Resources: + - Docker: https://www.docker.com/ + - Docker Compose: https://docs.docker.com/compose/ + - Docker Compose file reference: https://docs.docker.com/compose/compose-file/ + - Docker Compose for WordPress: https://docs.docker.com/compose/wordpress/ + +### Valet (MacOS only) + +One lightweight option is to use Valet. This is a tool that you install on your Mac and it will create a local domain for you. You can use this domain to access your WordPress instance. This require some manual setup. You need to download WordPress and install it manually. Install a database manually. Install PHP, Composer, NodeJS, etc. manually. + +The main benefit of this option is that it is not a VM. The resource consumption is minimal. Like Docker, this is not a friendly option if you do not know how to use it. But is heavily customizable. + +Resources: + - Website: https://laravel.com/docs/5.8/valet + - Installation guide: https://laravel.com/docs/5.8/valet#installation + +Tools included: + - None + +Mentions: + - Since everything is installed on your computer, you can use any tool without constraints (any text editor, you can write bash script for automation). + +### MAMP + +App that can spin WordPress instances and other types of projects. + +Tools included: + - WordPress + - PHP + - MySQL + + Resources: + - Website: https://www.mamp.info/en/ + - Installation guide: https://documentation.mamp.info/en/MAMP-Mac/Getting-Started/ + +### Cloud + +One viable option is to have a cloud WP instance in which you can work. You can connect to the instance with SSH and work with it (Visual Studio Code & PHPStorm have the tools for this). This is a costly options and not to versatile. + +The big benefit of this option is that you can work on the same instance with multiple people. Usually is not case, but good to know as an option. + +Mentions: +- If you travel a lot and need to be more conservative with the battery, you can spin up a cloud instance and to do the heavy work there. + +### Pay attention to resource consumption + +Tools like Local by Flywheel, Docker, PHPStorm are heavy on the resources. If you have a low end computer, you need to be careful with the tools you use. If you have a powerful computer, you can use any of the tools. Sometime is better to make a mix of tools based on your strong desires. For example, you can use Local by Flywheel for easily management the WordPress instance and Visual Studio or lightweight text editors for coding. If you really want powerful PHP IDE, you can use PHPStorm for coding and Valet for WordPress instances. Or have a very fancy docker setup with CLI tools like `vim/neovim`, `hx` or `emacs`. + +## Installing the plugin + +After you have a WordPress instance, you need to install the plugin. Go to folder `wp-content/plugins` and clone the repo there with git. + +Note: this need to be done in your working environment ( the terminal that has access to all the tools: terminal instanced by Local, docker connection, etc). + +(if you use the docker-compose.yml file, this is step is already done for you.) + +```bash +git clone https://github.com/Codeinwp/otter-blocks.git +``` + +After you clone the repo, you need to install the dependencies. This is done with `composer` and `npm`. + +```bash +composer install --prefer-dist --no-progress --no-suggest && npm ci +``` + +Note: Use `npm ci` instead of `npm install` because the after also upgrade the packages (you will see `package.json` file changed ). This is not what you want. + +## Running the plugin: + +When developing, you need to run the plugin. This is done with `npm run start`. This will start a watcher that will watch for changes in the files and will compile the files. + +```bash +npm run start +``` + +Note: This is not working for all JS files! If you work in `src/animation`, when you make a change you need to restart the process. If you change a SCSS file, you need to restart the process. You will mainly work in `src/blocks` and there the watcher will work. This is a limitation of the current setup. + +When you add a new PHP file or work with Pull Request that add new PHP files, you need to run `composer dumpautoload -o` to make sure that the autoloader is up to date. Without this you will see errors in WordPress about not finding the PHP class. + +```bash +composer dumpautoload -o +``` + +## Working with Git + +The main workflow is around Git and Github. If you work on a issue, make sure that you create a separate branch from the `development` branch. When you are done with the issue or just to save the current work, create a Pull Request to the `development` branch. + +There are a lot of tools for Git: + - Visual Studio Code has a built in Git support. There are plugin that hugely improve the experience (maybe the best experience): [Gitlens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) and [Git Graph](https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph). + - PHPStorm has a built in Git support. + - GitKraken is a GUI for Git. + - GitHub Desktop is a GUI for Git. Very simple to use. Not feature rich as GitKraken. + - Sublime Merge (free to try, but it is a paid product) is a GUI for Git. Has a very good support for shortcuts (you can do almost do anything without touching the keyboard). Very fast and lightweight. + +Resources: + - Git: https://git-scm.com/ + - Git Book: https://git-scm.com/book/en/v2 + - Git Flow: https://nvie.com/posts/a-successful-git-branching-model/ + +## Day to day struggle + +Tools like: `composer, node, git` and Text Editor (or IDE) are a must to work with Otter. There are some feature that make your life easier: + +- Code Completion: Any IDE or Text Editor that can hook to Typescript LSP can have good completion for Typescript/Javascript. For PHP, dedicated IDE or plugin is needed. +- Debugging: PHPStorm and Visual Studio Code have a very good support for debugging out of box or with plugins. **When most needed, this feature has its weight measured in gold.** +- Search: The ability to easily navigate trough the code base. You can even use GitHub for this. Their support is pretty good but not so advanced as PHPStorm or Visual Studio Code. +- Refactoring: The ability to easily refactor the code. Any IDE or Text Editor can do this. For PHP, the most advanced is PHPStorm; Visual Studio Code can do it but require a paid plugin. +- Utility: `watson` for time tracking, `gh` for working with GitHub from terminal, `tldr` for quick help, `bat` for better `cat`, `exa` for better `ls`, `fd` for better `find`, `ripgrep` for better `grep`, `fzf` for fuzzy search, `httpie` for better `curl`. +- Smart documentation: ChatGPT, [phind](https://www.phind.com) +- AI Tools: ChatGPT, Copilot, [phind](https://www.phind.com) +- Global/Multi-Repo Code Search: Github, [Sourcegraph](https://sourcegraph.com/search) + + + + + + diff --git a/docs/form-block-workflow.md b/docs/form-block-workflow.md new file mode 100644 index 000000000..33865f2af --- /dev/null +++ b/docs/form-block-workflow.md @@ -0,0 +1,30 @@ +# Form Block Workflow + +A guide about how Form workflow works. + +## The scope + +Allowing the end user to send some data to the website owner. Data can be: text, number, email, date, files, etc. The can be sent to a third party service or to the website owner. It can go via email or internal storage. + +When doing this we need to consider the following: +- Bots. We need to prevent bots from sending data. +- Security. We need to prevent malicious data from being sent. Data sanitization, validation, etc. For files we have to consider the numbers, the size and the type. +- 3rd party services. We need to consider the API of the service and how to send the data. + +## How it works from the Submit click to the data being sent + +When the user clicks on the Submit button, the following happens: +- The data is validated with JS and collected. Then sended to the server via `wp-json/form/frontend` endpoint (definition in `./inc/server/class-form-server.php`). +- The data is validated with PHP via `otter_form_validate_form` filter hook. If the data is invalid, we check if it was sended by a bot with `otter_form_anti_spam_validation` filter hook. +- If all ok, we apply some data preparation `otter_form_data_preparation`. This will add or change the data from the `$form_data` variable. +- If everything goes well, we get the provider (the service that will receive the data) and run with the current data request of `$form_data`. +- At the end we do a `otter_form_after_submit` to trigger extra actions. (deleting files, sending data to 3rd party services, auto-responder, etc.) + +You will see in the server a lot of error handling. This is because we need to be sure that the data is sent to the user. If something goes wrong, we need to inform the user or the admin (in critical cases). For the server part, the PHP utility files are in `./inc/integrations/`. + +## Where are the options for the form? + +As you know, you can not trust the request that come to the server. It might be malicious. When we process a request, we pull the options of the form that send the request with the `get_option` function (the data is saved in WordPress options and you can see the definition in `./inc/plugins/class-options-settings.php` ) and check if request respect the options. If not, we return an error. + +If you add a new feature and need to save in options, make sure to change the definition from `./inc/plugins/class-options-settings.php`. + diff --git a/docs/handbook.md b/docs/handbook.md new file mode 100644 index 000000000..ec828dd28 --- /dev/null +++ b/docs/handbook.md @@ -0,0 +1,68 @@ +# Handbook + +> Things to take in consideration when developing. + +### Working with React hooks + +Hooks can be tricky to work with, especially with the `useEffect` hook or `useSelect/useDispatch` hooks from Gutenberg. + +- Infinite loops. To detect infinite loops, you can use `console.count` function and check the console. If you see a number that does not stop from increasing, you have an infinite loop. +- Throttle. Sometimes the hooks can activate more times that you anticipate. If you initially think that the hook should be triggered only once per change, but you see in console that it is triggered multiple times, you need to investigate other factors that may trigger the hooks. +- Conditions. If you have a condition that should trigger the hook, make sure that the condition is correct. This is usually the most common way to create infinite loops or throttle hooks. +- Dependencies. If you have a hook that should be triggered only when a specific dependency changes, make sure that the dependency is correct. +- When you split a big `useEffect` in smaller ones, make sure to monitor the activation call when developing. You can have some surprises on the activation chain. +- Prefer batch changes. Instead of having function calls split ( like `setAttributes({ color: newColor })` and `setAttributes({ fontSize: newFontSize})` ), try to have them in the same function call (`setAttributes({ color: newColor, fontSize: newFontSize })`). Newer version on React will batch the changes automatically and will trigger only one render. But we working in Gutenberg, the people might not always have latest version of Gutenberg. +- Pay attention when working with `refs`, sometime you have some external libs that are not made with React in mind (like Lottie player), and you want to update the state, pay attention to undefined values. + +Tips: Always put a `console.log/console.count` in the hook to see when it is triggered. This will help you to understand the flow. And always check for undefined values. + +### Adding components in Inspector + +Most of your time when developing a block will go in the Inspector. You will need to add new components, new options, new controls, etc. Here are some tips: +- If you need to add a new reusable component, make sure to add it in the `./src/blocks/components` folder. This will help you to keep the code clean and organized. +- Looks on how other blocks are made. You can even check the Gutenberg core blocks to see how they are made or tackle a specific issue. [Link to repo](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src). +- Consult the Gutenberg Story website to see available controls and components. [Link to website](https://wordpress.github.io/gutenberg/?path=%2Fstory%2Fdocs-introduction--page). +- Try to keep the code as tidy as possible. Ideally you will not want to do a lot of have work in the `inspector.js` file. The main objective is to set new data to `attributes`. + +### Making the Save function + +The `save` function is the one that will render the block on the front-end. The pain point of a `save` function is that when you need to change it, you need to make sure it does not break the block by emitting an incompatible HTML code. This is why we make the CSS values as dynamic with PHP. This way, if you change the CSS, the block will still work on the front-end without breaking the HTML. +- Only put the essential. Anything else should be done with PHP or JS (with frontend script). +- If you think that the structure will not be stable (many things can be re-structured for future features -- like a Form field) consider making the rendering with PHP. + +### CSS pain points. + +Most of the bugs are related to CSS and 50% of the dev pain come from them. Here are some tips: +- SCOPING. Scoping your CSS is essential to avoid conflicts with other blocks / theme related CSS or Core Components/Blocks. Sometime `>`, `:not()`, `has:()`, `:is()` can be a saviors or villains depending on how you use them. +- CSS Variables. CSS Variables are a great way to make your CSS dynamic. But they can be tricky to use. Make sure to use them in the right way. You can check the [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) documentation to see how to use them. +- CHECK THE NAMES. Is very easy to make typos and get frustrated when you don't see the changes. +- Check their values. You will sometime see values like `undefinedpx` (undefined value used as `px` value) or `Object [object]` (you have a structure and forget to transform it). Check for undefined values and their type. +- Explore the possibilities. As time goes, CSS is getting more functionality which can save you from implementing complex JS code and reducing the size. You check those sources: [CSS Tricks](https://css-tricks.com/), [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS), [Can I Use](https://caniuse.com/), [Kevin Powell - CSS Wizard](https://www.youtube.com/@KevinPowell/videos). + +### JS pain points. + +JS is life and blood of the blocks. JS mistakes are more easily to spot but still a source of pain. + +- Use Typescript where you can. It will help you to spot errors more rapidly. +- Use `// @ts-check` to active Typescript checking in your JS files. This usually does not work great with files that have relays on Gutenberg libs, but very good to check for mistakes in simple function. +- Use `debugger;`. Most of the time you will use `console.log`, but some times more power is needed. [Read more about debugger](https://developer.chrome.com/docs/devtools/javascript/). +- When you update a state, make sure to create a new object. If you update the same object, React will not trigger a re-render. `setAttributes( attributes ) vs setAttributes({ ...attributes })`. [Read more about this](https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update). + +### PHP pain points. + +At the end of day, you need to load your JS and CSS files to see your work. PHP might look boring, but there a lot of thing going on when you take in consideration WordPress ecosystem. + +- Best friend when is come exploring the WordPress ecosystem is the [WP Codex](https://codex.wordpress.org/Main_Page). It is a great source of information. +- Best search engine: [phind](https://www.phind.com) +- DEBUGGER. If it is possible, use a debugger. It will help you to understand what is going on in a much better way. +- Careful when using or adding hooks. It might break other plugins or functionality. Great attention to their priority. + +### PHP REST API pain points. + +When you create a new rest point, have in consideration: +- The name of the endpoint. Make sure that the name is not already used by another plugin or core. You can check the [WP REST API Handbook](https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/) to see how to create a new endpoint. +- Sanitize the data. Make sure that the data is sanitized before usage. You can use the `sanitize_text_field` function to sanitize text data. [Read more about sanitization](https://developer.wordpress.org/themes/theme-security/data-sanitization-escaping/). +- Error handling. Make sure that you handle the errors and report back to the client (the script that made the request). You need to tell the client what is the problem so it can display the issue and might offer guidance to the user on how to fix it. + + + diff --git a/docs/images/local-by-flywheel-overview.png b/docs/images/local-by-flywheel-overview.png new file mode 100644 index 000000000..45e3b5018 Binary files /dev/null and b/docs/images/local-by-flywheel-overview.png differ diff --git a/docs/index.md b/docs/index.md index b162ca0c6..2dad91af5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,30 @@ # Gutenberg Blocks - -- [Creating Blocks for Gutenberg](blocks/index.md) - + +Otter Blocks +- [Environment installation](environment.md) +- [Handbook](handbook.md) +- [Otter Family Plugins](otter-family-plugins.md) +- [Project Structure](project-structure.md) +- [Creating a new block](creating-blocks.md) +- [Dynamic CSS with PHP](dynamic-css-with-php.md) +- [Adding Patterns](adding-patterns.md) +- [Form Workflow](form-block-workflow.md) +- [Dynamic Conditions and Content](dynamic-conditions-and-content.md) +- [Sticky Block](sticky-blocks.md) +- [Copy/Paste Block Style](copy-paste.md) +- [WooCommerce Extensions](woocommerce-extensions.md) +- [Live Search](live-search.md) - [Testing Cheatsheet](testing-cheatsheet.md) +- [Testing Checklist](testing-checklist.md) +- [Unit Testing & E2E](uniit-and-e2e-testing.md) +- [Pull Request Best Practices](pull-request-best-practices.md) +- [Coding Best Practices](coding-best-practices.md) +- [Knowledge Stash](knowledge-stash.md) -- [Copy/Paste Block Style](copy-paste.md) +Other -- [Blocks](blocks.md) +- [Creating Blocks for Gutenberg](blocks/index.md) +- [Blocks Basics](blocks.md) - [Registration](blocks.md#registration) - [Server-side Rendering](blocks.md#server-side-rendering) - [Custom CSS & Google Fonts](blocks.md#custom-css--google-fonts) diff --git a/docs/knowledge-stash.md b/docs/knowledge-stash.md new file mode 100644 index 000000000..08100f678 --- /dev/null +++ b/docs/knowledge-stash.md @@ -0,0 +1,8 @@ +# Knowledge Stash + +> A collection of useful information that helps with the knowledge necessary to work with the project. + +| Link | Description | +| :- | :- | +| [WordPress Data Series: Overview and Introduction](https://unfoldingneurons.com/2020/wordpress-data-series-overview-and-introduction) | Great series on exploring Gutenberg data store| +| [4 reasons your z-index isn’t working (and how to fix it)](https://www.freecodecamp.org/news/4-reasons-your-z-index-isnt-working-and-how-to-fix-it-coder-coder-6bc05f103e6c/) | A page which explain some quirk for `z-index` property. `z-index` does not always work on how you expect. When fixing issue related to Modal component, keep in mind the context of `z-index`. | \ No newline at end of file diff --git a/docs/live-search.md b/docs/live-search.md new file mode 100644 index 000000000..138ad9707 --- /dev/null +++ b/docs/live-search.md @@ -0,0 +1,28 @@ +# Live Search + +Live Search plugin is an enhancement to default Search block. It unlock new style and the ability to preview search results. + +## Structure + +| Feature | Location | +| :-- | :-- | +| Editor Block Extension Upsell | `./src/blocks/plugins/live-search` | +| Editor Block Extension | `./src/pro/plugins/live-search` | +| Frontend | `./src/blocks/frontend/live-search` | +| Backend | `./plugins/otter-pro/inc/plugins/class-live-search` | + + +## How it works + +The normal search will only show the results when you press the Submit button, and the result will be a page with a list of posts. Live Search will show the results as you type, and the result will be shown as a dropdown with list of posts, pages, products, and other custom post types. + +Every time you type a new character, the search will be triggered with [this function](https://github.com/Codeinwp/otter-blocks/blob/4d9eafabaec64c02d0e522d78f08404e56a80396/src/blocks/frontend/live-search/index.ts#L59-L79). The request will be processed [here](https://github.com/Codeinwp/otter-blocks/blob/4d9eafabaec64c02d0e522d78f08404e56a80396/plugins/otter-pro/inc/server/class-live-search-server.php#L131-L171) by leveraging WordPress `WP_Query`. + +In a nutshell, this is just fancy interface that makes the classic Search Block to feel more modern. The inspiration point was the [search bar for MDN Web Docs](https://developer.mozilla.org/en-US/) [#1135](https://github.com/Codeinwp/otter-blocks/issues/1135). + +The main selling point of this plugin is showing useful information to the end user. This information is mostly from metadata, like: date, author, product price, category, etc. Users to run e-commerce websites will find this plugin very useful. + +When you are developing a new feature for it have those in mind: +- Monitor the request time, it should be as fast as possible. +- Watch the requests number. You don't want to overload the server. +- Pay attention to error handling when integrating with other plugins. diff --git a/docs/otter-family-plugins.md b/docs/otter-family-plugins.md new file mode 100644 index 000000000..09163e3e1 --- /dev/null +++ b/docs/otter-family-plugins.md @@ -0,0 +1,29 @@ +# The big four + +As you can see, there are four other plugins besides Otter that are part of the family. They are: +- Block Animation: allow you to add animation to any block. +- Blocks CSS: allow you to add custom CSS to any block. +- Block Export & Import: allow you to export and import blocks as JSON files. + +## Block Animation + +This plugin offer the option to the user to animate the block via: +- `animate.css` library ([link](https://animate.style)): we just add this classes to `className` attribute of the block. +- Typing (text only). We use a format tag (`o-anim-typing`) to mark the text that should be animated. We use a custom script to animate the text. +- Counting: Same as Typing, but with the tag `o-anim-counting`. + +When it come to upgrading, the `animate.css` library is sometime troublesome. We need to check if the new version is compatible with the old one. If not, we need to update the code to make it compatible. + +For Typing and Counting, we have total control over the code (since their are made in-house). So, it is easier to upgrade. + +## Blocks CSS + +This plugin allow the user to add custom CSS to any block. The CSS is added to the block via a `style` tag. This is handy for edge cases where the user need to enhance a block with an option that is not available in Inspector. + +Also, it allow us to provide quick hacks to the user until we fix the issue with styling in a Block. + +## Block Export & Import + +This is a utility plugin that allow the user to export and import blocks as JSON files. It is useful for the user to backup their blocks and for us to debug issues. Sometime we need to test a block with a specific configuration. This plugin allow us to do the sharing more easily. + +For importing, the trick is simple: deserialize the JSON file and replace the block with the new one. For exporting, we need to serialize the block and save it to a JSON file. \ No newline at end of file diff --git a/docs/project-structure.md b/docs/project-structure.md new file mode 100644 index 000000000..2c3eff5a4 --- /dev/null +++ b/docs/project-structure.md @@ -0,0 +1,60 @@ +# The Everlasting Otter + +As time goes by, the project grows and evolves. The codebase becomes more complex. It's important to keep the project organized and easy to understand for everyone. + +## Project structure + +- `docs/` contains the documentation of the project +- `src/` contains the source code of the project (mainly JS & SCSS files) + - `blocks/` the JS & SCSS files for Otter Blocks plugin + - `components/` contains reusable components + - `blocks/` blocks definition and functionality + - `frontend/` script that add functionality for the end user. E.g.: opening tabs in accordion, sending form to the backend for Form Block. + - `plugins/` global features: Global Defaults, Sticky Blocks, Copy & Paste Styles, Dynamic Content & Conditions + - `helpers/` utility functions: Add an ID to the block, Google Fonts loader. + - `test` contains the test files for the blocks + - `css/` contains the CSS files for Custom CSS plugin + - `animation/` contains the JS & SCSS files for Animation plugin + - `dashboard/` contains the JS & SCSS files for Otter Dashboard (in WP: Tools > Otter ) + - `export-import/` contains the JS & SCSS files for Export/Import Block plugin + - `pro/` contains the JS & SCSS files for Otter Pro plugin + - `blocks/` Pro blocks source files + - `components/` reusable components + - `dashboard/` dashboard extension with Pro features + - `helpers/` utility functions + - `plugins/` Pro features for Blocks: Dynamic Content & Conditions, Sticky options, Countdown options, Live Search, etc. + - `woocommerce/` WooCommerce features and extensions +- `inc/` contains the PHP files for all plugins + - `css/` CSS dynamic generator for Blocks. It's used to generate the CSS for the blocks based on the user settings. + - `integration/` Form Block utilities + - `patterns/` contains the patterns for the Pattern Library + - `plugins/` plugins functionality: Dynamic Content & Conditions, Stripe, WordPress Options definitions for Rest API, etc. + - `render/` render classes for dynamic blocks (e.g.: Form File Field, Google/Leaflet Map, Plugin Card, Stripe Checkout, etc.) + - `server/` WP REST API endpoints: Form Block, Dynamic Content & Conditions, Stripe, etc. +- `plugins/` contains PHP files for other plugins + - `blocks-css/` Custom CSS plugin + - `blocks-animation/` Animation plugin + - `blocks-export-import/` Export/Import Block plugin + - `otter-pro/` Otter Pro plugin + - `css/` CSS dynamic generator for Pro blocks + - `plugins/` Pro features for Blocks: Dynamic Content & Conditions, Sticky options, Countdown options, Live Search, etc. + - `render/` render classes for dynamic blocks: WooCommerce + - `server/` WP REST API endpoints: Live Search + +## Tips on navigation + +If you are working on Form block: `./src/blocks/blocks/form/`, `./inc/integrations/` and `./inc/server/class-form-server.php` are the main hot spots. + +Dealing with the CSS generation? `./inc/css/` are the main files. + +PHP loading related issues: `./inc/class-registration.php` is the main file. + +Add PHP functionality only for Otter Pro: `./plugins/otter-pro/` is the main folder. + +JS is not working on frontend for a block: `./src/blocks/frontend/` + +Add a new tab in Global Default interface for a block: `./src/blocks/plugins/options/global-defaults/controls` + +Add a new options in WordPress Options Settings: `./inc/plugins/class-options-settings.php` + +When you make a PHP file and don't know where to hook it up (make it visible to others) look at how similar file do it. API Endpoint? `./inc/server/`. CSS Generator? `./inc/css/`. Dynamic block rendering? `./inc/render/`. \ No newline at end of file diff --git a/docs/pull-request-best-practices.md b/docs/pull-request-best-practices.md new file mode 100644 index 000000000..ab9d22633 --- /dev/null +++ b/docs/pull-request-best-practices.md @@ -0,0 +1,47 @@ +# Pull Request Best Practices + +Making a good pull request is a great way to get your code reviewed and merged quickly. + +## Why is this important? + +A good redacted pull request can save a lot of time for both the tester and the reviewer. + +Let's say your coworker asked you for a feedback on a feature he is working on. You open the pull request and see no screenshot or video about the current state of the feature. +You have to clone the branch, build the app, and try it out. And while you are trying out you are not sure why some thing does not work and spend like 15 minutes trying to figure it out. You do not succeed and you ask your coworker about it. +Then he tells you that you need some X setting before trying it out, then you are like "oh, I did not know that, you could have told me that". + +Now imagine you are the tester. Without a good description, screenshot or a video you do not know what is the correct behavior and what is not. Sometime you can get the context from the issued linked to the pull request, but sometimes you can't (sometime the issue is long a discussion about what feature to include, and the discussion might not be integral since some information might be on private messages). + +Having a clear description on what feature does or what bug does fix, and how to test it, can save a lot of time for everyone. + +## How to write a good pull request + +We know why it is important to write a good pull request, but how do we do it? How does it look like? + +Examples of good pull requests: +- [Otter#1596](https://github.com/Codeinwp/otter-blocks/pull/1596) :: Bug fix +- [Otter#1562](https://github.com/Codeinwp/otter-blocks/pull/1562) :: Feature +- [Otter#1598](https://github.com/Codeinwp/otter-blocks/pull/1598) :: Feature +- [Otter#1529](https://github.com/Codeinwp/otter-blocks/pull/1529) :: Bug fix +- [Otter#1457](https://github.com/Codeinwp/otter-blocks/pull/1457) :: Feature +- [Neve#3940](https://github.com/Codeinwp/neve/pull/3940) :: Bug Fix +- [Neve#3939](https://github.com/Codeinwp/neve/pull/3939) :: Feature +- [Neve#3923](https://github.com/Codeinwp/neve/pull/3923) :: Bug Fix +- [Neve#3945](https://github.com/Codeinwp/neve/pull/3945) :: Bug Fix + + +What do you notice? Let's break it down: +- Summary: a short description of what the pull request does - implemented feature, fixed bug, etc. You can mention what are the changes (using a new function, adding a new filter, etc). +- Screenshots: a screenshot of the feature or bug fix if it is not well understand from the context. If you are fixing a bug, you can add a screenshot of the bug and a screenshot of the fix. In case of a feature, you can add a screenshot or a video on how it works. +- Testing instructions: a list of steps that the tester needs to do in order to test the feature or the bug fix. +- If your PR require a specific setup: 3rd party plugin, theme, API key, etc, -- **mention it in the testing instructions**. +- If you have a complex workflow to test the feature (like creating Stripe products in Stripe dashboard to test Stripe Checkout Button Block), you can add a video or a gif. You can use [Loom](https://www.loom.com/) or OBS to record your screen. A good video can be worth a thousand words and will result in a faster review and good feedback from QA team. +- Issue link: Every PR has to be linked to an issue (Do not forgot to use Github linking from Sidebar under Development section). + +A good rule of thumb is: "Every coworker who is going to see the pull request should be able to understand what it does and how to test it". + +And also remember: +- Not everyone is aware of the context of the pull request. Make it clear. +- Do not assume that the tester always knows how to test the feature or the bug fix. The person might be a new person unfamiliar with the project, or might be a person who is not familiar with the feature you are working on. +- If you can not find your words to describe a complex feature, you can make a video to show it and shortly explain some parts of it. +- Github has a lot of feature for formatting a pull request. Also, try to use as much as possible embedded images and videos. If they are embedded, they will be visible on all the apps that integrate with Github (like Visual Studio Code, JetBrains IDE suite), also it can reduce the number of opened tabs in the browser. \ No newline at end of file diff --git a/docs/testing-checklist.md b/docs/testing-checklist.md new file mode 100644 index 000000000..104f5be32 --- /dev/null +++ b/docs/testing-checklist.md @@ -0,0 +1,110 @@ +# Testing List +> This is a checklist, you can use it to make sure you did not miss a case. + +In this list we will try to cover the pain point cases when it come to testing a specific block. If you think that we missed something, please add it to the list. +This should help up to remember the most important cases when testing a block. + +Also have in mind [QA Guideline](https://themeisle.notion.site/QA-Rating-16d6511c22854a439c91e9776534cf79) + +## :warning: Preparations + + - If you reuse an instance for testing and the page CSS is broken (all blocks on the page are missing their settings for style), go to Settings > Otter > Dashboard and Regenerated the style. + - Make sure to use the development version of the plugin, so that console errors can be human read. + - For blocks that are responsive (section, tabs, etc) make sure that they inherit the values: Desktop > Tablet > Mobile. If tablet values are not set, then it will inherit the values from the desktop. Same principle for mobile values, they will inherit the values from the tablet. + +## Checklist Blocks + +A small checklist for not so obviously testing parts. + +- [ ] Section + - [ ] Working with 4-levels nesting. + - [ ] Responsive settings are working. + - [ ] Working with all the blocks. +- [ ] Accordion + - [ ] Very long text in title + - [ ] Working with 4-levels nesting +- [ ] Advanced Heading + - [ ] Animate the text + - [ ] Count Animation + - [ ] Typing Animation + - [ ] Transform to core Heading only block and vice versa +- [ ] Slider + - [ ] `slider.js` is loaded if Slider block is present in page + - [ ] Transform to core Gallery block and vice versa +- [ ] Bushiness Hour + - [ ] Works only Pro is enabled. Does not break the page when disabled. +- [ ] Countdown + - [ ] `countdown.js` is loaded only if Countdown block is present in page. + - [ ] Pro features work only for Pro users. + - [ ] Check linking with other blocks when time expire. +- [ ] Flip +- [ ] Font Awesome Icons + - [ ] FA and Themeisle icons have the same visual boundary. +- [ ] Form + - [ ] Test Email is working. + - [ ] Mailchimp Integration is working. + - [ ] Sendinblue Integration is working. + - [ ] An email with the error is send when a problem happen. + - [ ] Form options are saved. + - [ ] All inputs options are working. + - [ ] Consent is rendered when `Submit & Subscribe` action is set. + - [ ] ReCaptcha is working. + - [ ] 'form.js' is loaded only if Form block is present in page. + - [ ] Autoresponder is working. + - [ ] File Field is working with the given settings. + - [ ] File size limit. + - [ ] File type limit. + - [ ] File upload limit. + - [ ] It display correct error messages in Console. +- [ ] Google Map + - [ ] API Key are saved. + - [ ] Markers are working, including reusable blocks. +- [ ] Map + - [ ] Markers are working, including reusable blocks. + - [ ] Leaflet scripts are loaded only if Map block is present in page. +- [ ] Icon List + - [ ] FA and Themeisle icons have the same visual boundary. +- [ ] Lottie + - [ ] JSON loading is working. + - [ ] Lottie script is loaded only if Lottie block is present in page +- [ ] Progress Bar + - [ ] `progress-bar.js` script is loaded only if Lottie block is present in page +- [ ] Circle Counter + - [ ] `circle-counter.js` script is loaded only if Lottie block is present in page +- [ ] Tabs + - [ ] 'tabs.js' script is loaded only if Tabs block is present in page + - [ ] Working with 4-levels nesting + - [ ] Is working with other blocks: Countdown, Progress Bar, Circle Counter, Accordion, Flip, etc. +- [ ] Popup + - [ ] `popup.js` script is loaded only if Popup block is present in page + - [ ] Pro features work only for Pro users. +- [ ] Sticky + - [ ] All Stick To options are working. + - [ ] It does not break the page. + - [ ] Pro features work only for Pro users. +- [ ] Posts + - [ ] ACF Integration is working only for PRO. +- [ ] Product Review +- [ ] Product Review Comparison +- [ ] Section + - [ ] Pulling templates from Template Libary is working. +- [ ] Pricing +- [ ] About Author +- [ ] Group Button +- [ ] Dynamic Conditions + - [ ] It does not break the page. + - [ ] Pro features work only for Pro users. +- [ ] Dynamic Content + - [ ] It does not break the page. + - [ ] Pro features work only for Pro users. +- [ ] Live Search + - [ ] WooCommerce Integration + - [ ] Pro features work only for Pro users. + +## Checklist Performance + +- [ ] Core Web Vitals + - [ ] Minimum comulative layout shift. + - [ ] Scripts are defered (they are present in `` with `async` or `defer` attribute) and not affecting First Paint +- [ ] Page is not slowed down when few heavy blocks (e.g: Form, Slider, Icon List, Section) are present (minium 8) +- [ ] Block scripts are not loaded if their respective block is not present. (`form.js` should not be loaded if no Form block is present in page) diff --git a/docs/woocommerce-extensions.md b/docs/woocommerce-extensions.md new file mode 100644 index 000000000..60451e579 --- /dev/null +++ b/docs/woocommerce-extensions.md @@ -0,0 +1,28 @@ +# WooCommerce Extensions + +In WordPress, WooCommerce is the most popular plugin for e-commerce. In a simple view of a plugin developer: people with business that have a high chance to pay for a premium plugin. The market is so big that we can't ignore it. Otter can not a respectable plugin, without WooCommerce integration. + +The available WooCommerce extensions are: +- Add to cart Block +- Product Review with Woo Sync +- Live Search (show product information in search results) +- Small Showcase Blocks (title, price, rating, stock, etc.) + +Woo Commerce own plugin offer a lot of features for Gutenberg, and is mostly redundant to re-invent the wheel for most of them. So winning strategy is to allow Otter users to integrate WooCommerce with Otter Blocks. You use Product Review at the beginning then migrate to WooCommerce, no need to redo things, you can just sync it. The feeling is more like a 'handy feature` than a full blow WooCommerce extension plugin. + +## Structure + +| Feature | Location | +| :-- | :-- | +| Frontend Render Add to Cart | `./plugins/otter-pro/inc/render/class-add-to-cart-button-block.php` | +| Frontend Render Small Blocks | `./plugins/otter-pro/inc/render/woocommerce` | +| Editor Add to Cart | `./src/pro/blocks/add-to-cart-button` | +| Editor Small Blocks | `./src/pro/woocommerce` | + +For Live Search, learn more [here](live-search.md). + +## Mentions + +- All of those feature require WooCommerce to be installed and activated. If WooCommerce is not installed, the block will be disabled. The features are built on top of WooCommerce API, so it is not possible to use them without WooCommerce. +- When developing a new feature, you should always check if WooCommerce is installed and activated. If not, you should disable the feature. +- Have stability always in mind. An e-commerce website is extremely important for the owner. Crashing the website is like hiding the owner's wallet. \ No newline at end of file diff --git a/inc/class-base-css.php b/inc/class-base-css.php index 975c86be4..b4207244e 100644 --- a/inc/class-base-css.php +++ b/inc/class-base-css.php @@ -88,6 +88,7 @@ public function autoload_block_classes() { '\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_CSS', '\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_Input_CSS', '\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_Textarea_CSS', + '\ThemeIsle\GutenbergBlocks\CSS\Blocks\Form_Multiple_Choice_CSS', '\ThemeIsle\GutenbergBlocks\CSS\Blocks\Flip_CSS', '\ThemeIsle\GutenbergBlocks\CSS\Blocks\Progress_Bar_CSS', '\ThemeIsle\GutenbergBlocks\CSS\Blocks\Popup_CSS', @@ -230,19 +231,12 @@ public function get_blocks_css( $post_id ) { */ public function get_widgets_css() { if ( function_exists( 'has_blocks' ) ) { - $content = ''; - $widgets = get_option( 'widget_block', array() ); - - foreach ( $widgets as $widget ) { - if ( is_array( $widget ) && isset( $widget['content'] ) ) { - $content .= $widget['content']; - } - } + $content = Registration::get_active_widgets_content(); $blocks = parse_blocks( $content ); if ( ! is_array( $blocks ) || empty( $blocks ) ) { - return; + return ''; } $animations = boolval( preg_match( '/\banimated\b/', $content ) ); diff --git a/inc/class-blocks-css.php b/inc/class-blocks-css.php index c7fbcac9a..cd644316b 100644 --- a/inc/class-blocks-css.php +++ b/inc/class-blocks-css.php @@ -31,7 +31,7 @@ public function init() { add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_assets' ), 1 ); add_action( 'wp_head', array( $this, 'render_server_side_css' ) ); add_action( 'wp_loaded', array( $this, 'add_attributes_to_blocks' ) ); - add_filter( 'otter_blocks_css', array( $this, 'add_css_to_otter' ), 10, 1 ); + add_filter( 'otter_blocks_css', array( $this, 'add_css_to_otter' ) ); } /** @@ -100,6 +100,8 @@ public function render_server_side_css() { return; } + $content = ''; + if ( ! defined( 'OTTER_BLOCKS_VERSION' ) && get_queried_object() === null && @@ -108,11 +110,31 @@ function_exists( 'wp_is_block_theme' ) && current_theme_supports( 'block-templates' ) ) { global $_wp_current_template_content; - $blocks = parse_blocks( $_wp_current_template_content ); + + $slugs = array(); + $template_blocks = parse_blocks( $_wp_current_template_content ); + + foreach ( $template_blocks as $template_block ) { + if ( 'core/template-part' === $template_block['blockName'] ) { + $slugs[] = $template_block['attrs']['slug']; + } + } + + $templates_parts = get_block_templates( array( 'slugs__in' => $slugs ), 'wp_template_part' ); + + foreach ( $templates_parts as $templates_part ) { + if ( isset( $templates_part->content ) && in_array( $templates_part->slug, $slugs ) ) { + $content .= $templates_part->content; + } + } + + $content .= $_wp_current_template_content; } else { - $blocks = parse_blocks( $post->post_content ); + $content = $post->post_content; } + $blocks = parse_blocks( $content ); + if ( ! is_array( $blocks ) || empty( $blocks ) ) { return; } @@ -132,7 +154,7 @@ function_exists( 'wp_is_block_theme' ) && } /** - * Cycle thorugh Blocks + * Cycle through Blocks * * @param array $inner_blocks Array of blocks. * @param int $id Post ID. @@ -142,6 +164,7 @@ function_exists( 'wp_is_block_theme' ) && */ public function cycle_through_blocks( $inner_blocks, $id ) { $style = ''; + foreach ( $inner_blocks as $block ) { $file_name = get_post_meta( $id, '_themeisle_gutenberg_block_stylesheet', true ); $render_css = empty( $file_name ) || strpos( $file_name, 'post-v2' ) === false; @@ -156,11 +179,11 @@ public function cycle_through_blocks( $inner_blocks, $id ) { $reusable_block = get_post( $block['attrs']['ref'] ); if ( ! $reusable_block || 'wp_block' !== $reusable_block->post_type ) { - return; + return ''; } if ( 'publish' !== $reusable_block->post_status || ! empty( $reusable_block->post_password ) ) { - return; + return ''; } $blocks = parse_blocks( $reusable_block->post_content ); @@ -168,10 +191,11 @@ public function cycle_through_blocks( $inner_blocks, $id ) { $style .= $this->cycle_through_blocks( $blocks, $reusable_block->ID ); } - if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $style .= $this->cycle_through_blocks( $block['innerBlocks'], $id ); } } + return $style; } @@ -185,7 +209,7 @@ public function cycle_through_blocks( $inner_blocks, $id ) { public function add_attributes_to_blocks() { $registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered(); - foreach ( $registered_blocks as $name => $block ) { + foreach ( $registered_blocks as $block ) { $block->attributes['hasCustomCSS'] = array( 'type' => 'boolean', 'default' => false, diff --git a/inc/class-registration.php b/inc/class-registration.php index a6e12ff2b..e33c292d6 100644 --- a/inc/class-registration.php +++ b/inc/class-registration.php @@ -368,15 +368,7 @@ public function enqueue_dependencies( $post = null ) { $content = ''; if ( 'widgets' === $post ) { - $widgets = get_option( 'widget_block', array() ); - - foreach ( $widgets as $widget ) { - if ( is_array( $widget ) && isset( $widget['content'] ) ) { - $content .= $widget['content']; - } - } - - $post = $content; + $post = self::get_active_widgets_content(); } elseif ( 'block-templates' === $post ) { global $_wp_current_template_content; @@ -514,6 +506,9 @@ function() { 'already-registered' => __( 'The email was already registered!', 'otter-blocks' ), 'try-again' => __( 'Error. Something is wrong with the server! Try again later.', 'otter-blocks' ), 'privacy' => __( 'I have read and agreed the privacy statement.', 'otter-blocks' ), + 'too-many-files' => __( 'Too many files loaded. Maximum is: ', 'otter-blocks' ), + 'big-file' => __( 'File size is to big. The limit is: ', 'otter-blocks' ), + 'invalid-file' => __( 'Invalid files type. The submitted files could not be processed.', 'otter-blocks' ), ), ) ); @@ -687,15 +682,16 @@ public function enqueue_block_styles( $post ) { */ public function register_blocks() { $dynamic_blocks = array( - 'about-author' => '\ThemeIsle\GutenbergBlocks\Render\About_Author_Block', - 'form-nonce' => '\ThemeIsle\GutenbergBlocks\Render\Form_Nonce_Block', - 'google-map' => '\ThemeIsle\GutenbergBlocks\Render\Google_Map_Block', - 'leaflet-map' => '\ThemeIsle\GutenbergBlocks\Render\Leaflet_Map_Block', - 'plugin-cards' => '\ThemeIsle\GutenbergBlocks\Render\Plugin_Card_Block', - 'posts-grid' => '\ThemeIsle\GutenbergBlocks\Render\Posts_Grid_Block', - 'review' => '\ThemeIsle\GutenbergBlocks\Render\Review_Block', - 'sharing-icons' => '\ThemeIsle\GutenbergBlocks\Render\Sharing_Icons_Block', - 'stripe-checkout' => '\ThemeIsle\GutenbergBlocks\Render\Stripe_Checkout_Block', + 'about-author' => '\ThemeIsle\GutenbergBlocks\Render\About_Author_Block', + 'form-nonce' => '\ThemeIsle\GutenbergBlocks\Render\Form_Nonce_Block', + 'google-map' => '\ThemeIsle\GutenbergBlocks\Render\Google_Map_Block', + 'leaflet-map' => '\ThemeIsle\GutenbergBlocks\Render\Leaflet_Map_Block', + 'plugin-cards' => '\ThemeIsle\GutenbergBlocks\Render\Plugin_Card_Block', + 'posts-grid' => '\ThemeIsle\GutenbergBlocks\Render\Posts_Grid_Block', + 'review' => '\ThemeIsle\GutenbergBlocks\Render\Review_Block', + 'sharing-icons' => '\ThemeIsle\GutenbergBlocks\Render\Sharing_Icons_Block', + 'stripe-checkout' => '\ThemeIsle\GutenbergBlocks\Render\Stripe_Checkout_Block', + 'form-multiple-choice' => '\ThemeIsle\GutenbergBlocks\Render\Form_Multiple_Choice_Block', ); $dynamic_blocks = apply_filters( 'otter_blocks_register_dynamic_blocks', $dynamic_blocks ); @@ -717,6 +713,7 @@ public function register_blocks() { 'form-input', 'form-nonce', 'form-textarea', + 'form-multiple-choice', 'google-map', 'icon-list', 'icon-list-item', @@ -965,6 +962,40 @@ public static function sticky_style() { echo ''; } + /** + * Get the content of all active widgets. + * + * @return string + */ + public static function get_active_widgets_content() { + global $wp_registered_widgets; + $content = ''; + $valid_widgets = array(); + $widget_data = get_option( 'widget_block', array() ); + + // Loop through all widgets, and add any that are active. + foreach ( $wp_registered_widgets as $widget_name => $widget ) { + // Get the active sidebar the widget is located in. + $sidebar = is_active_widget( $widget['callback'], $widget['id'] ); + + if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) { + $key = $widget['params'][0]['number']; + + if ( isset( $widget_data[ $key ] ) ) { + $valid_widgets[] = (object) $widget_data[ $key ]; + } + } + } + + foreach ( $valid_widgets as $widget ) { + if ( isset( $widget->content ) ) { + $content .= $widget->content; + } + } + + return $content; + } + /** * The instance method for the static class. * Defines and returns the instance of the static class. diff --git a/inc/css/blocks/class-form-css.php b/inc/css/blocks/class-form-css.php index 568b0b485..3b58078e6 100644 --- a/inc/css/blocks/class-form-css.php +++ b/inc/css/blocks/class-form-css.php @@ -204,7 +204,7 @@ public function render_css( $block ) { ), array( 'property' => '--padding-tablet', - 'value' => 'paddingTablet', + 'value' => 'inputPaddingTablet', 'format' => function( $value, $attrs ) { return CSS_Utility::box_values( $value, @@ -220,7 +220,7 @@ public function render_css( $block ) { ), array( 'property' => '--padding-mobile', - 'value' => 'paddingMobile', + 'value' => 'inputPaddingMobile', 'format' => function( $value, $attrs ) { return CSS_Utility::box_values( $value, diff --git a/inc/css/blocks/class-form-multiple-choice-css.php b/inc/css/blocks/class-form-multiple-choice-css.php new file mode 100644 index 000000000..1ae268f26 --- /dev/null +++ b/inc/css/blocks/class-form-multiple-choice-css.php @@ -0,0 +1,52 @@ +add_item( + array( + 'properties' => array( + array( + 'property' => '--label-color', + 'value' => 'labelColor', + ), + ), + ) + ); + + $style = $css->generate(); + + return $style; + } + +} diff --git a/inc/css/class-css-handler.php b/inc/css/class-css-handler.php index e335a18ff..5f1149051 100644 --- a/inc/css/class-css-handler.php +++ b/inc/css/class-css-handler.php @@ -122,6 +122,8 @@ public function save_post_meta( \WP_REST_Request $request ) { $post_id = $request->get_param( 'id' ); self::generate_css_file( $post_id ); + self::mark_review_block_metadata( $post_id ); + return rest_ensure_response( array( 'message' => __( 'CSS updated.', 'otter-blocks' ) ) ); } @@ -207,10 +209,11 @@ public function save_block_meta( \WP_REST_Request $request ) { self::save_css_file( $post_id, $css ); + self::mark_review_block_metadata( $post_id ); + return rest_ensure_response( array( 'message' => __( 'CSS updated.', 'otter-blocks' ) ) ); } - /** * Function to save CSS into WordPress Filesystem. * @@ -443,6 +446,37 @@ public static function compress( $css ) { return $css; } + /** + * Mark in post meta if the post has a review block. + * + * @param int $post_id Post ID. + * @since 2.4.0 + * @access public + */ + public static function mark_review_block_metadata( $post_id ) { + if ( empty( $post_id ) ) { + return; + } + + $content = get_the_content( '', false, $post_id ); + $saved_value = boolval( get_post_meta( $post_id, '_themeisle_gutenberg_block_has_review', true ) ); + + if ( empty( $content ) ) { + + if ( true === $saved_value ) { + delete_post_meta( $post_id, '_themeisle_gutenberg_block_has_review' ); + } + + return; + } + + $has_review = false !== strpos( $content, '

About the Author

John Peterson

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Quinoa ramps hashtag yuccie, selfies af wolf. Thundercats brunch gastropub whatever poutine tattooed godard bespoke blog seitan flannel jianbing bitters cloud.. Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', + 'content' => '

About the Author

John Peterson

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Quinoa ramps hashtag yuccie, selfies af wolf. Thundercats brunch gastropub whatever poutine tattooed godard bespoke blog seitan flannel jianbing bitters cloud.. Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', ); diff --git a/inc/patterns/call-to-action.php b/inc/patterns/call-to-action.php index 568735545..98a874d16 100644 --- a/inc/patterns/call-to-action.php +++ b/inc/patterns/call-to-action.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Call to Action', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'cta' ), - 'content' => '
', // phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation + 'content' => '

A call to action section

A Call to action section

', // phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation ); diff --git a/inc/patterns/centered-testimonial-with-star-icons.php b/inc/patterns/centered-testimonial-with-star-icons.php index 4bccb5349..0bfe97de6 100644 --- a/inc/patterns/centered-testimonial-with-star-icons.php +++ b/inc/patterns/centered-testimonial-with-star-icons.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Centered Testimonial with Star Icons', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'columns' ), - 'content' => '

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

', + 'content' => '

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

', ); diff --git a/inc/patterns/columns-with-flip-boxes.php b/inc/patterns/columns-with-flip-boxes.php index 226701a4a..c409f9866 100644 --- a/inc/patterns/columns-with-flip-boxes.php +++ b/inc/patterns/columns-with-flip-boxes.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Columns with Flip Boxes', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'columns' ), - 'content' => '

A section with Flipboxes

This is a section description

Artworks

Finely crafted artworks

It\'s Party time!

This is the content of the back side. Customise it to match your needs.

Posters

Finely crafted artworks

It\'s Party time!

This is the content of the back side. Customise it to match your needs.

Museums

Finely crafted artworks

It\'s Party time!

This is the content of the back side. Customise it to match your needs.

', + 'content' => '

A section with Flipboxes

This is a section description

Artworks

Finely crafted artworks

It\'s Party time!

This is the content of the back side. Customise it to match your needs.

Posters

Finely crafted artworks

It\'s Party time!

This is the content of the back side. Customise it to match your needs.

Museums

Finely crafted artworks

It\'s Party time!

This is the content of the back side. Customise it to match your needs.

', ); diff --git a/inc/patterns/columns-with-image-features.php b/inc/patterns/columns-with-image-features.php index 2c7a09ab7..72f5f5851 100644 --- a/inc/patterns/columns-with-image-features.php +++ b/inc/patterns/columns-with-image-features.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Columns with Image Features', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'columns' ), - 'content' => '

Feature one

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Feature two

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Feature three

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', + 'content' => '

Feature one

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Feature two

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Feature three

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', ); diff --git a/inc/patterns/content-with-features.php b/inc/patterns/content-with-features.php index 78cb71617..f4cbc7910 100644 --- a/inc/patterns/content-with-features.php +++ b/inc/patterns/content-with-features.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Content with Features', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'columns' ), - 'content' => '
This is an overline

This is Heading two

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

Service title

Synergestic actionables. Organic growth deep dive but circle back or but what\'s the real problem we\'re trying to solve here?

Service title

Synergestic actionables. Organic growth deep dive but circle back or but what\'s the real problem we\'re trying to solve here?

Service title

Synergestic actionables. Organic growth deep dive but circle back or but what\'s the real problem we\'re trying to solve here?

', + 'content' => '
This is an overline

This is Heading two

Capitalize on low hanging fruit to identify a ballpark value added activity to beta test. Override the digital divide with additional clickthroughs from DevOps. Nanotechnology immersion along the information highway will close the loop on focusing solely on the bottom line.

Service title

Synergestic actionables. Organic growth deep dive but circle back or but what\'s the real problem we\'re trying to solve here?

Service title

Synergestic actionables. Organic growth deep dive but circle back or but what\'s the real problem we\'re trying to solve here?

Service title

Synergestic actionables. Organic growth deep dive but circle back or but what\'s the real problem we\'re trying to solve here?

', ); diff --git a/inc/patterns/content-with-progress-bars.php b/inc/patterns/content-with-progress-bars.php index b292c0dad..cddaaf975 100644 --- a/inc/patterns/content-with-progress-bars.php +++ b/inc/patterns/content-with-progress-bars.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Content with Progress Bars', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'columns' ), - 'content' => '

Skills

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta.

Statistic
50
Statistic
82
', + 'content' => '

Skills

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta.

Statistic
50
Statistic
82
', ); diff --git a/inc/patterns/cover-boxes-with-title-and-button.php b/inc/patterns/cover-boxes-with-title-and-button.php index fe6a978ee..38cd347cb 100644 --- a/inc/patterns/cover-boxes-with-title-and-button.php +++ b/inc/patterns/cover-boxes-with-title-and-button.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Cover Boxes with Title and Button', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'columns' ), - 'content' => '

Buildings

', + 'content' => '

Buildings

', ); diff --git a/inc/patterns/gallery.php b/inc/patterns/gallery.php index ff78f3dfd..50b85b5fb 100644 --- a/inc/patterns/gallery.php +++ b/inc/patterns/gallery.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Gallery', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'gallery' ), - 'content' => '

Gallery section

A gallery with a text column and some text

', + 'content' => '

Gallery section

A gallery with a text column and some text

', ); diff --git a/inc/patterns/hero-area-with-button.php b/inc/patterns/hero-area-with-button.php index dbecb8308..996c66537 100644 --- a/inc/patterns/hero-area-with-button.php +++ b/inc/patterns/hero-area-with-button.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Hero Area with Button', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'columns', 'header' ), - 'content' => '

Pumpkins & Penguins

Sed ut perspiciatis unde omnis iste natus

', + 'content' => '

Pumpkins & Penguins

Sed ut perspiciatis unde omnis iste natus

', ); diff --git a/inc/patterns/icons-and-text.php b/inc/patterns/icons-and-text.php index 4ec6f0656..1169834c0 100644 --- a/inc/patterns/icons-and-text.php +++ b/inc/patterns/icons-and-text.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Icons and Text', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'columns' ), - 'content' => '

Just another Section with a title on the side

Customisable

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Online

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Secure

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Strategic

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', + 'content' => '

Just another Section with a title on the side

Customisable

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Online

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Secure

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Strategic

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', ); diff --git a/inc/patterns/image-and-text-over-dark-background.php b/inc/patterns/image-and-text-over-dark-background.php index 643c5bc03..de1875463 100644 --- a/inc/patterns/image-and-text-over-dark-background.php +++ b/inc/patterns/image-and-text-over-dark-background.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Image and Text over Dark Background', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'columns' ), - 'content' => '

Overline

Section with image

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta.

', + 'content' => '

Overline

Section with image

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta.

', ); diff --git a/inc/patterns/service-boxes-on-dark-background.php b/inc/patterns/service-boxes-on-dark-background.php index 9c71f9d9b..b345f49ce 100644 --- a/inc/patterns/service-boxes-on-dark-background.php +++ b/inc/patterns/service-boxes-on-dark-background.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Service Boxes on Dark Background', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'columns' ), - 'content' => '

Subheader

Services Content

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Quinoa ramps hashtag yuccie, selfies af wolf. Thundercats brunch gastropub whatever poutine tattooed godard bespoke blog seitan flannel jianbing bitters cloud.

Service 1

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Wolf moon shoreditch biodiesel hoodie kale chips bitter.

Service 3

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Wolf moon shoreditch biodiesel hoodie kale chips bitter.

Service 2

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Wolf moon shoreditch biodiesel hoodie kale chips bitter.

', + 'content' => '

Subheader

Services Content

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Quinoa ramps hashtag yuccie, selfies af wolf. Thundercats brunch gastropub whatever poutine tattooed godard bespoke blog seitan flannel jianbing bitters cloud.

Service 1

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Wolf moon shoreditch biodiesel hoodie kale chips bitter.

Service 3

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Wolf moon shoreditch biodiesel hoodie kale chips bitter.

Service 2

Tousled letterpre tote bag bicycle rights cliche twee hashtag pokpo demos tanero lamina sime voti. Wolf moon shoreditch biodiesel hoodie kale chips bitter.

', ); diff --git a/inc/patterns/team-members.php b/inc/patterns/team-members.php index 38b0022ca..b2f6d62aa 100644 --- a/inc/patterns/team-members.php +++ b/inc/patterns/team-members.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Team Members', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'featured', 'columns', 'team' ), - 'content' => '

Our Team

The whole is much more than the sum of it\'s part. Meet our team!

Jake Austin

Founder

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

John Peterson

Technical Lead

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Mary Andrews

Marketing

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Josh Bourne

Support

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', + 'content' => '

Our Team

The whole is much more than the sum of it\'s part. Meet our team!

Jake Austin

Founder

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

John Peterson

Technical Lead

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Mary Andrews

Marketing

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

Josh Bourne

Support

Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque.

', ); diff --git a/inc/patterns/testimonial-columns.php b/inc/patterns/testimonial-columns.php index 9f306afbb..592c2404f 100644 --- a/inc/patterns/testimonial-columns.php +++ b/inc/patterns/testimonial-columns.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Testimonial Columns', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'testimonials', 'featured', 'columns' ), - 'content' => '

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

', + 'content' => '

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

', ); diff --git a/inc/patterns/testimonial-with-inline-image.php b/inc/patterns/testimonial-with-inline-image.php index 5e1e81a7a..6e9230cfa 100644 --- a/inc/patterns/testimonial-with-inline-image.php +++ b/inc/patterns/testimonial-with-inline-image.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Testimonial with Inline Image', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'testimonials' ), - 'content' => '

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

', + 'content' => '

"...Sed ut perspiciatis unde omnis natus error sit voluptatem accusantium doloremque..."

Jason Doe

', ); diff --git a/inc/patterns/text-with-image-columns.php b/inc/patterns/text-with-image-columns.php index 7efb63089..b8090fc12 100644 --- a/inc/patterns/text-with-image-columns.php +++ b/inc/patterns/text-with-image-columns.php @@ -8,5 +8,5 @@ return array( 'title' => __( 'Text with Image Columns', 'otter-blocks' ), 'categories' => array( 'otter-blocks', 'columns' ), - 'content' => '

Overline

Section with image

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque.

', + 'content' => '

Overline

Section with image

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque.

', ); diff --git a/inc/plugins/class-dashboard.php b/inc/plugins/class-dashboard.php index 9a3babf2f..6e7c089b5 100644 --- a/inc/plugins/class-dashboard.php +++ b/inc/plugins/class-dashboard.php @@ -27,6 +27,7 @@ class Dashboard { 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 ); } /** @@ -36,15 +37,38 @@ public function init() { * @access public */ public function register_menu_page() { - $page_hook_suffix = add_options_page( - __( 'Otter', 'otter-blocks' ), - __( 'Otter', 'otter-blocks' ), + $otter_icon = 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmcyIgogICB3aWR0aD0iMTc4LjY2NjY3IgogICBoZWlnaHQ9IjE3Ny4zMzMzMyIKICAgdmlld0JveD0iMCAwIDE3OC42NjY2NyAxNzcuMzMzMzMiCiAgIHNvZGlwb2RpOmRvY25hbWU9Ik90dGVyIFZlY3RvciBzaW1wbGUuYWkiCiAgIHhtbG5zOmlua3NjYXBlPSJodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy9uYW1lc3BhY2VzL2lua3NjYXBlIgogICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnMKICAgICBpZD0iZGVmczYiPgogICAgPGNsaXBQYXRoCiAgICAgICBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIKICAgICAgIGlkPSJjbGlwUGF0aDE2Ij4KICAgICAgPHBhdGgKICAgICAgICAgZD0iTSAwLDEzMyBIIDEzNCBWIDAgSCAwIFoiCiAgICAgICAgIGlkPSJwYXRoMTQiIC8+CiAgICA8L2NsaXBQYXRoPgogIDwvZGVmcz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzQiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcm9wYWNpdHk9IjAuMjUiCiAgICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIgLz4KICA8ZwogICAgIGlkPSJnOCIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlua3NjYXBlOmxhYmVsPSJPdHRlciBWZWN0b3Igc2ltcGxlIgogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDE3Ny4zMzMzMykiPgogICAgPGcKICAgICAgIGlkPSJnMTAiPgogICAgICA8ZwogICAgICAgICBpZD0iZzEyIgogICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxNikiPgogICAgICAgIDxnCiAgICAgICAgICAgaWQ9ImcxOCIKICAgICAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4Ny4zLDk2LjEpIj4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICBkPSJNIDAsMCBDIDAsMC4xIDAsMC4xIDAsMCAtMC4yLDAuNCAtMC4zLDAuNyAtMC41LDEgLTAuNiwxLjEgLTAuNiwxLjMgLTAuNywxLjQgLTEuNywzLjIgLTIuMSw1IC00LjUsNS4zIEggLTQuNyBDIC01LjYsNS4zIC02LjQsNSAtNyw0LjQgLTcuNiwzLjggLTgsMyAtOC4xLDIuMiAtNS45LDEuOCAtMy44LDEuMyAtMS43LDAuNiBMIC0wLjcsMC4yIC0wLjMsMC4xIDAsMCBjIC0wLjEsMCAtMC4xLDAgMCwwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgICAgIGlkPSJwYXRoMjAiIC8+CiAgICAgICAgPC9nPgogICAgICAgIDxnCiAgICAgICAgICAgaWQ9ImcyMiIKICAgICAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5Ny40LDg3LjUpIj4KICAgICAgICAgIDxwYXRoCiAgICAgICAgICAgICBkPSJtIDAsMCBjIC0wLjMsLTUuMSAtMi40LC0xMCAtNS43LC0xMy44IC02LjMsLTcuNCAtMTYuMiwtMTEuOSAtMjQuNywtMTYuMiAtOC41LDQuMyAtMTguNCw4LjggLTI0LjcsMTYuMSAtMy40LDMuOSAtNS40LDguNyAtNS43LDEzLjggLTAuNSw4LjEgMi4xLDIxLjYgMTEuMiwyNC42IDguOSwyLjkgMTQuNSwtNC43IDE2LjUsLTguMSAwLjMsLTAuNSAwLjcsLTAuOSAxLjIsLTEuMSAwLjUsLTAuMyAxLC0wLjQgMS42LC0wLjQgMC42LDAgMS4xLDAuMSAxLjYsMC40IDAuNSwwLjMgMC45LDAuNyAxLjIsMS4xIDIsMy41IDcuNywxMSAxNi41LDguMSBDIC0yLjEsMjEuNiAwLjUsOCAwLDAgbSA1LjEsLTEyLjEgYyAwLDAgMCwxMy44IC0xLjMsMjQuOCAwLjEsMCAwLjIsMC4xIDAuMywwLjEgMC44LDAuMiAxLjUsMC43IDIuMSwxLjQgMC41LDAuNyAwLjgsMS41IDAuOCwyLjMgMCwwLjggLTAuMiwxLjcgLTAuNywyLjQgLTAuNSwwLjcgLTEuMiwxLjIgLTIsMS41IEggNC4yIEMgMi41LDIxIDEsMjIuMSAwLDIzLjYgYyAtNC43LDYuNyAtMTEuNiwxMC4xIC0xNy42LDExLjkgLTQuMiwxLjIgLTguNSwxLjggLTEyLjgsMS44IC00LjYsMCAtOS4yLC0wLjcgLTEzLjYsLTIuMSAtNS44LC0xLjggLTEyLjMsLTUuMyAtMTYuNywtMTEuNyAtMSwtMS41IC0yLjUsLTIuNiAtNC4yLC0zLjIgLTAuOCwtMC4zIC0xLjUsLTAuOCAtMiwtMS41IC0wLjUsLTAuNyAtMC43LC0xLjUgLTAuNywtMi40IDAsLTAuOSAwLjMsLTEuNyAwLjgsLTIuNCAwLjUsLTAuNyAxLjMsLTEuMiAyLjEsLTEuNCBsIDAuMiwtMC4xIEMgLTY0LjksOSAtNjUuMiw1LjMgLTY1LjQsMS43IGMgLTAuNCwtNy42IC0wLjQsLTE0IC0wLjQsLTE0IDAsMCAtMTUuNiwtMjEuMSAtMTYuOSwtNDAgLTAuNywtOS40IDIuMiwtMTguMyAxMi4yLC0yMy44IDE3LjQsLTguOSAyNS4zLDIuOCAyNS4zLDIuOCAwLDAgNS4xLDYuMSAtNi42LDEyLjIgdiAxNi45IGMgMCwwLjYgMC4zLDEuMiAwLjcsMS42IDAuNCwwLjQgMSwwLjcgMS42LDAuNyBoIDQuNCB2IDIuMiBjIDAsMC41IDAuMiwwLjkgMC41LDEuMyAwLjMsMC4zIDAuOCwwLjUgMS4zLDAuNSBoIDYuMSBjIDAuNSwwIDAuOSwtMC4yIDEuMywtMC41IDAuMywtMC4zIDAuNSwtMC44IDAuNSwtMS4zIHYgLTIuMiBoIDEwIHYgMi4yIGMgMCwwLjUgMC4yLDAuOSAwLjUsMS4zIDAuMywwLjMgMC44LDAuNSAxLjMsMC41IGggNi4xIGMgMC41LDAgMC45LC0wLjIgMS4zLC0wLjUgMC4zLC0wLjMgMC41LC0wLjggMC41LC0xLjMgdiAtMi4yIGggNC40IGMgMC42LDAgMS4yLC0wLjIgMS42LC0wLjcgMC40LC0wLjQgMC43LC0xIDAuNywtMS42IHYgLTE2LjcgYyAtMTEuNywtNi4yIC02LjUsLTEyLjIgLTYuNSwtMTIuMiAwLDAgNy45LC0xMS43IDI1LjMsLTIuOCAzMC4yLDE2LjQgLTQuNyw2My44IC00LjcsNjMuOCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgICAgICBpZD0icGF0aDI0IiAvPgogICAgICAgIDwvZz4KICAgICAgICA8ZwogICAgICAgICAgIGlkPSJnMjYiCiAgICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDYuNyw5Ni4xKSI+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgZD0iTSAwLDAgQyAwLDAuMSAwLDAuMSAwLDAgMC4yLDAuNCAwLjMsMC43IDAuNSwxIDAuNiwxLjEgMC42LDEuMyAwLjcsMS40IDEuNywzLjIgMi4xLDUgNC41LDUuMyBIIDQuNyBDIDUuNiw1LjMgNi40LDUgNyw0LjQgNy42LDMuOCA4LDMgOCwyLjIgNS44LDEuOCAzLjcsMS4zIDEuNiwwLjYgTCAwLjYsMC4yIDAuMiwwLjEgMCwwIGMgMC4xLDAgMCwwIDAsMCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgICAgICBpZD0icGF0aDI4IiAvPgogICAgICAgIDwvZz4KICAgICAgICA8ZwogICAgICAgICAgIGlkPSJnMzAiCiAgICAgICAgICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjYuMSw3OS4yKSI+CiAgICAgICAgICA8cGF0aAogICAgICAgICAgICAgZD0ibSAwLDAgdiAtMi43IGMgLTEuOCwwIC0zLjcsLTAuMyAtNS40LC0wLjkgLTAuOCwtMC4yIC0xLjYsLTAuNSAtMi4zLC0wLjkgLTEuNCwtMC42IC0yLjcsLTEuMSAtNC4yLC0wLjMgLTAuNSwwLjMgLTAuOSwwLjcgLTEuMiwxLjIgLTAuMywwLjUgLTAuNCwxIC0wLjQsMS42IDAsMS4yIDAuNCwyLjMgMS4xLDMuMyAwLjEsMC4yIDAuMiwwLjUgMC4xLDAuNiAtMC4yLDAuMiAtMC41LDAgLTAuNywtMC4xIC0wLjUsLTAuMyAtMC45LC0wLjYgLTEuMywtMSAtMC43LC0wLjggLTEuMSwtMS45IC0xLjEsLTIuOSAwLC0wLjkgMC4zLC0xLjcgMC43LC0yLjUgMC41LC0wLjggMS4xLC0xLjQgMS45LC0xLjggMC44LC0wLjUgMS43LC0wLjcgMi43LC0wLjcgMC45LDAgMS45LDAuMiAyLjcsMC43IDIuNiwxLjMgNS41LDIgOC41LDEuOSBoIDAuMSBjIDMsMC4xIDYsLTAuNiA4LjYsLTIgMC44LC0wLjQgMS42LC0wLjcgMi41LC0wLjcgMC45LDAgMS43LDAuMiAyLjUsMC43IDAuOCwwLjUgMS41LDEuMSAyLDEuOSAwLjUsMC44IDAuNywxLjcgMC43LDIuNiAwLDAuOCAtMC4yLDEuNyAtMC42LDIuNCAtMC40LDAuNyAtMSwxLjMgLTEuOCwxLjcgQyAxNSwyLjIgMTQuOCwyLjMgMTQuNiwyLjMgSCAxNC41IEMgMTQsMi4yIDE0LjIsMS43IDE0LjQsMS40IGMgMC43LC0xIDEuMSwtMi4yIDEuMSwtMy40IDAsLTAuNiAtMC4yLC0xLjIgLTAuNSwtMS43IC0wLjMsLTAuNSAtMC43LC0wLjkgLTEuMywtMS4yIC0yLC0xLjEgLTMuNiwwLjQgLTUuNCwxIC0yLDAuNyAtNC4yLDEuMSAtNi40LDEuMiBsIDAsMi43IHoiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICAgICAgaWQ9InBhdGgzMiIgLz4KICAgICAgICA8L2c+CiAgICAgICAgPGcKICAgICAgICAgICBpZD0iZzM0IgogICAgICAgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY4LjMsODAuOCkiPgogICAgICAgICAgPHBhdGgKICAgICAgICAgICAgIGQ9Im0gMCwwIGMgMy42LDAuOCA2LjksMi43IDkuNCw1LjQgMC4zLDAuNCAwLjYsMC44IDAuNywxLjIgMC4xLDAuNSAwLjIsMC45IDAuMSwxLjQgLTAuMSwwLjUgLTAuMiwwLjkgLTAuNSwxLjQgLTAuMywwLjQgLTAuNiwwLjggLTEsMSAtMywyLjEgLTYuNiwzLjIgLTEwLjIsMy4yIC0zLjcsMCAtNy4yLC0xLjIgLTEwLjIsLTMuMyAtMC40LC0wLjMgLTAuNywtMC42IC0wLjksLTEgQyAtMTIuOCw4LjkgLTEzLDguNSAtMTMsOCBjIC0wLjEsLTAuNCAwLC0wLjkgMC4xLC0xLjMgMC4xLC0wLjQgMC40LC0wLjggMC43LC0xLjIgMi41LC0yLjggNS44LC00LjggOS41LC01LjYgMSwtMC4xIDEuOSwtMC4xIDIuNywwLjEiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICAgICAgaWQ9InBhdGgzNiIgLz4KICAgICAgICA8L2c+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo='; + + $page_hook_suffix = add_menu_page( + __( 'Otter Blocks', 'otter-blocks' ), + __( 'Otter Blocks', 'otter-blocks' ), 'manage_options', 'otter', - array( $this, 'menu_callback' ) + array( $this, 'menu_callback' ), + $otter_icon ); - add_action( "admin_print_scripts-{$page_hook_suffix}", array( $this, 'enqueue_options_assets' ) ); + add_action( "admin_print_scripts-$page_hook_suffix", array( $this, 'enqueue_options_assets' ) ); + + add_submenu_page( + 'otter', + __( 'Settings', 'otter-blocks' ), + __( 'Settings', 'otter-blocks' ), + 'manage_options', + 'otter', + '', + 0 + ); + + add_submenu_page( + 'otter', + __( 'Form Submissions', 'otter-blocks' ), + __( 'Form Submissions', 'otter-blocks' ), + 'manage_options', + 'form-submissions-free', + array( $this, 'form_submissions_callback' ), + 10 + ); } /** @@ -57,6 +81,56 @@ public function menu_callback() { echo '
'; } + /** + * The content of the form submissions upsell page. + */ + public function form_submissions_callback() { + ?> + +
+
+ Otter Form Submissions Upsell +

+

+ +
+
+ id || 'otter-blocks_page_form-submissions-free' === $screen->id ) { + $this->the_otter_banner(); + } + } + /** * Whether to show the feedback notice or not. * @@ -138,6 +224,50 @@ private function should_show_feedback_notice() { return ! empty( $installed ) && $installed < strtotime( '-5 days' ); } + /** + * The top Otter banner. + * + * @return void + */ + private function the_otter_banner() { + ?> + +
+
+ <?php esc_attr_e( 'Otter Blocks', 'otter-blocks' ); ?> +
+
+

+ +
+
+ [^"\'<>]+)["\']|data-id=["\'](?P[^"\'<>]+)["\']|data-before=["\'](?P[^"\'<>]+)["\']|data-after=["\'](?P[^"\'<>]+)["\']|data-length=["\'](?P[^"\'<>]+)["\']|data-date-type=["\'](?P[^"\'<>]+)["\']|data-date-format=["\'](?P[^"\'<>]+)["\']|data-date-custom=["\'](?P[^"\'<>]+)["\']|data-time-type=["\'](?P[^"\'<>]+)["\']|data-time-format=["\'](?P[^"\'<>]+)["\']|data-time-custom=["\'](?P[^"\'<>]+)["\']|data-term-type=["\'](?P[^"\'<>]+)["\']|data-term-separator=["\'](?P[^"\'<>]+)["\']|data-meta-key=["\'](?P[^"\'<>]+)["\']|data-parameter=["\'](?P[^"\'<>]+)["\']|data-format=["\'](?P[^"\'<>]+)["\']|data-context=["\'](?P[^"\'<>]+)["\']|[a-zA-Z-]+=["\'][^"\'<>]+["\']))*\s*>(?[^ $].*?)<\s*\/\s*o-dynamic>/'; + $re = '/[^"\'<>]+)["\']|data-id=["\'](?P[^"\'<>]+)["\']|data-before=["\'](?P[^"\'<>]+)["\']|data-after=["\'](?P[^"\'<>]+)["\']|data-length=["\'](?P[^"\'<>]+)["\']|data-date-type=["\'](?P[^"\'<>]+)["\']|data-date-format=["\'](?P[^"\'<>]+)["\']|data-date-custom=["\'](?P[^"\'<>]+)["\']|data-time-type=["\'](?P[^"\'<>]+)["\']|data-time-format=["\'](?P[^"\'<>]+)["\']|data-time-custom=["\'](?P[^"\'<>]+)["\']|data-term-type=["\'](?P[^"\'<>]+)["\']|data-term-separator=["\'](?P[^"\'<>]+)["\']|data-meta-key=["\'](?P[^"\'<>]+)["\']|data-parameter=["\'](?P[^"\'<>]+)["\']|data-format=["\'](?P[^"\'<>]+)["\']|data-context=["\'](?P[^"\'<>]+)["\']|data-taxonomy=["\'](?P[^"\'<>]+)["\']|[a-zA-Z-]+=["\'][^"\'<>]+["\']))*\s*>(?[^ $].*?)<\s*\/\s*o-dynamic>/'; return preg_replace_callback( $re, array( $this, 'apply_data' ), $content ); } diff --git a/inc/plugins/class-options-settings.php b/inc/plugins/class-options-settings.php index b492a823b..735bac7ae 100644 --- a/inc/plugins/class-options-settings.php +++ b/inc/plugins/class-options-settings.php @@ -318,6 +318,10 @@ function ( $item ) { if ( isset( $item['integration']['action'] ) ) { $item['integration']['action'] = sanitize_text_field( $item['integration']['action'] ); } + if ( isset( $item['submissionsSaveLocation'] ) ) { + $item['submissionsSaveLocation'] = sanitize_text_field( $item['submissionsSaveLocation'] ); + } + return $item; }, $array @@ -329,37 +333,37 @@ function ( $item ) { 'items' => array( 'type' => 'object', 'properties' => array( - 'form' => array( + 'form' => array( 'type' => 'string', ), - 'hasCaptcha' => array( + 'hasCaptcha' => array( 'type' => array( 'boolean', 'number', 'string' ), ), - 'email' => array( + 'email' => array( 'type' => 'string', ), - 'fromName' => array( + 'fromName' => array( 'type' => 'string', ), - 'redirectLink' => array( + 'redirectLink' => array( 'type' => 'string', ), - 'emailSubject' => array( + 'emailSubject' => array( 'type' => 'string', ), - 'submitMessage' => array( + 'submitMessage' => array( 'type' => 'string', ), - 'errorMessage' => array( + 'errorMessage' => array( 'type' => 'string', ), - 'cc' => array( + 'cc' => array( 'type' => 'string', ), - 'bcc' => array( + 'bcc' => array( 'type' => 'string', ), - 'autoresponder' => array( + 'autoresponder' => array( 'type' => 'object', 'properties' => array( 'subject' => array( @@ -370,7 +374,7 @@ function ( $item ) { ), ), ), - 'integration' => array( + 'integration' => array( 'type' => 'object', 'properties' => array( 'provider' => array( @@ -387,6 +391,86 @@ function ( $item ) { ), ), ), + 'submissionsSaveLocation' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + 'default' => array(), + ) + ); + + register_setting( + 'themeisle_blocks_settings', + 'themeisle_blocks_form_fields_option', + array( + 'type' => 'array', + 'description' => __( 'Form Fields used in the Form block.', 'otter-blocks' ), + 'sanitize_callback' => function ( $array ) { + return array_map( + function ( $item ) { + if ( isset( $item['fieldOptionName'] ) ) { + $item['fieldOptionName'] = sanitize_text_field( $item['fieldOptionName'] ); + } + if ( isset( $item['fieldOptionType'] ) ) { + $item['fieldOptionType'] = sanitize_text_field( $item['fieldOptionType'] ); + } + + if ( isset( $item['options']['maxFileSize'] ) ) { + $item['options']['maxFileSize'] = sanitize_text_field( $item['options']['maxFileSize'] ); + } + if ( isset( $item['options']['allowedFileTypes'] ) && is_array( $item['options']['allowedFileTypes'] ) ) { + foreach ( $item['options']['allowedFileTypes'] as $key => $value ) { + $item['options']['allowedFileTypes'][ $key ] = sanitize_text_field( $value ); + } + } + if ( isset( $item['options']['saveFiles'] ) ) { + $item['options']['saveFiles'] = sanitize_text_field( $item['options']['saveFiles'] ); + } + if ( isset( $item['options']['maxFilesNumber'] ) && ! is_int( $item['options']['maxFilesNumber'] ) ) { + $item['options']['maxFilesNumber'] = sanitize_text_field( $item['options']['maxFilesNumber'] ); + } + + return $item; + }, + $array + ); + }, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'fieldOptionName' => array( + 'type' => 'string', + ), + 'fieldOptionType' => array( + 'type' => 'string', + ), + 'options' => array( + 'type' => 'object', + 'properties' => array( + 'maxFileSize' => array( + 'type' => array( 'string', 'number' ), + ), + 'allowedFileTypes' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'saveFiles' => array( + 'type' => 'string', + ), + 'maxFilesNumber' => array( + 'type' => array( 'string', 'number' ), + ), + ), + 'default' => array(), + ), ), ), ), diff --git a/inc/plugins/class-stripe-api.php b/inc/plugins/class-stripe-api.php index 6be423e33..0fe548016 100644 --- a/inc/plugins/class-stripe-api.php +++ b/inc/plugins/class-stripe-api.php @@ -57,7 +57,7 @@ public function __construct() { /** * Check if API keys are set - * + * * @return bool * @access public */ @@ -86,7 +86,7 @@ public function init() { * Build Error Message * * @param object $error Error Object. - * + * * @return \WP_Error * @access public */ @@ -107,13 +107,17 @@ public function build_error_response( $error ) { * * @param string $path Request path. * @param array $args Request arguments. - * + * * @return mixed * @access public */ public function create_request( $path, $args = array() ) { $response = array(); + if ( ! self::has_keys() ) { + return $response; + } + try { switch ( $path ) { case 'products': @@ -169,7 +173,7 @@ public function create_request( $path, $args = array() ) { * * @param string $session_id Stripe Session ID. * @param string $price_id Price ID. - * + * * @return false|string * @access public */ @@ -198,7 +202,7 @@ public function get_status_for_price_id( $session_id, $price_id ) { * Set Customer ID for curent user. * * @param string $session_id Stripe Session ID. - * + * * @access public */ public function save_customer_data( $session_id ) { @@ -247,7 +251,7 @@ public function save_customer_data( $session_id ) { /** * Get Customer ID for curent user. - * + * * @return array * @access public */ @@ -277,7 +281,7 @@ public function get_customer_data() { * Check if user owns a product. * * @param string $product Product ID. - * + * * @return bool * @access public */ @@ -318,7 +322,7 @@ function( $item ) use ( $product ) { break; } } - + return $bool; } } diff --git a/inc/render/class-form-multiple-choice.php b/inc/render/class-form-multiple-choice.php new file mode 100644 index 000000000..70a96299c --- /dev/null +++ b/inc/render/class-form-multiple-choice.php @@ -0,0 +1,124 @@ +'; + + if ( 'select' === $field_type ) { + $output .= $this->render_select_field( $label, $options_array, $id, $has_multiple_selection, $is_required ); + } else { + $output .= ''; + + $output .= '
'; + + foreach ( $options_array as $field_label ) { + if ( empty( $field_label ) ) { + continue; + } + + $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 .= '
'; + } + + $output .= '' . $help_text . ''; + + $output .= ''; + return $output; + } + + /** + * Render an input field. + * + * @param string $type The type of the field (checkbox, radio). + * @param string $label The label of the field. + * @param string $value The value of the field. + * @param string $name The name of the field. + * @param string $id The id of the field. + * @param bool $is_required The required status of the field. + * @return string + */ + public function render_field( $type, $label, $value, $name, $id, $is_required = false ) { + $output = '
'; + + $output .= ''; + $output .= ''; + + $output .= '
'; + + return $output; + } + + /** + * Render a select field. + * + * @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 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 ) { + $output = ''; + $output .= ''; + return $output; + } + + /** + * Render the required sign. + * + * @param bool $is_required The required status of the field. + * @return string + */ + public function render_required_sign( $is_required ) { + return $is_required ? '*' : ''; + } +} diff --git a/inc/render/class-stripe-checkout-block.php b/inc/render/class-stripe-checkout-block.php index 21d17fc9c..4bc65eed2 100644 --- a/inc/render/class-stripe-checkout-block.php +++ b/inc/render/class-stripe-checkout-block.php @@ -38,9 +38,9 @@ public function render( $attributes ) { if ( false !== $status ) { if ( 'success' === $status ) { - $message = isset( $attributes['successMessage'] ) ? $attributes['successMessage'] : __( 'Your payment was successful. If you have any questions, please email orders@example.com.', 'otter-blocks' ); + $message = isset( $attributes['successMessage'] ) ? wp_kses_post( $attributes['successMessage'] ) : __( 'Your payment was successful. If you have any questions, please email orders@example.com.', 'otter-blocks' ); } else { - $message = isset( $attributes['cancelMessage'] ) ? $attributes['cancelMessage'] : __( 'Your payment was unsuccessful. If you have any questions, please email orders@example.com.', 'otter-blocks' ); + $message = isset( $attributes['cancelMessage'] ) ? wp_kses_post( $attributes['cancelMessage'] ) : __( 'Your payment was unsuccessful. If you have any questions, please email orders@example.com.', 'otter-blocks' ); } return sprintf( '

%1$s

', $message, $status ); diff --git a/inc/server/class-form-server.php b/inc/server/class-form-server.php index 8f9bc5006..63d3ccf8f 100644 --- a/inc/server/class-form-server.php +++ b/inc/server/class-form-server.php @@ -11,10 +11,13 @@ use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request; use ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response; use ThemeIsle\GutenbergBlocks\Integration\Form_Email; +use ThemeIsle\GutenbergBlocks\Integration\Form_Field_Option_Data; use ThemeIsle\GutenbergBlocks\Integration\Form_Providers; use ThemeIsle\GutenbergBlocks\Integration\Form_Settings_Data; +use ThemeIsle\GutenbergBlocks\Integration\Form_Utils; use ThemeIsle\GutenbergBlocks\Integration\Mailchimp_Integration; use ThemeIsle\GutenbergBlocks\Integration\Sendinblue_Integration; +use ThemeIsle\GutenbergBlocks\Pro; use WP_Error; use WP_HTTP_Response; use WP_REST_Request; @@ -57,6 +60,10 @@ class Form_Server { */ const ANTI_SPAM_TIMEOUT = 5000; // 5 seconds + /** + * Autoresponder Email Error Expiration Time + */ + const AUTO_RESPONDER_ERROR_EXPIRATION_TIME = 7 * 24 * 60; // 1 week /** * Initialize the class @@ -111,11 +118,34 @@ public function init() { ) ); - add_action( 'otter_form_before_submit', array( $this, 'before_submit' ) ); - add_action( 'otter_form_after_submit', array( $this, 'after_submit' ) ); - add_filter( 'otter_form_email_build_body', array( $this, 'build_email_content' ) ); - add_filter( 'otter_form_email_build_body_error', array( $this, 'build_email_error_content' ), 1, 2 ); + /** + * Register utility filters for form data validation. + */ + add_filter( 'otter_form_validate_form', array( $this, 'check_form_conditions' ) ); + add_filter( 'otter_form_validate_form', array( $this, 'check_form_files' ) ); + + /** + * Register utility filters for bot detection (e.g.: captcha, honeypot methods). + */ add_filter( 'otter_form_anti_spam_validation', array( $this, 'anti_spam_validation' ) ); + add_filter( 'otter_form_anti_spam_validation', array( $this, 'check_form_captcha' ) ); + + /** + * Register utility filters for form data preparation (e.g.: uploading files, database queries). + */ + add_filter( 'otter_form_data_preparation', array( $this, 'change_provider_based_on_consent' ) ); + + /** + * Register utility filters for email content building. + */ + add_filter( 'otter_form_email_build_body', array( $this, 'build_email_content' ) ); + add_filter( 'otter_form_email_build_body_error', array( $this, 'build_email_error_content' ), 1 ); + + /** + * Register utility actions that triggers after the main submit action. Actions that clean the data, generated files or auxiliary actions. + */ + add_action( 'otter_form_after_submit', array( $this, 'after_submit' ) ); + add_action( 'otter_form_after_submit', array( $this, 'send_error_email_to_admin' ), 999 ); } /** @@ -198,79 +228,90 @@ public function editor( $request ) { * * @param WP_REST_Request $request Form request. * @return WP_Error|WP_HTTP_Response|WP_REST_Response + * @throws \Exception Error. * @since 2.0.3 */ public function frontend( $request ) { $res = new Form_Data_Response(); - $form_data = new Form_Data_Request( json_decode( $request->get_body(), true ) ); + $form_data = new Form_Data_Request( $request ); try { - // Check is the request is OK. - $error_code = $this->check_form_conditions( $form_data ); - - if ( $error_code ) { - $res->set_code( $error_code ); - return $res->build_response(); - } - - // Verify the reCaptcha token. - if ( $form_data->payload_has_field( 'token' ) ) { - $result = $this->check_form_captcha( $form_data ); - if ( ! $result['success'] ) { - $res->set_code( Form_Data_Response::ERROR_INVALID_CAPTCHA_TOKEN ); - return $res->build_response(); - } - } + // Validate the form data. + $form_data = apply_filters( 'otter_form_data_validation', $form_data ); $form_options = Form_Settings_Data::get_form_setting_from_wordpress_options( $form_data->get_payload_field( 'formOption' ) ); $form_data->set_form_options( $form_options ); + $form_data = $this->pull_fields_options_for_form( $form_data ); - $anti_bot_check = apply_filters( 'otter_form_anti_spam_validation', $form_data ); + // Check bot validation. + $form_data = apply_filters( 'otter_form_anti_spam_validation', $form_data ); - if ( $anti_bot_check ) { - do_action( 'otter_form_before_submit', $form_data ); + // Prepare the form data. + $form_data = apply_filters( 'otter_form_data_preparation', $form_data ); + + // Check if $form_data has function get_error_code. Otherwise, it will throw an error. + if ( ! ( $form_data instanceof Form_Data_Request ) ) { + throw new \Exception( __( 'The form data class is not valid! Some hook is corrupting the data.', 'otter-blocks' ) ); + } + + if ( ! isset( $form_data ) || $form_data->has_error() ) { + $res->set_code( $form_data->get_error_code() ); + } else { // Select the submit function based on the provider. $provider_handlers = apply_filters( 'otter_select_form_provider', $form_data ); if ( $provider_handlers && Form_Providers::provider_has_handler( $provider_handlers, $form_data->get( 'handler' ) ) ) { // Send the data to the provider handler. - $provider_response = $provider_handlers[ $form_data->get( 'handler' ) ]( $form_data ); - - do_action( 'otter_form_after_submit', $form_data ); - - return $provider_response; + $form_data = $provider_handlers[ $form_data->get( 'handler' ) ]( $form_data ); } else { $res->set_code( Form_Data_Response::ERROR_PROVIDER_NOT_REGISTERED ); } do_action( 'otter_form_after_submit', $form_data ); - } else { - $res->set_code( Form_Data_Response::ERROR_BOT_DETECTED ); + + if ( ! ( $form_data instanceof Form_Data_Request ) ) { + throw new \Exception( __( 'The form data class is not valid after performing provider actions! Some hook is corrupting the data.', 'otter-blocks' ) ); + } + + if ( ! isset( $form_data ) || $form_data->has_error() ) { + $res->set_code( $form_data->get_error_code() ); + } else { + $res->set_code( Form_Data_Response::SUCCESS_EMAIL_SEND ); + $res->mark_as_success(); + } } } catch ( Exception $e ) { $res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR ); $res->add_reason( $e->getMessage() ); - $this->send_error_email( $e->getMessage(), $form_data ); + $form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, $e->getMessage() ); + $this->send_error_email( $form_data ); + } finally { + return $res->build_response(); } - - return $res->build_response(); } - /** * Send Email using SMTP. * * @param Form_Data_Request $form_data Data from request body. - * @return WP_Error|WP_HTTP_Response|WP_REST_Response + * @return Form_Data_Request * @since 2.0.3 */ public function send_default_email( $form_data ) { - $res = new Form_Data_Response(); + + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return $form_data; + } try { - $form_options = $form_data->get_form_options(); + $form_options = $form_data->get_form_options(); + + if ( Pro::is_pro_active() && ! str_ends_with( $form_options->get_submissions_save_location(), 'email' ) ) { + return $form_data; + } + $email_subject = isset( $form_options ) && $form_options->has_email_subject() ? $form_options->get_email_subject() : ( __( 'A new form submission on ', 'otter-blocks' ) . get_bloginfo( 'name' ) ); $email_message = Form_Email::instance()->build_email( $form_data ); @@ -313,21 +354,73 @@ public function send_default_email( $form_data ) { } } + $attachments = array(); + if ( $form_data->has_uploaded_files() && ! $form_data->can_keep_uploaded_files() ) { + foreach ( $form_data->get_uploaded_files_path() as $file ) { + if ( empty( $file['file_location_slug'] ) ) { + $attachments[] = $file['file_path']; + } + } + } + // phpcs:ignore - $res->set_success( wp_mail( $to, $email_subject, $email_body, $headers ) ); - if ( $res->is_success() ) { - $res->set_code( Form_Data_Response::SUCCESS_EMAIL_SEND ); - } else { - $res->set_code( Form_Data_Response::ERROR_EMAIL_NOT_SEND ); + $email_was_send = wp_mail( $to, $email_subject, $email_body, $headers, $attachments ); + if ( ! $email_was_send ) { + $form_data->set_error( Form_Data_Response::ERROR_EMAIL_NOT_SEND ); } } catch ( Exception $e ) { - $res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR ); - $res->add_reason( $e->getMessage() ); - $this->send_error_email( $e->getMessage(), $form_data ); + $form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, array( $e->getMessage() ) ); + $this->send_error_email( $form_data ); } finally { - $form_options = $form_data->get_form_options(); - $res->add_values( $form_options->get_submit_data() ); - return $res->build_response(); + return $form_data; + } + } + + /** + * Email the admin when an important error occurs. + * + * @param Form_Data_Request $form_data The form request data. + * @since 2.2.3 + */ + public function send_error_email_to_admin( $form_data ) { + if ( ! isset( $form_data ) || ( ! $form_data instanceof Form_Data_Request ) || $form_data->has_error() || $form_data->has_warning() ) { + + if ( ! isset( $form_data ) || ( ! $form_data instanceof Form_Data_Request ) ) { + $form_data = new Form_Data_Request( array() ); + $form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, array( __( 'Some hook is corrupting the Form processing pipeline.', 'otter-blocks' ) ) ); + } + + $send_email = false; + + switch ( $form_data->get_error_code() ) { + case Form_Data_Response::ERROR_PROVIDER_CREDENTIAL_ERROR: + case Form_Data_Response::ERROR_MISSING_EMAIL: + case Form_Data_Response::ERROR_RUNTIME_ERROR: + $send_email = true; + break; + } + + if ( + ! $send_email && + $form_data->has_warning() && + $form_data->has_warning_codes( + array( + Form_Data_Response::ERROR_AUTORESPONDER_COULD_NOT_SEND, + Form_Data_Response::ERROR_AUTORESPONDER_MISSING_EMAIL_FIELD, + ) + ) + ) { + $key = $form_data->get_form_option_id() . '_autoresponder_error'; + + if ( ! get_transient( $key ) ) { + $send_email = true; + set_transient( $key, true, self::AUTO_RESPONDER_ERROR_EXPIRATION_TIME ); + } + } + + if ( $send_email ) { + $this->send_error_email( $form_data ); + } } } @@ -335,9 +428,13 @@ public function send_default_email( $form_data ) { * Make additional changes before using the main handler function for submitting. * * @param Form_Data_Request $form_data The form request data. + * @return Form_Data_Request * @since 2.0.3 */ - public function before_submit( $form_data ) { + public function change_provider_based_on_consent( $form_data ) { + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return $form_data; + } // If there is no consent, change the service to send only an email. if ( @@ -349,6 +446,8 @@ public function before_submit( $form_data ) { ) { $form_data->change_provider( 'default' ); } + + return $form_data; } /** @@ -360,6 +459,10 @@ public function before_submit( $form_data ) { */ public function after_submit( $form_data ) { + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return; + } + // Send also an email to the form editor/owner with the data alongside the subscription. if ( 'submit-subscribe' === $form_data->get_form_options()->get_action() && @@ -374,10 +477,13 @@ public function after_submit( $form_data ) { * Check if the form was not completed by a bot. * * @param Form_Data_Request $form_data The form request data. - * @return boolean + * @return Form_Data_Request * @since 2.2.3 */ public function anti_spam_validation( $form_data ) { + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return $form_data; + } if ( $form_data->payload_has_field( 'antiSpamTime' ) && @@ -388,25 +494,30 @@ public function anti_spam_validation( $form_data ) { $form_data->get_payload_field( 'antiSpamTime' ) >= self::ANTI_SPAM_TIMEOUT && '' === $form_data->get_payload_field( 'antiSpamHoneyPot' ) ) { - return true; + return $form_data; } } - return false; + $form_data->set_error( Form_Data_Response::ERROR_BOT_DETECTED ); + return $form_data; } /** * Send an email about error, like: the integration api key is no longer valid. * - * @param string $error The error message. * @param Form_Data_Request $form_data The form request data. * @return void * @since 2.0.3 */ - public static function send_error_email( $error, $form_data ) { + public static function send_error_email( $form_data ) { + + if ( ! isset( $form_data ) ) { + $form_data = new Form_Data_Request( array() ); + } + $email_subject = ( __( 'An error with the Form blocks has occurred on ', 'otter-blocks' ) . get_bloginfo( 'name' ) ); - $email_message = Form_Email::instance()->build_error_email( $error, $form_data ); - $email_body = apply_filters( 'otter_form_email_build_body_error', $error, $email_message ); + $email_message = Form_Email::instance()->build_error_email( $form_data ); + $email_body = apply_filters( 'otter_form_email_build_body_error', $email_message ); // Sent the form date to the admin site as a default behaviour. $to = sanitize_email( get_site_option( 'admin_email' ) ); $headers = array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . esc_url( get_site_url() ) ); @@ -533,21 +644,22 @@ public function test_subscription_service( $form_data ) { * Subscribe the user to a service. * * @param Form_Data_Request $form_data The form data. - * @return WP_Error|WP_HTTP_Response|WP_REST_Response + * @return Form_Data_Request * @since 2.0.3 */ public function subscribe_to_service( $form_data ) { - $res = new Form_Data_Response(); + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return $form_data; + } try { // Get the first email from form. - $email = $this->get_email_from_form_input( $form_data ); + $email = $form_data->get_email_from_form_input(); if ( '' === $email ) { - self::send_error_email( __( 'Marketing Integration is active, but there is no Email field in the form. Please check your Form block settings in the page.', 'otter-blocks' ), $form_data ); - $res->mark_as_success(); - return $res->build_response(); + $form_data->set_error( Form_Data_Response::ERROR_MISSING_EMAIL, array( __( 'Marketing Integration is active, but there is no Email field in the form. Please check your Form block settings in the page.', 'otter-blocks' ) ) ); + return $form_data; } if ( @@ -555,8 +667,7 @@ public function subscribe_to_service( $form_data ) { $form_data->payload_has_field( 'consent' ) && ! $form_data->get_payload_field( 'consent' ) ) { - $res->mark_as_success(); - return $res->build_response(); + return $form_data; } // Get the api credentials from the Form block. @@ -580,46 +691,39 @@ public function subscribe_to_service( $form_data ) { $valid_api_key = $service::validate_api_key( $wp_options_form->get_api_key() ); if ( $valid_api_key['valid'] ) { - $res->copy( $service->subscribe( $email ) ); + $form_data = $service->subscribe( $form_data ); } else { - $res->set_code( $valid_api_key['code'] ); + $form_data->set_error( $valid_api_key['code'] ); } } else { - $res->set_code( $error_code ); + $form_data->set_error( $error_code ); } } catch ( Exception $e ) { - $res->set_code( Form_Data_Response::ERROR_RUNTIME_ERROR ); - $res->add_reason( $e->getMessage() ); - $this->send_error_email( $e->getMessage(), $form_data ); + $form_data->set_error( Form_Data_Response::ERROR_RUNTIME_ERROR, array( $e->getMessage() ) ); + $this->send_error_email( $form_data ); } finally { - // Handle the case when the credential are no longer valid. - if ( $res->is_credential_error() ) { - self::send_error_email( 'error', $form_data ); - } - $form_options = $form_data->get_form_options(); - $res->add_values( $form_options->get_submit_data() ); - - return $res->build_response(); + return $form_data; } } /** * Check for required data. * - * @param Form_Data_Request $data Data from the request. + * @param Form_Data_Request $form_data Data from the request. * * @return boolean * @since 2.0.0 */ - public function has_required_data( $data ) { - $main_fields_set = $data->are_fields_set( + public function has_required_data( $form_data ) { + + $main_fields_set = $form_data->are_fields_set( array( 'handler', 'payload', ) ); - $required_payload_fields = $data->are_payload_fields_set( + $required_payload_fields = $form_data->are_payload_fields_set( array( 'nonceValue', 'postUrl', @@ -628,7 +732,7 @@ public function has_required_data( $data ) { ) ); - $is_nonce_valid = wp_verify_nonce( $data->get_payload_field( 'nonceValue' ), 'form-verification' ); + $is_nonce_valid = wp_verify_nonce( $form_data->get_payload_field( 'nonceValue' ), 'form-verification' ); return $main_fields_set && $required_payload_fields && $is_nonce_valid; } @@ -639,25 +743,20 @@ public function has_required_data( $data ) { * @access public * @param Form_Data_Request $form_data Data from the request. * - * @return string + * @return Form_Data_Request * @since 2.0.0 */ public function check_form_conditions( $form_data ) { - if ( ! $this->has_required_data( $form_data ) ) { - return Form_Data_Response::ERROR_MISSING_DATA; + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return $form_data; } - $form_options = $form_data->get_form_options(); - - if ( - $form_options->form_has_captcha() && - ( - ! $form_data->payload_has_field( 'token' ) || - '' === $form_data->get_payload_field( 'token' ) - ) - ) { - return Form_Data_Response::ERROR_MISSING_CAPTCHA; + if ( ! $this->has_required_data( $form_data ) ) { + $form_data->set_error( Form_Data_Response::ERROR_MISSING_DATA ); + return $form_data; } + + return $form_data; } /** @@ -666,39 +765,47 @@ public function check_form_conditions( $form_data ) { * @access public * @param Form_Data_Request $form_data Data from the request. * - * @return array + * @return Form_Data_Request * @since 2.0.0 */ public function check_form_captcha( $form_data ) { - $secret = get_option( 'themeisle_google_captcha_api_secret_key' ); - $resp = wp_remote_post( - apply_filters( 'otter_blocks_recaptcha_verify_url', 'https://www.google.com/recaptcha/api/siteverify' ), - array( - 'body' => 'secret=' . $secret . '&response=' . $form_data->get_payload_field( 'token' ), - 'headers' => [ - 'Content-Type' => 'application/x-www-form-urlencoded', - ], + + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return $form_data; + } + + $form_options = $form_data->get_form_options(); + + if ( + $form_options->form_has_captcha() && + ( + ! $form_data->payload_has_field( 'token' ) || + '' === $form_data->get_payload_field( 'token' ) ) - ); - return json_decode( $resp['body'], true ); - } + ) { + $form_data->set_error( Form_Data_Response::ERROR_MISSING_CAPTCHA ); + } - /** - * The instance method for the static class. - * Defines and returns the instance of the static class. - * - * @static - * @since 1.0.0 - * @access public - * @return Form_Server - */ - public static function instance() { - if ( is_null( self::$instance ) ) { - self::$instance = new self(); - self::$instance->init(); + if ( $form_data->payload_has_field( 'token' ) ) { + $secret = get_option( 'themeisle_google_captcha_api_secret_key' ); + $resp = wp_remote_post( + apply_filters( 'otter_blocks_recaptcha_verify_url', 'https://www.google.com/recaptcha/api/siteverify' ), + array( + 'body' => 'secret=' . $secret . '&response=' . $form_data->get_payload_field( 'token' ), + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + ) + ); + + $result = json_decode( $resp['body'], true ); + + if ( ! $result['success'] ) { + $form_data->set_error( Form_Data_Response::ERROR_INVALID_CAPTCHA_TOKEN ); + } } - return self::$instance; + return $form_data; } /** @@ -715,12 +822,11 @@ public function build_email_content( $content ) { /** * Filter for the content of the email for errors. * - * @param string $error The error message. * @param string $content The content. * @return string * @sincee 2.0.3 */ - public function build_email_error_content( $error, $content ) { + public function build_email_error_content( $content ) { return $content; } @@ -736,7 +842,7 @@ public function get_email_from_form_input( Form_Data_Request $data ) { $inputs = $data->get_payload_field( 'formInputsData' ); if ( is_array( $inputs ) ) { foreach ( $data->get_payload_field( 'formInputsData' ) as $input_field ) { - if ( 'email' == $input_field['type'] ) { + if ( isset( $input_field['type'] ) && 'email' == $input_field['type'] ) { return $input_field['value']; } } @@ -744,6 +850,81 @@ public function get_email_from_form_input( Form_Data_Request $data ) { return ''; } + /** + * Validate the input fields with files. + * + * @param Form_Data_Request $form_data The form data. + * @return Form_Data_Request + * @since 2.2.3 + */ + public function check_form_files( $form_data ) { + if ( ! isset( $form_data ) || $form_data->has_error() ) { + return $form_data; + } + + $inputs = $form_data->get_form_inputs(); + + foreach ( $inputs as $input ) { + if ( Form_Utils::is_file_field( $input ) && ! Form_Utils::is_file_field_valid( $input ) ) { + $form_data->set_error( Form_Data_Response::ERROR_FILES_METADATA_FORMAT ); + return $form_data; + } + } + + return $form_data; + } + + /** + * Get the Field Options for the given Form. + * + * @param Form_Data_Request $form_data The form data. + * @since 2.2.3 + */ + public function pull_fields_options_for_form( $form_data ) { + if ( ! ( $form_data instanceof Form_Data_Request ) || $form_data->has_error() ) { + return $form_data; + } + + $global_fields_options = get_option( 'themeisle_blocks_form_fields_option' ); + + if ( empty( $global_fields_options ) ) { + return $form_data; + } + + foreach ( $form_data->get_form_inputs() as $input ) { + if ( isset( $input['metadata']['fieldOptionName'] ) ) { + $field_name = $input['metadata']['fieldOptionName']; + foreach ( $global_fields_options as $field ) { + if ( isset( $field['fieldOptionName'] ) && $field['fieldOptionName'] === $field_name ) { + $new_field = new Form_Field_Option_Data( $field_name, $field['fieldOptionType'], $field['options'] ); + $form_data->add_field_option( $new_field ); + break; + } + } + } + } + + return $form_data; + } + + /** + * The instance method for the static class. + * Defines and returns the instance of the static class. + * + * @static + * @since 1.0.0 + * @access public + * @return Form_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 * diff --git a/inc/server/class-stripe-server.php b/inc/server/class-stripe-server.php index 6476ba4fc..ddbe53550 100644 --- a/inc/server/class-stripe-server.php +++ b/inc/server/class-stripe-server.php @@ -35,18 +35,11 @@ class Stripe_Server { */ public $version = 'v1'; - /** - * Stripe Object. - * - * @var Stripe_Server - */ - public $stripe = ''; - /** * Initialize the class */ public function init() { - $this->stripe = new Stripe_API(); + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } @@ -105,7 +98,7 @@ public function register_routes() { * @access public */ public function get_products( \WP_REST_Request $request ) { - return $this->stripe->create_request( + return ( new Stripe_API() )->create_request( 'products', array( 'active' => true, @@ -123,7 +116,7 @@ public function get_products( \WP_REST_Request $request ) { * @access public */ public function get_price( \WP_REST_Request $request ) { - return $this->stripe->create_request( + return ( new Stripe_API() )->create_request( 'prices', array( 'active' => true, diff --git a/plugins/otter-pro/inc/class-main.php b/plugins/otter-pro/inc/class-main.php index 3a3b3487c..0f24982d8 100644 --- a/plugins/otter-pro/inc/class-main.php +++ b/plugins/otter-pro/inc/class-main.php @@ -58,7 +58,8 @@ public function autoload_classes( $classnames ) { '\ThemeIsle\OtterPro\Plugins\Block_Conditions', '\ThemeIsle\OtterPro\Plugins\Dynamic_Content', '\ThemeIsle\OtterPro\Plugins\Fonts_Module', - '\ThemeIsle\OtterPro\Plugins\Form_Block', + '\ThemeIsle\OtterPro\Plugins\Form_Pro_Features', + '\ThemeIsle\OtterPro\Plugins\Form_Emails_Storing', '\ThemeIsle\OtterPro\Plugins\License', '\ThemeIsle\OtterPro\Plugins\Live_Search', '\ThemeIsle\OtterPro\Plugins\Options_Settings', @@ -102,6 +103,7 @@ public function register_blocks( $blocks ) { 'product-title', 'product-upsells', 'review-comparison', + 'form-file', ); $blocks = array_merge( $blocks, $pro_blocks ); @@ -132,6 +134,7 @@ public function register_dynamic_blocks( $dynamic_blocks ) { 'product-title' => '\ThemeIsle\OtterPro\Render\WooCommerce\Product_Title_Block', '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', ); $dynamic_blocks = array_merge( $dynamic_blocks, $blocks ); @@ -152,6 +155,7 @@ public function register_blocks_css( $blocks ) { '\ThemeIsle\OtterPro\CSS\Blocks\Business_Hours_CSS', '\ThemeIsle\OtterPro\CSS\Blocks\Business_Hours_Item_CSS', '\ThemeIsle\OtterPro\CSS\Blocks\Review_Comparison_CSS', + '\ThemeIsle\OtterPro\CSS\Blocks\Form_File_CSS', ); $blocks = array_merge( $blocks, $pro_blocks ); diff --git a/plugins/otter-pro/inc/css/blocks/class-form-file-css.php b/plugins/otter-pro/inc/css/blocks/class-form-file-css.php new file mode 100644 index 000000000..a95942511 --- /dev/null +++ b/plugins/otter-pro/inc/css/blocks/class-form-file-css.php @@ -0,0 +1,52 @@ +add_item( + array( + 'properties' => array( + array( + 'property' => '--label-color', + 'value' => 'labelColor', + ), + ), + ) + ); + + $style = $css->generate(); + + return $style; + } + +} diff --git a/plugins/otter-pro/inc/plugins/class-dynamic-content.php b/plugins/otter-pro/inc/plugins/class-dynamic-content.php index c2eefdbaf..39b4cd5e1 100644 --- a/plugins/otter-pro/inc/plugins/class-dynamic-content.php +++ b/plugins/otter-pro/inc/plugins/class-dynamic-content.php @@ -174,6 +174,11 @@ public function get_terms( $data ) { if ( isset( $data['termType'] ) && 'tags' === $data['termType'] ) { $terms = get_the_tag_list( '', $separator, '', $data['context'] ); + } elseif ( isset( $data['termType'] ) && 'custom' === $data['termType'] && isset( $data['taxonomy'] ) ) { + $taxonomy_terms = get_the_term_list( $data['context'], $data['taxonomy'], '', $separator, '' ); + if ( ! empty( $taxonomy_terms ) && ! is_wp_error( $taxonomy_terms ) ) { + $terms = $taxonomy_terms; + } } else { $terms = get_the_category_list( $separator, '', $data['context'] ); } diff --git a/plugins/otter-pro/inc/plugins/class-form-block.php b/plugins/otter-pro/inc/plugins/class-form-block.php deleted file mode 100644 index 6c8f3ca38..000000000 --- a/plugins/otter-pro/inc/plugins/class-form-block.php +++ /dev/null @@ -1,133 +0,0 @@ -get_form_options()->has_autoresponder() ) { - return; - } - - $res = new Form_Data_Response(); - - $to = Form_Server::instance()->get_email_from_form_input( $form_data ); - if ( empty( $to ) ) { - $res->set_code( Form_Data_Response::ERROR_EMAIL_NOT_SEND ); - return $res->build_response(); - } - - $headers[] = 'Content-Type: text/html'; - $headers[] = 'From: ' . ( $form_data->get_form_options()->has_from_name() ? sanitize_text_field( $form_data->get_form_options()->get_from_name() ) : get_bloginfo( 'name', 'display' ) ); - - $autoresponder = $form_data->get_form_options()->get_autoresponder(); - $body = $this->replace_magic_tags( $autoresponder['body'], $form_data->get_form_inputs() ); - - // phpcs:ignore - $res->set_success( wp_mail( $to, $autoresponder['subject'], $body, $headers ) ); - if ( $res->is_success() ) { - $res->set_code( Form_Data_Response::SUCCESS_EMAIL_SEND ); - } else { - $res->set_code( Form_Data_Response::ERROR_EMAIL_NOT_SEND ); - } - - $form_options = $form_data->get_form_options(); - $res->add_values( $form_options->get_submit_data() ); - return $res->build_response(); - } - - /** - * Replace magic tags with the values from the form inputs. - * - * @param string $content The content to replace the magic tags. - * @param array $form_inputs The form inputs. - * - * @return string - */ - public function replace_magic_tags( $content, $form_inputs ) { - foreach ( $form_inputs as $field ) { - if ( isset( $field['id'] ) ) { - $content = str_replace( '%' . $field['id'] . '%', $field['value'], $content ); - } - } - - return $content; - } - - /** - * The instance method for the static class. - * Defines and returns the instance of the static class. - * - * @static - * @access public - * @return Form_Block - */ - 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 - * @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 - * @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/plugins/otter-pro/inc/plugins/class-form-emails-storing.php b/plugins/otter-pro/inc/plugins/class-form-emails-storing.php new file mode 100644 index 000000000..b12eace96 --- /dev/null +++ b/plugins/otter-pro/inc/plugins/class-form-emails-storing.php @@ -0,0 +1,1124 @@ + array( + 'name' => esc_html_x( 'Form Submissions', '', 'otter-blocks' ), + 'singular_name' => esc_html_x( 'Form Submission', '', 'otter-blocks' ), + 'search_items' => esc_html__( 'Search Submissions', 'otter-blocks' ), + 'all_items' => esc_html__( 'Form Submissions', 'otter-blocks' ), + 'view_item' => esc_html__( 'View Submission', 'otter-blocks' ), + 'update_item' => esc_html__( 'Update Submission', 'otter-blocks' ), + 'not_found' => esc_html__( 'No submissions found', 'otter-blocks' ), + 'not_found_in_trash' => esc_html__( 'No submissions found in the Trash', 'otter-blocks' ), + ), + 'capability_type' => self::FORM_RECORD_TYPE, + 'capabilities' => array( + 'create_posts' => 'create_otter_form_records', + ), + 'description' => __( 'Holds the data from the form submissions', 'otter-blocks' ), + 'public' => false, + 'show_ui' => true, + 'show_in_rest' => true, + 'supports' => array( 'title' ), + ) + ); + + register_post_status( + 'read', + array( + 'label' => _x( 'Read', 'otter-form-record', 'otter-blocks' ), + 'public' => true, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s the number of posts */ + 'label_count' => _n_noop( + 'Read (%s)', + 'Read (%s)', + 'otter-blocks' + ), + ) + ); + + register_post_status( + 'unread', + array( + 'label' => _x( 'Unread', 'otter-form-record', 'otter-blocks' ), + 'public' => true, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + /* translators: %s the number of posts */ + 'label_count' => _n_noop( + 'Unread (%s)', + 'Unread (%s)', + 'otter-blocks' + ), + ) + ); + } + + /** + * Set custom capabilities for otter_form_record. + * + * @return void + */ + public function set_form_records_cap() { + $role = get_role( 'administrator' ); + + if ( null === $role ) { + return; + } + + if ( ! method_exists( $role, 'add_cap' ) ) { + return; + } + + $role->add_cap( 'edit_' . self::FORM_RECORD_TYPE ); + $role->add_cap( 'read_' . self::FORM_RECORD_TYPE ); + $role->add_cap( 'delete_' . self::FORM_RECORD_TYPE ); + $role->add_cap( 'edit_' . self::FORM_RECORD_TYPE . 's' ); + $role->add_cap( 'read_' . self::FORM_RECORD_TYPE . 's' ); + $role->add_cap( 'delete_' . self::FORM_RECORD_TYPE . 's' ); + $role->remove_cap( 'create_' . self::FORM_RECORD_TYPE ); + $role->remove_cap( 'create_' . self::FORM_RECORD_TYPE . 's' ); + } + + /** + * Store form record in custom post type. + * + * @param Form_Data_Request $form_data The form data object. + * @return Form_Data_Request + */ + public function store_form_record( $form_data ) { + if ( + ! isset( $form_data ) || + ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || + ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || + $form_data->has_error() + ) { + return $form_data; + } + + $form_options = $form_data->get_form_options(); + + if ( ! isset( $form_options ) || ! str_starts_with( $form_options->get_submissions_save_location(), 'database' ) ) { + return $form_data; + } + + $email = Form_Server::instance()->get_email_from_form_input( $form_data ); + + $post_id = wp_insert_post( + array( + 'post_type' => self::FORM_RECORD_TYPE, + 'post_title' => ! empty( $email ) ? $email : __( 'New submission', 'otter-blocks' ), + 'post_status' => 'unread', + ) + ); + + if ( ! $post_id ) { + return $form_data; + } + + $meta = array( + 'email' => array( + 'label' => 'Email', + 'value' => $email, + ), + 'form' => array( + 'label' => 'Form', + 'value' => $form_data->get_payload_field( 'formId' ), + ), + 'post_url' => array( + 'label' => 'Post URL', + 'value' => $form_data->get_payload_field( 'postUrl' ), + ), + 'post_id' => array( + 'label' => 'Post ID', + 'value' => $form_data->get_payload_field( 'postId' ), + ), + ); + + $form_inputs = $form_data->get_form_inputs(); + $uploaded_files = $form_data->get_uploaded_files_path(); + $media_files = $form_data->get_files_loaded_to_media_library(); + $files = $form_data->get_request()->get_file_params(); + + foreach ( $form_inputs as $input ) { + if ( ! isset( $input['id'] ) ) { + continue; + } + + $id = substr( $input['id'], -8 ); + + if ( 'file' === $input['type'] ) { + + $id .= $input['metadata']['name'] . '_' . $input['metadata']['size']; + + $meta['inputs'][ $id ] = array( + 'label' => $input['label'], + 'value' => $input['value'], + 'type' => $input['type'], + 'metadata' => $input['metadata'], + ); + + $file_data_key = $input['metadata']['data']; + + if ( isset( $media_files[ $file_data_key ] ) ) { + $meta['inputs'][ $id ] = array_merge( + $meta['inputs'][ $id ], + array( + 'path' => $media_files[ $file_data_key ]['file_path'], + 'mime_type' => $media_files[ $file_data_key ]['file_type'], + 'attachment_id' => $media_files[ $file_data_key ]['file_id'], + 'saved_in_media' => true, + ) + ); + } elseif ( isset( $uploaded_files[ $file_data_key ] ) ) { + $meta['inputs'][ $id ] = array_merge( + $meta['inputs'][ $id ], + array( + 'path' => $uploaded_files[ $file_data_key ]['file_path'], + 'mime_type' => $uploaded_files[ $file_data_key ]['file_type'], + 'saved_in_media' => false, + ) + ); + } + } else { + $meta['inputs'][ $id ] = array( + 'label' => $input['label'], + 'value' => $input['value'], + 'type' => $input['type'], + 'metadata' => $input['metadata'], + ); + } + } + + add_post_meta( $post_id, self::FORM_RECORD_META_KEY, $meta ); + } + + /** + * Hide the default headline. + * + * @return void + */ + public function add_style() { + $screen = get_current_screen(); + if ( 'edit-' . self::FORM_RECORD_TYPE === $screen->id ) { + ?> + + '', + 'email' => __( 'Email', 'otter-blocks' ), + 'form' => __( 'Form ID', 'otter-blocks' ), + 'post_url' => __( 'Post', 'otter-blocks' ), + 'ID' => __( 'ID', 'otter-blocks' ), + 'submission_date' => __( 'Submission Date', 'otter-blocks' ), + ); + } + + /** + * Set the table sortable columns. + * + * @return array + */ + public function form_record_sortable_columns() { + return array( + 'email' => __( 'Email', 'otter-blocks' ), + 'ID' => __( 'ID', 'otter-blocks' ), + 'submission_date' => __( 'Submission Date', 'otter-blocks' ), + ); + } + + /** + * Set form records bulk actions. + * + * @return array + */ + public function form_record_bulk_actions() { + $status = isset( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : 'all'; // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + $bulk_actions = array(); + + if ( 'trash' !== $status ) { + $bulk_actions['trash'] = __( 'Move to Trash', 'otter-blocks' ); + + if ( 'unread' !== $status ) { + $bulk_actions['unread'] = __( 'Mark as Unread', 'otter-blocks' ); + } + + if ( 'read' !== $status ) { + $bulk_actions['read'] = __( 'Mark as Read', 'otter-blocks' ); + } + } else { + $bulk_actions['untrash'] = __( 'Restore', 'otter-blocks' ); + $bulk_actions['delete'] = __( 'Delete Permanently', 'otter-blocks' ); + } + + return $bulk_actions; + } + + /** + * Manage form records row actions. + * + * @param array $actions The current row actions. + * @param WP_Post $post The current post object. + * + * @return array + */ + public function form_record_row_actions( $actions, $post ) { + if ( 'otter_form_record' !== $post->post_type ) { + return $actions; + } + + unset( $actions['inline hide-if-no-js'] ); + unset( $actions['edit'] ); + + $status = $post->post_status; + if ( 'trash' !== $status ) { + $actions['view'] = sprintf( + '%s', + get_edit_post_link( $post->ID ), + __( 'View', 'otter-blocks' ) + ); + } + + if ( 'unread' === $status ) { + $actions['read'] = sprintf( + '%s', + 'row-read', + $post->ID, + wp_create_nonce( 'read-' . self::FORM_RECORD_TYPE . '_' . $post->ID ), + __( 'Mark as Read', 'otter-blocks' ) + ); + } elseif ( 'trash' !== $status ) { + $actions['unread'] = sprintf( + '%s', + 'row-unread', + $post->ID, + wp_create_nonce( 'unread-' . self::FORM_RECORD_TYPE . '_' . $post->ID ), + __( 'Mark as Unread', 'otter-blocks' ) + ); + } + + return $actions; + } + + /** + * Handle form record bulk actions. + * + * @param string $redirect The redirect URL. + * @param string $doaction The action being taken. + * @param array $object_ids The object IDs. + * + * @return string + */ + public function handle_form_record_bulk_actions( $redirect, $doaction, $object_ids ) { + $redirect = remove_query_arg( 'post_status', $redirect ); + + switch ( $doaction ) { + case 'read': + foreach ( $object_ids as $object_id ) { + wp_update_post( + array( + 'ID' => $object_id, + 'post_status' => 'read', + ) + ); + } + + $redirect = add_query_arg( 'post_status', 'read', $redirect ); + break; + case 'unread': + foreach ( $object_ids as $object_id ) { + wp_update_post( + array( + 'ID' => $object_id, + 'post_status' => 'unread', + ) + ); + } + + $redirect = add_query_arg( 'post_status', 'unread', $redirect ); + break; + } + + return $redirect; + } + + /** + * Mark form record as read when they're restored from trash. + * + * @param string $new_status The new status. + * @param string $old_status The old status. + * @param WP_Post $post The post object. + */ + public function transition_draft_to_read( $new_status, $old_status, $post ) { + if ( self::FORM_RECORD_TYPE !== $post->post_type || 'trash' !== $old_status || 'draft' !== $new_status ) { + return; + } + + wp_update_post( + array( + 'ID' => $post->ID, + 'post_status' => 'read', + ) + ); + } + + /** + * Add form record filters. + * + * @return void + */ + public function form_record_add_filters() { + if ( ! get_current_screen() || get_current_screen()->id !== 'edit-' . self::FORM_RECORD_TYPE ) { + return; + } + + $this->form_dropdown(); + $this->post_dropdown(); + } + + /** + * Parse form record filters. + * + * @param WP_Query $query Query. + * + * @return WP_Query + */ + public function form_record_filter_query( $query ) { + if ( empty( $_GET['filters_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['filters_nonce'] ), 'filter' ) ) { + return $query; + } + + if ( ! is_admin() || ! isset( $_GET['post_type'] ) || self::FORM_RECORD_TYPE !== $_GET['post_type'] ) { + return $query; + } + + if ( ! isset( $query->query['post_type'] ) || self::FORM_RECORD_TYPE !== $query->query['post_type'] ) { + return $query; + } + + global $pagenow; + if ( 'edit.php' !== $pagenow || ! isset( $_GET['filter_action'] ) ) { + return $query; + } + + $form = ( ! empty( $_REQUEST['form'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['form'] ) ) : ''; + $post = ( ! empty( $_REQUEST['post'] ) ) ? esc_url_raw( wp_unslash( $_REQUEST['post'] ) ) : ''; + + if ( ! empty( $form ) ) { + $query->query_vars['meta_query'][] = array( + 'key' => self::FORM_RECORD_META_KEY, + 'value' => $form, + 'compare' => 'LIKE', + ); + } + + if ( ! empty( $post ) ) { + $query->query_vars['meta_query'][] = array( + 'key' => self::FORM_RECORD_META_KEY, + 'value' => $post, + 'compare' => 'LIKE', + ); + } + + return $query; + } + + /** + * Manage form record columns. + * + * @param string $column The column name. + * @param int $post_id The post ID. + * + * @return void + */ + public function form_record_column_values( $column, $post_id ) { + $meta = get_post_meta( $post_id, self::FORM_RECORD_META_KEY, true ); + + switch ( $column ) { + case 'email': + if ( get_post_status( $post_id ) !== 'trash' ) { + $this->format_based_on_status( + sprintf( + '%2$s', + esc_url( get_edit_post_link( $post_id ) ), + esc_html( get_the_title( $post_id ) ) + ), + get_post_status( $post_id ) + ); + break; + } + + echo esc_html( get_the_title( $post_id ) ); + break; + case 'form': + $this->format_based_on_status( + sprintf( + '%2$s', + esc_url( $meta['post_url']['value'] . '#' . $meta['form']['value'] ), + esc_html( substr( $meta['form']['value'], -8 ) ) + ), + get_post_status( $post_id ) + ); + break; + case 'post_url': + // If the post ID is set, use that to get the title and URL for better accuracy. + if ( ! empty( $meta['post_id'] ) ) { + $source_post = '0' !== $meta['post_id']['value'] ? $meta['post_id']['value'] : get_option( 'page_for_posts' ); + $title = get_the_title( $source_post ); + $url = get_permalink( $source_post ); + } else { + if ( function_exists( 'wpcom_vip_url_to_postid' ) ) { + $source_post = wpcom_vip_url_to_postid( $meta['post_url']['value'] ); + } else { + $source_post = url_to_postid( $meta['post_url']['value'] ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid + } + + $source_post = 0 !== $source_post ? $source_post : get_option( 'page_for_posts' ); + $title = $source_post ? get_the_title( $source_post ) : $meta['post_url']['value']; + $url = $meta['post_url']['value']; + } + + $this->format_based_on_status( + sprintf( + '%2$s', + esc_url( $url ), + esc_html( $title ) + ), + get_post_status( $post_id ) + ); + break; + case 'ID': + $this->format_based_on_status( substr( $post_id, -8 ), get_post_status( $post_id ) ); + break; + case 'submission_date': + $this->format_based_on_status( + esc_html( get_the_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $post_id ) ), + get_post_status( $post_id ) + ); + break; + } + } + + /** + * Remove the 'publish' box from the otter_form_record post type. + * + * @return void + */ + public function handle_admin_menu() { + remove_meta_box( 'submitdiv', self::FORM_RECORD_TYPE, 'side' ); + + global $submenu; + unset( $submenu[ 'edit.php?post_type=' . self::FORM_RECORD_TYPE ] ); + + remove_menu_page( 'edit.php?post_type=' . self::FORM_RECORD_TYPE ); + remove_submenu_page( 'otter', 'form-submissions-free' ); + + add_submenu_page( + 'otter', + __( 'Form Submissions', 'otter-blocks' ), + __( 'Form Submissions', 'otter-blocks' ), + 'manage_options', + 'edit.php?post_type=' . self::FORM_RECORD_TYPE, + '', + 10 + ); + } + + /** + * Add meta box for form record. + * + * @return void + */ + public function add_form_record_meta_box() { + add_meta_box( + 'field_values_meta_box', + esc_html__( 'Submission Data', 'otter-blocks' ), + array( $this, 'fields_meta_box_markup' ), + self::FORM_RECORD_TYPE + ); + + // this will replace the default publish box, that's why it's using its id. + add_meta_box( + 'submitpost', + esc_html__( 'Update', 'otter-blocks' ), + array( $this, 'update_meta_box_markup' ), + self::FORM_RECORD_TYPE, + 'side' + ); + } + + /** + * Save data from form record meta box. + * + * @param int $post_id The post ID. + * @param WP_Post $post The post object. + * + * @return void + */ + public function form_record_save_meta_box( $post_id, $post ) { + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + if ( self::FORM_RECORD_TYPE !== $post->post_type ) { + return; + } + + if ( empty( $_POST['action'] ) || 'editpost' !== $_POST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + return; + } + + if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'update-post_' . $post->ID ) ) { + wp_die( esc_html__( 'Nonce not verified.', 'otter-blocks' ) ); + } + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( esc_html__( 'User cannot edit this post.', 'otter-blocks' ) ); + } + + $meta = get_post_meta( $post_id, self::FORM_RECORD_META_KEY, true ); + + foreach ( $_POST as $key => $value ) { + if ( ! str_starts_with( $key, 'otter_meta_' ) ) { + continue; + } + + $id = substr( $key, -8 ); + + if ( isset( $meta['inputs'][ $id ] ) && $meta['inputs'][ $id ]['value'] !== $value ) { + $meta['inputs'][ $id ]['value'] = $value; + } + } + + update_post_meta( $post_id, self::FORM_RECORD_META_KEY, $meta ); + } + + /** + * Render form record meta box. + * + * @param WP_Post $post The post object. + * @return void + */ + public function fields_meta_box_markup( $post ) { + $meta = get_post_meta( $post->ID, self::FORM_RECORD_META_KEY, true ); + $previous_field_option = ''; + + if ( empty( $meta ) ) { + return; + } + ?> + + + $field ) { ?> + + + + + + +
+ + render_field( $field, $id ); ?>
+ + + + + + + + + + + + + + ID, self::FORM_RECORD_META_KEY, true ); + ?> +
+ +
+
+ %s', + 'trash', + esc_attr( $post->ID ), + esc_attr( wp_create_nonce( 'trash-post_' . $post->ID ) ), + esc_html__( 'Move to Trash', 'otter-blocks' ) + ); + ?> +
+ +
+ ', + esc_html__( 'Update', 'otter-blocks' ) + ); + ?> +
+
+
+
+ + $post, + 'post_status' => 'read', + ) + ); + } + } + + /** + * Check request nonce and post ID. + * + * @param string $action The action name. + * + * @return string The post ID. + */ + public function check_posts( $action ) { + $id = ! empty( $_REQUEST[ self::FORM_RECORD_TYPE ] ) ? sanitize_text_field( wp_unslash( $_REQUEST[ self::FORM_RECORD_TYPE ] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + $post = get_post( $id ); + + if ( empty( $_REQUEST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['_wpnonce'] ) ), $action . '-' . self::FORM_RECORD_TYPE . '_' . $id ) ) { + wp_die( esc_html__( 'Security check failed', 'otter-blocks' ) ); + } + + if ( ! isset( $_REQUEST[ self::FORM_RECORD_TYPE ] ) ) { + wp_die( esc_html__( 'Post ID is required', 'otter-blocks' ) ); + } + + if ( ! $post ) { + wp_die( esc_html__( 'Invalid post ID', 'otter-blocks' ) ); + } + + if ( self::FORM_RECORD_TYPE !== $post->post_type ) { + wp_die( esc_html__( 'Invalid post type', 'otter-blocks' ) ); + } + + return $id; + } + + /** + * Read form record. + * + * @return void + */ + public function read_otter_form_record() { + $id = $this->check_posts( 'read' ); + wp_update_post( + array( + 'ID' => $id, + 'post_status' => 'read', + ) + ); + + wp_safe_redirect( remove_query_arg( array( 'action', self::FORM_RECORD_TYPE, '_wpnonce' ), admin_url( 'edit.php?post_type=' . self::FORM_RECORD_TYPE ) ) ); + exit; + } + + /** + * Unread form record. + * + * @return void + */ + public function unread_otter_form_record() { + $id = $this->check_posts( 'unread' ); + wp_update_post( + array( + 'ID' => $id, + 'post_status' => 'unread', + ) + ); + + wp_safe_redirect( remove_query_arg( array( 'action', self::FORM_RECORD_TYPE, '_wpnonce' ), admin_url( 'edit.php?post_type=' . self::FORM_RECORD_TYPE ) ) ); + exit; + } + + /** + * Get filter options. + * + * @param string $filter Filter. + * + * @return array + */ + private function get_filter( $filter ) { + /** + * Get all form records. Here we want to avoid using WP_Query to not + * trigger the 'form_record_filter_query'. This is why the $wpdb. + */ + $cache_key = 'otter_form_records'; + $form_records = wp_cache_get( $cache_key ); + + if ( ! $form_records ) { + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $form_records = $wpdb->get_results( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE post_type = %s AND post_status IN ('read', 'unread', 'trash', 'publish')", + self::FORM_RECORD_TYPE + ) + ); + + wp_cache_set( $cache_key, $form_records ); + } + + $options = array(); + foreach ( $form_records as $record ) { + $meta = get_post_meta( $record->ID, self::FORM_RECORD_META_KEY, true ); + + switch ( $filter ) { + case 'form': + $options[ $meta['form']['value'] ] = substr( $meta['form']['value'], -8 ); + break; + case 'post': + if ( function_exists( 'wpcom_vip_url_to_postid' ) ) { + $post_id = wpcom_vip_url_to_postid( $meta['post_url']['value'] ); + } else { + $post_id = url_to_postid( $meta['post_url']['value'] ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid + } + + $options[ $meta['post_url']['value'] ] = $post_id ? get_the_title( $post_id ) : $meta['post_url']['value']; + break; + } + } + + return $options; + } + + /** + * Get forms dropdown. + * + * @return void + */ + private function form_dropdown() { + $forms = $this->get_filter( 'form' ); + + if ( empty( $forms ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + $form = isset( $_GET['form'] ) ? sanitize_text_field( wp_unslash( $_GET['form'] ) ) : ''; + + ?> + + + get_filter( 'post' ); + + if ( empty( $posts ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification + $post = isset( $_GET['post'] ) ? sanitize_text_field( wp_unslash( $_GET['post'] ) ) : ''; + + ?> + + + ' . wp_kses_post( $content ) . ''; + return; + } + + echo wp_kses_post( $content ); + } + + /** + * The instance method for the static class. + * Defines and returns the instance of the static class. + * + * @static + * @access public + * @return Form_Emails_Storing + */ + 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 + * @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 + * @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/plugins/otter-pro/inc/plugins/class-form-pro-features.php b/plugins/otter-pro/inc/plugins/class-form-pro-features.php new file mode 100644 index 000000000..570a08b35 --- /dev/null +++ b/plugins/otter-pro/inc/plugins/class-form-pro-features.php @@ -0,0 +1,402 @@ +has_error() ) { + return $form_data; + } + + $inputs = $form_data->get_form_inputs(); + + $saved_files = array(); + $approved_fields = array(); + + try { + $counts_files = array(); + $files = $form_data->get_request()->get_file_params(); + + foreach ( $inputs as $input ) { + if ( + \ThemeIsle\GutenbergBlocks\Integration\Form_Utils::is_file_field( $input ) && + isset( $input['metadata']['fieldOptionName'] ) && + $form_data->has_field_option( $input['metadata']['fieldOptionName'] ) + ) { + $name = $input['metadata']['fieldOptionName']; + if ( ! isset( $counts_files[ $name ] ) ) { + $counts_files[ $name ] = 1; + } else { + $counts_files[ $name ]++; + + if ( + $form_data->get_field_option( $name )->has_option( 'maxFilesNumber' ) && + $counts_files[ $name ] > $form_data->get_field_option( $name )->get_option( 'maxFilesNumber' ) + ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_UPLOAD_MAX_FILES_NUMBER ); + break; + } + + if ( + ! $form_data->get_field_option( $name )->has_option( 'maxFilesSize' ) && + $counts_files[ $name ] > 10 + ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_UPLOAD_MAX_FILES_NUMBER ); + break; + } + } + } + } + + if ( ! $form_data->has_error() ) { + foreach ( $inputs as $input ) { + + $field_option_name = null; + + if ( isset( $input['metadata']['fieldOptionName'] ) ) { + $field_option_name = $input['metadata']['fieldOptionName']; + } + + if ( + \ThemeIsle\GutenbergBlocks\Integration\Form_Utils::is_file_field( $input ) && + isset( $field_option_name ) + ) { + + $field_option = $form_data->get_field_option( $field_option_name ); + + if ( is_null( $field_option ) ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_MISSING_FILE_FIELD_OPTION ); + break; + } + + $file_data_key = $input['metadata']['data']; + + if ( ! isset( $file_data_key ) || ! isset( $files[ $file_data_key ] ) ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_MISSING_BINARY ); + break; + } + + $file_data = $files[ $file_data_key ]; + + // Get the extension from file using WordPress functions. + $extension = wp_check_filetype( $file_data['name'] ); + + if ( ! $extension['ext'] ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_UPLOAD_TYPE_WP ); + break; + } + + $form_files_ext = $field_option->get_option( 'allowedFileTypes' ); + + if ( ! empty( $form_files_ext ) ) { + $form_files_ext = str_replace( '.', '', $form_files_ext ); + $form_files_ext = str_replace( '/*', '', $form_files_ext ); + + $mime_match = wp_match_mime_types( $form_files_ext, $extension['type'] ); + + if ( 0 == count( $mime_match ) ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_UPLOAD_TYPE ); + break; + } + } + + // Check the file size. + if ( $field_option->has_option( 'maxFileSize' ) ) { + $max_file_size = $field_option->get_option( 'maxFileSize' ); + $max_file_size = $max_file_size * 1024 * 1024; + + // Get $file_data file size. + $file_size = filesize( $file_data['tmp_name'] ); + if ( false === $file_size || $max_file_size < strlen( $file_size ) ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_UPLOAD_MAX_SIZE ); + break; + } + } + + $approved_fields[] = $input; + } + } + } + + if ( ! $form_data->has_error() ) { + foreach ( $approved_fields as $field ) { + $file = \ThemeIsle\GutenbergBlocks\Integration\Form_Utils::save_file_from_field( $field, $files ); + + if ( $file['success'] ) { + $field_option = $form_data->get_field_option( $field['metadata']['fieldOptionName'] ); + $saved_file = $field_option->get_option( 'saveFiles' ); + if ( ! empty( $saved_file ) ) { + $file['file_location_slug'] = $saved_file; + } + $file['key'] = $field['metadata']['data']; + $saved_files[ $field['metadata']['data'] ] = $file; + } else { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_UPLOAD, array( $file['error'] ) ); + break; + } + } + + if ( ! empty( $saved_files ) ) { + $form_data->set_uploaded_files_path( $saved_files ); + } + } + } catch ( \Exception $e ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_FILE_UPLOAD, array( $e->getMessage() ) ); + } finally { + if ( $form_data->has_error() ) { + foreach ( $saved_files as $saved_file ) { + wp_delete_file( $saved_file['file_path'] ); + } + } + + return $form_data; + } + } + + /** + * Delete the files uploaded from the File field via attachments. + * + * @param Form_Data_Request $form_data The files to delete. + * @since 2.2.5 + */ + public function clean_files_from_uploads( $form_data ) { + + if ( + ! isset( $form_data ) || + ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || + ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || + $form_data->has_error() + ) { + return $form_data; + } + + try { + $form_options = $form_data->get_form_options(); + $can_delete = true; + + if ( isset( $form_options ) ) { + $can_delete = 'email' === $form_options->get_submissions_save_location(); + } + + if ( $can_delete && $form_data->has_uploaded_files() ) { + foreach ( $form_data->get_uploaded_files_path() as $file ) { + if ( ! empty( $file['file_path'] ) ) { + wp_delete_file( $file['file_path'] ); + } + } + } + } catch ( \Exception $e ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_RUNTIME_ERROR, array( $e->getMessage() ) ); + } finally { + return $form_data; + } + } + + /** + * Load the files to the media library. + * + * @param Form_Data_Request $form_data The files to load. + * @return Form_Data_Request + * @since 2.2.5 + */ + public function load_files_to_media_library( $form_data ) { + if ( + ! isset( $form_data ) || + ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || + ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || + $form_data->has_error() ) { + return $form_data; + } + + try { + if ( $form_data->has_uploaded_files() ) { + + $media_files = array(); + foreach ( $form_data->get_uploaded_files_path() as $file ) { + + if ( empty( $file['file_location_slug'] ) || 'media-library' !== $file['file_location_slug'] ) { + continue; + } + + $attachment = array( + 'post_mime_type' => $file['file_type'], + 'post_title' => $file['file_name'], + 'post_content' => '', + 'post_status' => 'inherit', + ); + + $attachment_id = wp_insert_attachment( $attachment, $file['file_path'] ); + + $media_files[ $file['key'] ] = array( + 'file_path' => $file['file_path'], + 'file_name' => $file['file_name'], + 'file_type' => $file['file_type'], + 'file_id' => $attachment_id, + ); + } + + if ( ! empty( $media_files ) ) { + $form_data->set_files_loaded_to_media_library( $media_files ); + } + } + } catch ( \Exception $e ) { + $form_data->set_error( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_RUNTIME_ERROR, array( $e->getMessage() ) ); + } finally { + return $form_data; + } + } + + /** + * Send autoresponder email to the subscriber. + * + * @param Form_Data_Request $form_data The files to load. + * @return Form_Data_Request + * @since 2.2.5 + */ + public function send_autoresponder( $form_data ) { + + if ( + ! isset( $form_data ) || + ( ! class_exists( 'ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request' ) ) || + ! ( $form_data instanceof \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Request ) || + $form_data->has_error() || + ! $form_data->get_form_options()->has_autoresponder() + ) { + return $form_data; + } + + try { + + $to = \ThemeIsle\GutenbergBlocks\Server\Form_Server::instance()->get_email_from_form_input( $form_data ); + if ( empty( $to ) ) { + $form_data->add_warning( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_AUTORESPONDER_MISSING_EMAIL_FIELD ); + return $form_data; + } + + $headers[] = 'Content-Type: text/html'; + $headers[] = 'From: ' . ( $form_data->get_form_options()->has_from_name() ? sanitize_text_field( $form_data->get_form_options()->get_from_name() ) : get_bloginfo( 'name', 'display' ) ); + + $autoresponder = $form_data->get_form_options()->get_autoresponder(); + $body = $this->replace_magic_tags( $autoresponder['body'], $form_data->get_form_inputs() ); + + // phpcs:ignore + if ( ! wp_mail( $to, $autoresponder['subject'], $body, $headers ) ) { + $form_data->add_warning( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_AUTORESPONDER_COULD_NOT_SEND ); + } + } catch ( \Exception $e ) { + $form_data->add_warning( \ThemeIsle\GutenbergBlocks\Integration\Form_Data_Response::ERROR_RUNTIME_ERROR, array( $e->getMessage() ) ); + } finally { + return $form_data; + } + } + + /** + * Replace magic tags with the values from the form inputs. + * + * @param string $content The content to replace the magic tags. + * @param array $form_inputs The form inputs. + * @since 2.2.5 + * + * @return string + */ + public function replace_magic_tags( $content, $form_inputs ) { + foreach ( $form_inputs as $field ) { + if ( isset( $field['id'] ) ) { + $content = str_replace( '%' . $field['id'] . '%', $field['value'], $content ); + } + } + + return $content; + } + + + /** + * The instance method for the static class. + * Defines and returns the instance of the static class. + * + * @static + * @since 1.7.1 + * @access public + * @return Form_Pro_Features + */ + 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.1 + * @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.1 + * @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/plugins/otter-pro/inc/render/class-form-file-block.php b/plugins/otter-pro/inc/render/class-form-file-block.php new file mode 100644 index 000000000..6f7416aea --- /dev/null +++ b/plugins/otter-pro/inc/render/class-form-file-block.php @@ -0,0 +1,71 @@ + 1 ); + $allowed_files = isset( $attributes['allowedFileTypes'] ) ? implode( ',', $attributes['allowedFileTypes'] ) : ''; + + $output = '
'; + + $output .= ''; + + $output .= ''; + + $output .= '' + . $help_text + . ''; + + $output .= '
'; + return $output; + } + + /** + * Render the required sign. + * + * @param bool $is_required The required status of the field. + * @return string + */ + public function render_required_sign( $is_required ) { + return $is_required ? '*' : ''; + } +} diff --git a/readme.md b/readme.md index 462fd0f72..72e540db3 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ **Tags:** gutenberg blocks, gutenberg, block,post grid block, google map block, columns block, advanced columns, section, row, layout, templates, lottie, progress bar, product review, review, accordion, tabs, page builder, countdown, contact form, masonry, popup, review builder **Requires at least:** 5.9 **Tested up to:** 6.2 -**Requires PHP:** 5.4 +**Requires PHP:** 5.6 **Stable tag:** trunk **License:** GPLv3 **License URI:** https://www.gnu.org/licenses/gpl-3.0.en.html diff --git a/readme.txt b/readme.txt index ded19a8b7..54bb3f9be 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: themeisle, hardeepasrani, soarerobertdaniel7, mariamunteanu1, arin Tags: gutenberg blocks, gutenberg, block,post grid block, google map block, columns block, advanced columns, section, row, layout, templates, lottie, progress bar, product review, review, accordion, tabs, page builder, countdown, contact form, masonry, popup, review builder Requires at least: 5.9 Tested up to: 6.2 -Requires PHP: 5.4 +Requires PHP: 5.6 Stable tag: trunk License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.en.html diff --git a/src/blocks/blocks/accordion/group/edit.js b/src/blocks/blocks/accordion/group/edit.js index 5206bf960..a1096d140 100644 --- a/src/blocks/blocks/accordion/group/edit.js +++ b/src/blocks/blocks/accordion/group/edit.js @@ -18,7 +18,7 @@ import { __ } from '@wordpress/i18n'; import metadata from './block.json'; import Inspector from './inspector.js'; import BlockAppender from '../../../components/block-appender-button'; -import { boxValues, hex2rgba } from '../../../helpers/helper-functions'; +import { _px, boxValues, hex2rgba } from '../../../helpers/helper-functions'; import { blockInit, getDefaultValueByField, @@ -39,8 +39,6 @@ const PREFIX_TO_FAMILY = { fab: 'Font Awesome 5 Brands' }; -const px = value => value ? `${ value }px` : value; - /** * Accordion Group component * @param {import('./types').AccordionGroupProps} props @@ -83,19 +81,19 @@ const Edit = ({ '--padding': boxValues( attributes.padding, { top: '18px', right: '24px', bottom: '18px', left: '24px' }), '--padding-tablet': boxValues( attributes.paddingTablet, { top: '18px', right: '24px', bottom: '18px', left: '24px' }), '--padding-mobile': boxValues( attributes.paddingMobile, { top: '18px', right: '24px', bottom: '18px', left: '24px' }), - '--gap': px( attributes.gap ) + '--gap': _px( attributes.gap ) }; const [ fontCSSNodeName, setFontNodeCSS ] = useCSSNode(); useEffect( () => { setFontNodeCSS([ `> * > * > .wp-block-themeisle-blocks-accordion-item .wp-block-themeisle-blocks-accordion-item__title > * { - ${ attributes.fontSize ? ( 'font-size:' + attributes.fontSize + 'px' ) : '' }; + ${ attributes.fontSize ? ( 'font-size:' + _px( attributes.fontSize ) ) : '' }; ${ attributes.fontFamily ? ( 'font-family:' + attributes.fontFamily ) : '' }; ${ attributes.fontVariant ? ( 'font-variant:' + attributes.fontVariant ) : '' }; ${ attributes.fontStyle ? ( 'font-style:' + attributes.fontStyle ) : '' }; ${ attributes.textTransform ? ( 'text-transform:' + attributes.textTransform ) : '' }; - ${ attributes.letterSpacing ? ( 'letter-spacing:' + attributes.letterSpacing + 'px' ) : '' }; + ${ attributes.letterSpacing ? ( 'letter-spacing:' + _px( attributes.letterSpacing ) ) : '' }; }` ]); }, [ attributes.fontSize, attributes.fontFamily, attributes.fontVariant, attributes.fontStyle, attributes.textTransform, attributes.letterSpacing ]); diff --git a/src/blocks/blocks/accordion/item/inspector.js b/src/blocks/blocks/accordion/item/inspector.js index 8657036b9..ef61e7e84 100644 --- a/src/blocks/blocks/accordion/item/inspector.js +++ b/src/blocks/blocks/accordion/item/inspector.js @@ -28,16 +28,16 @@ const Inspector = ({ return; } - const parentClientId = select( 'core/block-editor' ).getBlockParents( clientId ).at( -1 ); - const parentBlock = select( 'core/block-editor' ).getBlock( parentClientId ); + const parentClientId = select( 'core/block-editor' )?.getBlockParents( clientId ).at( -1 ); + const parentBlock = select( 'core/block-editor' )?.getBlock( parentClientId ); - if ( parentBlock.attributes.alwaysOpen ) { + if ( parentBlock?.attributes.alwaysOpen ) { return; } - parentBlock.innerBlocks.forEach( sibling => { + parentBlock?.innerBlocks?.forEach( sibling => { if ( sibling.clientId !== clientId ) { - dispatch( 'core/editor' ).updateBlockAttributes( sibling.clientId, { initialOpen: false }); + dispatch( 'core/editor' )?.updateBlockAttributes( sibling.clientId, { initialOpen: false }); } }); }; diff --git a/src/blocks/blocks/circle-counter/edit.js b/src/blocks/blocks/circle-counter/edit.js index d05050335..0d5279d7b 100644 --- a/src/blocks/blocks/circle-counter/edit.js +++ b/src/blocks/blocks/circle-counter/edit.js @@ -30,11 +30,10 @@ import metadata from './block.json'; import Inspector from './inspector.js'; import CircularProgressBar from './components/circular-progress-bar.js'; import { blockInit } from '../../helpers/block-utility.js'; +import { _px } from '../../helpers/helper-functions'; const { attributes: defaultAttributes } = metadata; -const px = value => value ? `${ value }px` : value; - /** * * @param {import('./types').CircleCounterPros} param0 @@ -116,7 +115,7 @@ const CircularProgressBarBlock = ({ }, [ attributes.percentage, attributes.height ]); const inlineStyles = { - '--font-size-title': px( attributes.fontSizeTitle ) + '--font-size-title': _px( attributes.fontSizeTitle ) }; const blockProps = useBlockProps({ diff --git a/src/blocks/blocks/countdown/edit.tsx b/src/blocks/blocks/countdown/edit.tsx index b357b1499..66d4c8435 100644 --- a/src/blocks/blocks/countdown/edit.tsx +++ b/src/blocks/blocks/countdown/edit.tsx @@ -37,6 +37,8 @@ import { } from '../../helpers/block-utility'; import Inspector from './inspector.js'; import { + _percent, + _px, boxValues, getTimezone } from '../../helpers/helper-functions.js'; @@ -46,8 +48,6 @@ import { CountdownProps } from './types'; const { attributes: defaultAttributes } = metadata; -const optionalUnit = ( value: unknown, unit = 'px' ) => isNumber( value ) ? `${ value }${unit}` : value; - const Edit = ({ attributes, setAttributes, @@ -71,8 +71,8 @@ const Edit = ({ ) { const borderRadiusBox = pickBy( 'linked' === attributes?.borderRadiusType ? - { left: optionalUnit( attributes.borderRadius, '%' ), right: optionalUnit( attributes.borderRadius, '%' ), bottom: optionalUnit( attributes.borderRadius, '%' ), top: optionalUnit( attributes.borderRadius, '%' ) } : - { left: optionalUnit( attributes.borderRadiusBottomLeft, '%' ), right: optionalUnit( attributes.borderRadiusTopRight, '%' ), bottom: optionalUnit( attributes.borderRadiusBottomRight, '%' ), top: optionalUnit( attributes.borderRadiusTopLeft, '%' ) }, x => x ); + { left: _percent( attributes.borderRadius ), right: _percent( attributes.borderRadius ), bottom: _percent( attributes.borderRadius ), top: _percent( attributes.borderRadius ) } : + { left: _percent( attributes.borderRadiusBottomLeft ), right: _percent( attributes.borderRadiusTopRight ), bottom: _percent( attributes.borderRadiusBottomRight ), top: _percent( attributes.borderRadiusTopLeft ) }, x => x ); if ( ! isEmpty( borderRadiusBox ) ) { setAttributes({ borderRadiusBox, borderRadius: undefined, borderRadiusBottomLeft: undefined, borderRadiusTopRight: undefined, borderRadiusBottomRight: undefined, borderRadiusTopLeft: undefined, borderRadiusType: undefined }); @@ -119,21 +119,21 @@ const Edit = ({ '--container-width': attributes.containerWidth, '--container-width-tablet': attributes.containerWidthTablet, '--container-width-mobile': attributes.containerWidthMobile, - '--height': optionalUnit( attributes.height ), - '--height-tablet': optionalUnit( attributes.heightTablet ), - '--height-mobile': optionalUnit( attributes.heightMobile ), - '--border-width': optionalUnit( attributes.borderWidth ), - '--border-width-tablet': optionalUnit( attributes.borderWidthTablet ), - '--border-width-mobile': optionalUnit( attributes.borderWidthMobile ), - '--gap': optionalUnit( attributes.gap ), - '--gap-tablet': optionalUnit( attributes.gapTablet ), - '--gap-mobile': optionalUnit( attributes.gapMobile ), - '--value-font-size': optionalUnit( attributes.valueFontSize ), - '--value-font-size-tablet': optionalUnit( attributes.valueFontSizeTablet ), - '--value-font-size-mobile': optionalUnit( attributes.valueFontSizeMobile ), - '--label-font-size': optionalUnit( attributes.labelFontSize ), - '--label-font-size-tablet': optionalUnit( attributes.labelFontSizeTablet ), - '--label-font-size-mobile': optionalUnit( attributes.labelFontSizeMobile ), + '--height': _px( attributes.height ), + '--height-tablet': _px( attributes.heightTablet ), + '--height-mobile': _px( attributes.heightMobile ), + '--border-width': _px( attributes.borderWidth ), + '--border-width-tablet': _px( attributes.borderWidthTablet ), + '--border-width-mobile': _px( attributes.borderWidthMobile ), + '--gap': _px( attributes.gap ), + '--gap-tablet': _px( attributes.gapTablet ), + '--gap-mobile': _px( attributes.gapMobile ), + '--value-font-size': _px( attributes.valueFontSize ), + '--value-font-size-tablet': _px( attributes.valueFontSizeTablet ), + '--value-font-size-mobile': _px( attributes.valueFontSizeMobile ), + '--label-font-size': _px( attributes.labelFontSize ), + '--label-font-size-tablet': _px( attributes.labelFontSizeTablet ), + '--label-font-size-mobile': _px( attributes.labelFontSizeMobile ), '--alignment': attributes.alignment, '--padding': boxValues( attributes.padding ), '--padding-tablet': boxValues( attributes.paddingTablet ), diff --git a/src/blocks/blocks/flip/inspector.js b/src/blocks/blocks/flip/inspector.js index 537614d24..0c94b1b88 100644 --- a/src/blocks/blocks/flip/inspector.js +++ b/src/blocks/blocks/flip/inspector.js @@ -60,15 +60,14 @@ import { removeBoxDefaultValues, stringToBox, _i, - _px + _px, + numberToBox } from '../../helpers/helper-functions.js'; import { alignBottom, alignTop, alignCenter as oAlignCenter } from '../../helpers/icons.js'; import { useResponsiveAttributes } from '../../helpers/utility-hooks.js'; -const wrapNumberInBox = ( x ) => isNumber( x ) ? stringToBox( _px( x ) ) : x; - const defaultFontSizes = [ { name: '14', @@ -470,7 +469,7 @@ const Inspector = ({ label={ __( 'Description', 'otter-blocks' ) } > setAttributes({ descriptionFontSize }) } fontSizes={[ ...defaultFontSizes, { name: '28', size: '28px', slug: '28' }]} allowReset @@ -570,7 +569,7 @@ const Inspector = ({ label={ __( 'Border Width', 'otter-blocks' ) } values={ mergeBoxDefaultValues( - wrapNumberInBox( attributes.borderWidth ), + numberToBox( attributes.borderWidth ), stringToBox( '3px' ) ) } @@ -587,7 +586,7 @@ const Inspector = ({ label={ __( 'Border Radius', 'otter-blocks' ) } values={ mergeBoxDefaultValues( - wrapNumberInBox( attributes.borderRadius ), + numberToBox( attributes.borderRadius ), stringToBox( '10px' ) ) } diff --git a/src/blocks/blocks/form/common.tsx b/src/blocks/blocks/form/common.tsx index 5ba88e043..f6b9bbe48 100644 --- a/src/blocks/blocks/form/common.tsx +++ b/src/blocks/blocks/form/common.tsx @@ -1,10 +1,15 @@ -// @ts-nocheck import { __ } from '@wordpress/i18n'; import { __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + ToggleControl } from '@wordpress/components'; -import { changeActiveStyle, getActiveStyle } from '../../helpers/helper-functions'; +import { omit } from 'lodash'; +import { createBlock } from '@wordpress/blocks'; +import { dispatch } from '@wordpress/data'; + +import { BlockProps } from '../../helpers/blocks'; +import { changeActiveStyle, getActiveStyle, getChoice } from '../../helpers/helper-functions'; export const FieldInputWidth = ( props ) => { @@ -51,4 +56,157 @@ export const FieldInputWidth = ( props ) => { ); }; -export default { FieldInputWidth }; +export type FieldOption = { + fieldOptionName: string + fieldOptionType: string + options: { + maxFileSize?: number | string + allowedFileTypes?: string[] + saveFiles?: string + maxFilesNumber?: number + } +} + +export type FormInputCommonProps = { + id: string + label: string + placeholder: string + isRequired: boolean + mappedName: string + labelColor: string + helpText: string +} + +export const fieldTypesOptions = () => ([ + { + label: __( 'Checkbox', 'otter-blocks' ), + value: 'checkbox' + }, + { + label: __( 'Date', 'otter-blocks' ), + value: 'date' + }, + { + label: __( 'Email', 'otter-blocks' ), + value: 'email' + }, + { + label: ( Boolean( window.otterPro?.isActive ) && ! Boolean( window.otterPro?.isExpired ) ) ? __( 'File', 'otter-blocks' ) : __( 'File (Pro)', 'otter-blocks' ), + value: 'file' + }, + { + label: __( 'Number', 'otter-blocks' ), + value: 'number' + }, + { + label: __( 'Radio', 'otter-blocks' ), + value: 'radio' + }, + { + label: __( 'Select', 'otter-blocks' ), + value: 'select' + }, + { + label: __( 'Text', 'otter-blocks' ), + value: 'text' + }, + { + label: __( 'Textarea', 'otter-blocks' ), + value: 'textarea' + }, + { + label: __( 'Url', 'otter-blocks' ), + value: 'url' + } +]); + +export const switchFormFieldTo = ( type?: string, clientId ?:string, attributes?: any ) => { + + if ( ! type || ! clientId || ! attributes ) { + return; + } + + const { replaceBlock } = dispatch( 'core/block-editor' ); + + const blockName = getChoice([ + [ 'textarea' === type, 'form-textarea' ], + [ 'select' === type || 'checkbox' === type || 'radio' === type, 'form-multiple-choice' ], + [ 'file' === type, 'form-file' ], + [ 'form-input' ] + ]); + + + const newBlock = createBlock( `themeisle-blocks/${ blockName }`, + omit({ ...attributes, type: type }, 'form-textarea' === blockName ? [ 'multipleSelection', 'options', 'type' ] : [ 'multipleSelection', 'options' ]) + ); + + replaceBlock( clientId, newBlock ); +}; + +const stylesHide = [ + { + label: '', + value: 'hidden-label' + } +]; + +export const HideFieldLabelToggle = ( props: Partial> ) => { + + const { attributes, setAttributes } = props; + + return ( + + // @ts-ignore + { + const classes = changeActiveStyle( attributes?.className, stylesHide, value ? 'hidden-label' : undefined ); + setAttributes?.({ className: classes }); + } } + /> + ); +}; + +export const hasFormFieldName = ( name?: string ) => ( name?.startsWith( 'themeisle-blocks/form-input' ) || name?.startsWith( 'themeisle-blocks/form-textarea' ) || name?.startsWith( 'themeisle-blocks/form-multiple-choice' ) || name?.startsWith( 'themeisle-blocks/form-file' ) ); + +export const getFormFieldsFromInnerBlock = ( block: any ) : ( any | undefined )[] => { + return block?.innerBlocks?.map( ( child: any ) => { + if ( hasFormFieldName( child?.name ) ) { + return child; + } + + if ( 'themeisle-blocks/form' === child?.name ) { + return undefined; + } + + if ( child?.innerBlocks?.length ) { + return getFormFieldsFromInnerBlock( child )?.flat() as ( string | undefined )[]; + } + + return undefined; + })?.flat(); +}; + +export const selectAllFieldsFromForm = ( children: any[]) : ({ parentClientId: string, inputField: any })[] => { + return ( children?.map( ( child: any ) => { + + if ( hasFormFieldName( child?.name ) ) { + return { parentClientId: child?.clientId, inputField: child }; + } + + if ( 'themeisle-blocks/form' === child?.name ) { + return undefined; + } + + if ( child?.innerBlocks?.length ) { + return getFormFieldsFromInnerBlock( child ) + .filter( i => i !== undefined ) + .map( ( input: any ) => ({ parentClientId: child?.clientId, inputField: input }) ); + } + + return undefined; + }).flat().filter( c => c !== undefined ) ?? []) as ({ parentClientId: string, inputField: any })[]; +}; + +export default { switchFormFieldTo, HideFieldLabelToggle, FieldInputWidth }; diff --git a/src/blocks/blocks/form/edit.js b/src/blocks/blocks/form/edit.js index dbfda74dc..08710f29e 100644 --- a/src/blocks/blocks/form/edit.js +++ b/src/blocks/blocks/form/edit.js @@ -52,7 +52,9 @@ import { import Inspector from './inspector.js'; import Placeholder from './placeholder.js'; import { useResponsiveAttributes } from '../../helpers/utility-hooks'; -import { renderBoxOrNumWithUnit, _cssBlock, _px } from '../../helpers/helper-functions'; +import { renderBoxOrNumWithUnit, _cssBlock, _px, findInnerBlocks } from '../../helpers/helper-functions'; +import { Notice } from '@wordpress/components'; + const { attributes: defaultAttributes } = metadata; export const FormContext = createContext({}); @@ -66,7 +68,8 @@ const formOptionsMap = { fromName: 'fromName', cc: 'cc', bcc: 'bcc', - autoresponder: 'autoresponder' + autoresponder: 'autoresponder', + submissionsSaveLocation: 'submissionsSaveLocation' }; /** @@ -95,10 +98,7 @@ const Edit = ({ serviceTesting: 'init' }); - const [ optionsHaveChanged, setOptionsHaveChanged ] = useState( false ); - const setLoading = l => { - setOptionsHaveChanged( true ); setLoadingState( loading => ({ ...loading, ...l }) ); }; @@ -114,6 +114,8 @@ const Edit = ({ return attributes?.[field]; }; + + /** @type {[import('./type').FormOptions, React.Dispatch>]} */ const [ formOptions, setFormOptions ] = useState({ provider: undefined, redirectLink: undefined, @@ -129,7 +131,8 @@ const Edit = ({ apiKey: undefined, cc: undefined, bcc: undefined, - autoresponder: undefined + autoresponder: undefined, + submissionsSaveLocation: undefined }); const { @@ -140,11 +143,21 @@ const Edit = ({ moveBlockToPosition } = useDispatch( 'core/block-editor' ); + const { + unlockPostSaving + } = useDispatch( 'core/editor' ); + const setFormOption = option => { setFormOptions( options => ({ ...options, ...option }) ); }; + const setFormOptionAndSaveUnlock = option => { + setFormOption( option ); + unlockPostSaving?.(); + }; + const [ savedFormOptions, setSavedFormOptions ] = useState( true ); + const [ showAutoResponderNotice, setShowAutoResponderNotice ] = useState( false ); const [ listIDOptions, setListIDOptions ] = useState([{ label: __( 'None', 'otter-blocks' ), value: '' }]); @@ -173,34 +186,37 @@ const Edit = ({ [ name ] ); - const { children, hasEmailField, hasProtection } = useSelect( select => { + const [ hasEmailField, setHasEmailField ] = useState( false ); + + const { children, hasProtection } = useSelect( select => { const { getBlock } = select( 'core/block-editor' ); const children = getBlock( clientId ).innerBlocks; return { children, - hasEmailField: children?.some( b => ( 'email' === b?.attributes?.type ) ), hasProtection: 0 < children?.filter( ({ name }) => 'themeisle-blocks/form-nonce' === name )?.length }; }); const { canSaveData } = useSelect( select => { const isSavingPost = select( 'core/editor' )?.isSavingPost(); + const isPublishingPost = select( 'core/editor' )?.isPublishingPost(); const isAutosaving = select( 'core/editor' )?.isAutosavingPost(); + const widgetSaving = select( 'core/edit-widgets' )?.isSavingWidgetAreas(); return { - canSaveData: ! isAutosaving && isSavingPost + canSaveData: ( ! isAutosaving && ( isSavingPost || isPublishingPost ) ) || widgetSaving }; }); const hasEssentialData = attributes.optionName && hasProtection; useEffect( () => { - if ( canSaveData && optionsHaveChanged ) { + if ( canSaveData ) { saveFormEmailOptions(); } - }, [ canSaveData, optionsHaveChanged ]); + }, [ canSaveData ]); useEffect( () => { const unsubscribe = blockInit( clientId, defaultAttributes ); @@ -236,7 +252,27 @@ const Edit = ({ } } } - }, [ children ]); + + if ( formOptions.autoresponder || formOptions.action ) { + const emailFields = findInnerBlocks( + children, + block => { + return 'email' === block?.attributes?.type && 'themeisle-blocks/form-input' === block?.name; + }, + block => { + + // Do not find email field inside inner Form blocks. + return 'themeisle-blocks/form' !== block?.name; + } + ); + + + setHasEmailField( 0 < emailFields?.length ); + + setShowAutoResponderNotice( 0 === emailFields?.length ); + } + + }, [ children, formOptions.autoresponder, formOptions.action ]); /** * Get the data from the WP Options for the current form. @@ -265,11 +301,13 @@ const Edit = ({ listId: wpOptions?.integration?.listId, action: wpOptions?.integration?.action, hasCaptcha: wpOptions?.hasCaptcha, - autoresponder: wpOptions?.autoresponder + autoresponder: wpOptions?.autoresponder, + autoresponderSubject: wpOptions?.autoresponderSubject, + submissionsSaveLocation: wpOptions?.submissionsSaveLocation }); }; - /** + /**` * Load data from the server. */ useEffect( () => { @@ -278,30 +316,34 @@ const Edit = ({ setLoading({ formOptions: 'done', formIntegration: 'done' }); }, 3000 ); - if ( attributes.optionName ) { - api.loadPromise.then( () => { - setLoading({ formOptions: 'loading', formIntegration: 'loading' }); - ( new api.models.Settings() ).fetch({ signal: controller.signal }).done( res => { - controller = null; - const formData = extractDataFromWpOptions( res.themeisle_blocks_form_emails ); - if ( formData ) { - parseDataFormOptions( formData ); - setSavedFormOptions( formData ); - } - setLoading({ - formIntegration: 'done', - formOptions: 'done' - }); - clearTimeout( t ); - }).catch( () => { - setLoading({ - formIntegration: 'done', - formOptions: 'done' + try { + api.loadPromise.then( () => { + setLoading({ formOptions: 'loading', formIntegration: 'loading' }); + ( new api.models.Settings() ).fetch({ signal: controller.signal }).done( res => { + controller = null; + const formData = extractDataFromWpOptions( res.themeisle_blocks_form_emails ); + if ( formData ) { + parseDataFormOptions( formData ); + setSavedFormOptions( formData ); + } + setLoading({ + formIntegration: 'done', + formOptions: 'done' + }); + clearTimeout( t ); + }).catch( () => { + setLoading({ + formIntegration: 'done', + formOptions: 'done' + }); + clearTimeout( t ); }); - clearTimeout( t ); }); - }); + } catch ( e ) { + console.error( e ); + setLoading({ formOptions: 'error' }); + } } return () => { @@ -312,64 +354,73 @@ const Edit = ({ const saveFormEmailOptions = () => { setLoading({ formOptions: 'saving' }); - ( new api.models.Settings() ).fetch().done( res => { - const emails = res.themeisle_blocks_form_emails ? res.themeisle_blocks_form_emails : []; - let isMissing = true; - let hasUpdated = false; + try { + ( new api.models.Settings() ).fetch().done( res => { + const emails = res.themeisle_blocks_form_emails ? res.themeisle_blocks_form_emails : []; + let isMissing = true; + let hasUpdated = false; + + emails?.forEach( ({ form }, index ) => { + if ( form !== attributes.optionName ) { + return; + } - emails?.forEach( ({ form }, index ) => { - if ( form !== attributes.optionName ) { - return; - } + hasUpdated = Object.keys( formOptionsMap ).reduce( ( acc, key ) => { + return acc || ! isEqual( emails[index][key], formOptions[formOptionsMap[key]]); + }, false ); - hasUpdated = Object.keys( formOptionsMap ).reduce( ( acc, key ) => { - return acc || ! isEqual( emails[index][key], formOptions[formOptionsMap[key]]); - }, false ); + hasUpdated = Object.keys( formOptionsMap ).some( key => ! isEqual( emails[index][key], formOptions[formOptionsMap[key]]) ); - // Update the values - Object.keys( formOptionsMap ).forEach( key => emails[index][key] = formOptions[formOptionsMap[key]]); + // Update the values + if ( hasUpdated ) { + Object.keys( formOptionsMap ).forEach( key => emails[index][key] = formOptions[formOptionsMap[key]]); + } - isMissing = false; - }); + isMissing = false; + }); - if ( isMissing ) { - const data = { form: attributes.optionName }; + if ( isMissing ) { + const data = { form: attributes.optionName }; - Object.keys( formOptionsMap ).forEach( key => { - data[key] = formOptions[formOptionsMap[key]]; - }); + Object.keys( formOptionsMap ).forEach( key => { + data[key] = formOptions[formOptionsMap[key]]; + }); - emails.push( data ); - } + emails.push( data ); + } - if ( isMissing || hasUpdated ) { - const model = new api.models.Settings({ - // eslint-disable-next-line camelcase - themeisle_blocks_form_emails: emails - }); + if ( isMissing || hasUpdated ) { + const model = new api.models.Settings({ + // eslint-disable-next-line camelcase + themeisle_blocks_form_emails: emails + }); - model.save().then( response => { - const formOptions = extractDataFromWpOptions( response.themeisle_blocks_form_emails ); - if ( formOptions ) { - parseDataFormOptions( formOptions ); - setSavedFormOptions( formOptions ); - setLoading({ formOptions: 'done' }); - createNotice( - 'info', - __( 'Form options have been saved.', 'otter-blocks' ), - { - isDismissible: true, - type: 'snackbar' - } - ); - } else { - setLoading({ formOptions: 'error' }); - } - }); - } else { - setLoading({ formOptions: 'done' }); - } - }).catch( () => setLoading({ formOptions: 'error' }) ); + model.save().then( response => { + const formOptions = extractDataFromWpOptions( response.themeisle_blocks_form_emails ); + if ( formOptions ) { + parseDataFormOptions( formOptions ); + setSavedFormOptions( formOptions ); + setLoading({ formOptions: 'done' }); + createNotice( + 'info', + __( 'Form options have been saved.', 'otter-blocks' ), + { + isDismissible: true, + type: 'snackbar' + } + ); + } else { + setLoading({ formOptions: 'error' }); + } + }); + } else { + setLoading({ formOptions: 'done' }); + } + }).catch( () => setLoading({ formOptions: 'error' }) ); + } catch ( e ) { + console.error( e ); + setLoading({ formOptions: 'error' }); + } }; /** @@ -382,6 +433,7 @@ const Edit = ({ let isMissing = true; let hasUpdated = false; + emails?.forEach( ({ form }, index ) => { if ( form === attributes.optionName ) { if ( ! emails[index]?.integration ) { @@ -414,6 +466,7 @@ const Edit = ({ }); } + if ( isMissing || hasUpdated ) { const model = new api.models.Settings({ // eslint-disable-next-line camelcase @@ -801,8 +854,7 @@ const Edit = ({ const inputFieldActions = { select: ( blockId ) => { if ( 0 < children?.length ) { - const block = children.find( block => block.clientId === blockId ); - selectBlock( block.clientId ); + selectBlock( blockId ); } }, move: ( blockId, position ) => { @@ -813,8 +865,7 @@ const Edit = ({ }, delete: ( blockId ) => { if ( 0 < children?.length ) { - const block = children.find( block => block.clientId === blockId ); - removeBlock( block.clientId, false ); + removeBlock( blockId, false ); } }, add: ( blockName ) => { @@ -832,7 +883,7 @@ const Edit = ({ setListIDOptions, saveFormEmailOptions, formOptions, - setFormOption, + setFormOption: setFormOptionAndSaveUnlock, saveIntegration, sendTestEmail, loadingState, @@ -840,7 +891,9 @@ const Edit = ({ hasEmailField, children, inputFieldActions, - hasInnerBlocks + hasInnerBlocks, + selectForm: () => selectBlock( clientId ), + showAutoResponderNotice }} > - + + { attributes.hasCaptcha && 'done' !== loadingState?.captcha && ( @@ -952,6 +1005,7 @@ const Edit = ({ } ) } + ) : ( diff --git a/src/blocks/blocks/form/editor.scss b/src/blocks/blocks/form/editor.scss index 54b6b2792..19bfc062c 100644 --- a/src/blocks/blocks/form/editor.scss +++ b/src/blocks/blocks/form/editor.scss @@ -35,6 +35,7 @@ margin: 8px 0; border-left: 3px solid orange; padding-left: 18px; + grid-column: span 2; &.o-error { border-color: red; diff --git a/src/blocks/blocks/form/file/block.json b/src/blocks/blocks/form/file/block.json new file mode 100644 index 000000000..a83f8c0e0 --- /dev/null +++ b/src/blocks/blocks/form/file/block.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "themeisle-blocks/form-file", + "title": "File Field", + "category": "themeisle-blocks", + "description": "Display a contact form for your clients.", + "keywords": [ "input", "file", "field" ], + "ancestor": [ "themeisle-blocks/form" ], + "textdomain": "otter-blocks", + "attributes": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "default": "file" + }, + "label": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "isRequired": { + "type": "boolean" + }, + "mappedName": { + "type": "string" + }, + "labelColor": { + "type": "string" + }, + "inputWidth": { + "type": "number" + }, + "helpText": { + "type": "string" + }, + "maxFileSize": { + "type": "string" + }, + "allowedFileTypes": { + "type": "array", + "default": [ "image/*", "audio/*", "video/*" ] + }, + "saveFiles": { + "type": "string", + "default": "media-library" + }, + "multipleFiles": { + "type": "boolean" + }, + "fieldOptionName": { + "type": "string" + }, + "maxFilesNumber": { + "type": "string" + } + }, + "supports": { + "align": [ "wide", "full" ] + } +} diff --git a/src/blocks/blocks/form/file/index.js b/src/blocks/blocks/form/file/index.js new file mode 100644 index 000000000..8557054c8 --- /dev/null +++ b/src/blocks/blocks/form/file/index.js @@ -0,0 +1,78 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { registerBlockType } from '@wordpress/blocks'; + +import { createBlock } from '@wordpress/blocks'; + +import { omit } from 'lodash'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { formFieldIcon as icon } from '../../../helpers/icons.js'; +import Inspector from './inspector'; + +const { name } = metadata; + +if ( ! window.themeisleGutenberg.isAncestorTypeAvailable ) { + metadata.parent = [ 'themeisle-blocks/form' ]; +} + +if ( ! Boolean( window.themeisleGutenberg.hasPro ) ) { + registerBlockType( name, { + ...metadata, + title: __( 'File Field (Pro)', 'otter-blocks' ), + description: __( 'Display a file field for uploading.', 'otter-blocks' ), + icon, + keywords: [ + 'input', + 'field', + 'file' + ], + edit: ( props ) => { + return ( + + ); + }, + save: () => null, + transforms: { + to: [ + { + type: 'block', + blocks: [ 'themeisle-blocks/form-input' ], + transform: ( attributes ) => { + + return createBlock( 'themeisle-blocks/form-input', { + ...attributes + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-textarea' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-textarea', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-multiple-choice' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-multiple-choice', { + ...attrs + }); + } + } + ] + } + }); +} + diff --git a/src/blocks/blocks/form/file/inspector.js b/src/blocks/blocks/form/file/inspector.js new file mode 100644 index 000000000..05262b43e --- /dev/null +++ b/src/blocks/blocks/form/file/inspector.js @@ -0,0 +1,163 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + InspectorControls, + PanelColorSettings +} from '@wordpress/block-editor'; + +import { + Button, + Disabled, + ExternalLink, + FormTokenField, + PanelBody, + SelectControl, + TextControl, + ToggleControl +} from '@wordpress/components'; +import { applyFilters } from '@wordpress/hooks'; +import { Fragment, useContext } from '@wordpress/element'; + +import { fieldTypesOptions, HideFieldLabelToggle, switchFormFieldTo } from '../common'; +import { FormContext } from '../edit'; +import { setUtm } from '../../../helpers/helper-functions'; +import { Notice } from '../../../components'; + +const ProPreview = ({ attributes }) => { + + return ( + + + + {} } + /> + + {} } /> + + {} } + help={ __( 'You may need to contact your hosting provider to increase file sizes.', 'otter-blocks' ) } + /> + + {} } + help={ __( 'Add the allowed files types that can be loaded. E.g.: jpg, zip, pdf.', 'otter-blocks' ) } + /> + + {} } + /> + + {} } + /> + + {} } + /> + + { + attributes.multipleFiles && ( + {} } + /> + ) + } + + {} } + /> + + + { ! Boolean( window.themeisleGutenberg.hasPro ) && ( + { __( 'Get more options with Otter Pro. ', 'otter-blocks' ) } } + variant="upsell" + /> ) } + + ); +}; + +/** + * + * @param {import('./types').FormFileInspectorProps} props + * @returns {JSX.Element} + */ +const Inspector = ({ + attributes, + setAttributes, + clientId +}) => { + + const { + selectForm + } = useContext( FormContext ); + + return ( + + + + + { + if ( 'file' !== type ) { + switchFormFieldTo( type, clientId, attributes ); + } + }} + /> + + + + + + {}, + label: __( 'Label Color', 'otter-blocks' ) + } + ] } + /> + + ); +}; + +export default Inspector; diff --git a/src/blocks/blocks/form/index.js b/src/blocks/blocks/form/index.js index 130d8d9b5..15fd42e73 100644 --- a/src/blocks/blocks/form/index.js +++ b/src/blocks/blocks/form/index.js @@ -16,6 +16,8 @@ import save from './save.js'; import './input/index.js'; import './nonce/index.js'; import './textarea/index.js'; +import './multiple-choice/index.js'; +import './file/index.js'; const { name } = metadata; diff --git a/src/blocks/blocks/form/input/edit.js b/src/blocks/blocks/form/input/edit.js index f31e41947..2446d97ab 100644 --- a/src/blocks/blocks/form/input/edit.js +++ b/src/blocks/blocks/form/input/edit.js @@ -10,23 +10,16 @@ import { import { Fragment, - useEffect, - useRef + useEffect } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; - -import { omit } from 'lodash'; - -import { createBlock } from '@wordpress/blocks'; - /** * Internal dependencies */ import metadata from './block.json'; import { blockInit } from '../../../helpers/block-utility.js'; import Inspector from './inspector.js'; - +import { _cssBlock } from '../../../helpers/helper-functions'; const { attributes: defaultAttributes } = metadata; @@ -47,69 +40,23 @@ const Edit = ({ const blockProps = useBlockProps(); - const labelRef = useRef( null ); - const inputRef = useRef( null ); - const helpRef = useRef( null ); - - const { - parentClientId - } = useSelect( select => { - const { - getBlock, - getBlockRootClientId - } = select( 'core/block-editor' ); - - if ( ! clientId ) { - return { - parentClientId: '' - }; - } - - const parentClientId = getBlockRootClientId( clientId ); - - return { - parentClientId: parentClientId ?? '' - }; - }, [ clientId ]); - - const { selectBlock, replaceBlock } = useDispatch( 'core/block-editor' ); - - const switchToTextarea = () => { - const block = createBlock( 'themeisle-blocks/form-textarea', { ...omit( attributes, [ 'type' ]) }); - replaceBlock( clientId, block ); - }; - - - useEffect( () => { - const per = x => x ? x + '%' : null; - - /** - * TODO: Refactor this based on #748 - */ - - if ( inputRef.current ) { - inputRef.current?.style?.setProperty( '--input-width', per( attributes.inputWidth ) ); - } - if ( labelRef.current ) { - labelRef.current?.style?.setProperty( '--label-color', attributes.labelColor || null ); - } - if ( helpRef.current ) { - helpRef.current?.style?.setProperty( '--label-color', attributes.labelColor || null ); - } - }, [ inputRef.current, labelRef.current, helpRef.current, attributes.labelColor, attributes.inputWidth ]); - return ( selectBlock( parentClientId ) } - switchToTextarea={ switchToTextarea } + clientId={ clientId } />
+ { attributes.helpText } diff --git a/src/blocks/blocks/form/input/index.js b/src/blocks/blocks/form/input/index.js index 21a3832d4..823cbda43 100644 --- a/src/blocks/blocks/form/input/index.js +++ b/src/blocks/blocks/form/input/index.js @@ -39,28 +39,38 @@ registerBlockType( name, { { name: 'themeisle-blocks/form-input-email', description: __( 'Insert an email field', 'otter-blocks' ), - icon: 'email', + icon: icon, title: __( 'Email Field', 'otter-blocks' ), attributes: { type: 'email' } }, + { + name: 'themeisle-blocks/form-input-date', + description: __( 'Insert a date field', 'otter-blocks' ), + icon: icon, + title: __( 'Date Field', 'otter-blocks' ), + attributes: { + type: 'date' + } + }, { name: 'themeisle-blocks/form-input-number', description: __( 'Insert a number field', 'otter-blocks' ), - icon: 'calculator', + icon: icon, title: __( 'Number Field', 'otter-blocks' ), attributes: { type: 'number' } }, { - name: 'themeisle-blocks/form-input-date', - description: __( 'Insert a date field', 'otter-blocks' ), - icon: 'calendar-alt', - title: __( 'Date Field', 'otter-blocks' ), + name: 'themeisle-blocks/form-input-url', + description: __( 'Insert an URL field', 'otter-blocks' ), + icon: icon, + title: __( 'URL Field', 'otter-blocks' ), attributes: { - type: 'date' + type: 'url', + placeholder: 'https://' } } ], @@ -77,6 +87,26 @@ registerBlockType( name, { ...attrs }); } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-multiple-choice' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-multiple-choice', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-file' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-file', { + ...attrs + }); + } } ] } diff --git a/src/blocks/blocks/form/input/inspector.js b/src/blocks/blocks/form/input/inspector.js index a80f1aee0..bfd1a4dc7 100644 --- a/src/blocks/blocks/form/input/inspector.js +++ b/src/blocks/blocks/form/input/inspector.js @@ -15,7 +15,9 @@ import { TextControl, ToggleControl } from '@wordpress/components'; -import { FieldInputWidth } from '../common'; +import { FieldInputWidth, fieldTypesOptions, HideFieldLabelToggle, switchFormFieldTo } from '../common'; +import { useContext } from '@wordpress/element'; +import { FormContext } from '../edit'; /** * @@ -25,9 +27,13 @@ import { FieldInputWidth } from '../common'; const Inspector = ({ attributes, setAttributes, - selectParent, - switchToTextarea + clientId }) => { + + const { + selectForm + } = useContext( FormContext ); + return ( selectParent?.() } + onClick={ () => selectForm?.() } > { __( 'Back to the Form', 'otter-blocks' ) } @@ -44,33 +50,13 @@ const Inspector = ({ { - if ( 'textarea' === type ) { - switchToTextarea?.(); + if ( 'textarea' === type || 'radio' === type || 'checkbox' === type || 'select' === type || 'file' === type ) { + switchFormFieldTo( type, clientId, attributes ); return; } + setAttributes({ type }); }} /> @@ -81,13 +67,19 @@ const Inspector = ({ onChange={ label => setAttributes({ label }) } /> + + - setAttributes({ placeholder }) } - /> + { + ( 'date' !== attributes.type || undefined === attributes.type ) && ( + setAttributes({ placeholder }) } + /> + ) + } diff --git a/src/blocks/blocks/form/inspector.js b/src/blocks/blocks/form/inspector.js index 5b0aa53ce..198605425 100644 --- a/src/blocks/blocks/form/inspector.js +++ b/src/blocks/blocks/form/inspector.js @@ -65,6 +65,7 @@ import { makeBox } from '../../plugins/copy-paste/utils'; import { _px, setUtm } from '../../helpers/helper-functions.js'; import { SortableInputField } from './sortable-input-fields'; import AutoDisableSyncAttr from '../../components/auto-disable-sync-attr/index'; +import { selectAllFieldsFromForm } from './common'; const compare = x => { return x?.[1] && x[0] !== x[1]; @@ -227,42 +228,73 @@ const FormOptions = ({ formOptions, setFormOption, attributes, setAttributes }) /> + { ! Boolean( window.themeisleGutenberg?.hasPro ) && ( - false } - label={ __( 'Autoresponder (Pro)', 'otter-blocks' ) } - onDeselect={ () => {} } - > - {} } - help={ __( 'Enter the subject of the autoresponder email.', 'otter-blocks' ) } - disabled - className="o-disabled" - /> - - {} } - help={ __( 'Enter the body of the autoresponder email.', 'otter-blocks' ) } - disabled - className="o-disabled" - /> - -
- { __( 'Unlock this with Otter Pro.', 'otter-blocks' ) } } - variant="upsell" + + undefined !== formOptions.submissionsSaveLocation } + label={ __( 'Submissions', 'otter-blocks' ) } + onDeselect={ () => setFormOption({ submissionsSaveLocation: undefined }) } + isShownByDefault={ true } + > + {} } + options={ + [ + { label: __( 'Database (Pro)', 'otter-blocks' ), value: 'database' }, + { label: __( 'Email Only', 'otter-blocks' ), value: 'email' }, + { label: __( 'Database and Email (Pro)', 'otter-blocks' ), value: 'database-email' } + ] + } + help={ __( 'The submissions are send only via email. No data will be saved on the server, use this option to handle sensitive data.', 'otter-blocks' ) } + /> + +
+ { __( 'Unlock this with Otter Pro.', 'otter-blocks' ) } } + variant="upsell" + /> +

{ __( 'Enhance your email process with our new feature. Store submissions in a database for easy access.', 'otter-blocks' ) }

+
+
+ false } + label={ __( 'Autoresponder (Pro)', 'otter-blocks' ) } + onDeselect={ () => {} } + > + {} } + help={ __( 'Enter the subject of the autoresponder email.', 'otter-blocks' ) } + disabled + className="o-disabled" + /> + + {} } + help={ __( 'Enter the body of the autoresponder email.', 'otter-blocks' ) } + disabled + className="o-disabled" /> -

{ __( 'Automatically send follow-up emails to your users with the Autoresponder feature.', 'otter-blocks' ) }

-
-
+
+ { __( 'Unlock this with Otter Pro.', 'otter-blocks' ) } } + variant="upsell" + /> +

{ __( 'Automatically send follow-up emails to your users with the Autoresponder feature.', 'otter-blocks' ) }

+
+ + + ) } ); @@ -302,9 +334,7 @@ const Inspector = ({ hasInnerBlocks } = useContext( FormContext ); - const inputFields = children.filter( inputField => { - return 'themeisle-blocks/form-input' === inputField.name || 'themeisle-blocks/form-textarea' === inputField.name; - }); + const inputFields = selectAllFieldsFromForm( children ); const formIntegrationChanged = isChanged([ [ formOptions.provider, savedFormOptions?.integration?.provider ], @@ -315,12 +345,12 @@ const Inspector = ({ const InputFieldList = SortableContainer( ({ items }) => { return (
- { items.map( ( inputField, index ) => { + { items.map( ( item, index ) => { return ( ); @@ -330,7 +360,7 @@ const Inspector = ({ }); const onSortEnd = ({ oldIndex, newIndex }) => { - inputFieldActions.move( inputFields[oldIndex].clientId, newIndex ); + inputFieldActions.move( inputFields?.[oldIndex]?.parentClientId, newIndex ); }; return ( @@ -406,7 +436,8 @@ const Inspector = ({ setAttributes={setAttributes} />, formOptions, - setFormOption + setFormOption, + useContext( FormContext ) ) } diff --git a/src/blocks/blocks/form/multiple-choice/block.json b/src/blocks/blocks/form/multiple-choice/block.json new file mode 100644 index 000000000..e8c6b1705 --- /dev/null +++ b/src/blocks/blocks/form/multiple-choice/block.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "themeisle-blocks/form-multiple-choice", + "title": "Form Multiple Choice Input", + "category": "themeisle-blocks", + "description": "Display a checkbox, select, radio.", + "keywords": [ "form", "checkbox", "select", "radio", "multiple" ], + "textdomain": "otter-blocks", + "ancestor": [ "themeisle-blocks/form" ], + "attributes": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "default": "checkbox" + }, + "label": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "isRequired": { + "type": "boolean" + }, + "mappedName": { + "type": "string" + }, + "labelColor": { + "type": "string" + }, + "inputWidth": { + "type": "object" + }, + "helpText": { + "type": "string" + }, + "options": { + "type": "string" + }, + "multipleSelection": { + "type": "boolean" + } + }, + "supports": { + "align": [ "wide", "full" ] + } +} diff --git a/src/blocks/blocks/form/multiple-choice/edit.js b/src/blocks/blocks/form/multiple-choice/edit.js new file mode 100644 index 000000000..6b1ce49d0 --- /dev/null +++ b/src/blocks/blocks/form/multiple-choice/edit.js @@ -0,0 +1,160 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + RichText, + useBlockProps +} from '@wordpress/block-editor'; + +import { + Fragment, + useEffect +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { blockInit } from '../../../helpers/block-utility.js'; +import Inspector from './inspector.js'; +import { _cssBlock } from '../../../helpers/helper-functions'; +import { Disabled } from '@wordpress/components'; + + +const { attributes: defaultAttributes } = metadata; + +const Field = ({ fieldType, label, setAttributes, position, attributes }) => { + + const id = `${attributes.id ?? ''}-field-${position}`; + const value = label?.toLowerCase().replace( / /g, '_' ); + + const onChangeLabel = label => { + const options = attributes.options?.split( '\n' ) ?? []; + if ( options.length < position ) { + return; + } + + options[ position ] = label; + setAttributes({ options: options.join( '\n' ) }); + }; + + return ( +
+ + + + +
+ ); +}; + +const SelectField = ({ attributes }) => { + return ( + + ); +}; + +/** + * Form Input component + * @param {import('./types').FormMultipleChoiceInputProps} props + * @returns + */ +const Edit = ({ + attributes, + setAttributes, + clientId +}) => { + useEffect( () => { + const unsubscribe = blockInit( clientId, defaultAttributes ); + return () => unsubscribe( attributes.id ); + }, [ attributes.id ]); + + const blockProps = useBlockProps(); + + return ( + + + +
+ + + + { + 'select' === attributes?.type ? : ( +
+ { + ( attributes?.options ?? '' )?.split( '\n' )?.map( ( label, index ) => { + return ; + }) + } +
+ ) + } + + { + attributes.helpText && ( + + { attributes.helpText } + + ) + } +
+
+ ); +}; + +export default Edit; diff --git a/src/blocks/blocks/form/multiple-choice/index.js b/src/blocks/blocks/form/multiple-choice/index.js new file mode 100644 index 000000000..2829b1a20 --- /dev/null +++ b/src/blocks/blocks/form/multiple-choice/index.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { registerBlockType } from '@wordpress/blocks'; + +import { createBlock } from '@wordpress/blocks'; + +import { omit } from 'lodash'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import { formFieldIcon as icon } from '../../../helpers/icons.js'; +import edit from './edit.js'; + +const { name } = metadata; + +if ( ! window.themeisleGutenberg.isAncestorTypeAvailable ) { + metadata.parent = [ 'themeisle-blocks/form' ]; +} + +registerBlockType( name, { + ...metadata, + title: __( 'Multiple Choice Field', 'otter-blocks' ), + description: __( 'Display a checkbox or radio list.', 'otter-blocks' ), + icon, + variations: [ + { + name: 'themeisle-blocks/form-input-checkbox', + description: __( 'Insert a checkbox list field', 'otter-blocks' ), + icon: icon, + title: __( 'Checkbox Field', 'otter-blocks' ), + attributes: { + type: 'checkbox' + } + }, + { + name: 'themeisle-blocks/form-input-radio', + description: __( 'Insert a radio list field', 'otter-blocks' ), + icon: icon, + title: __( 'Radio Field', 'otter-blocks' ), + attributes: { + type: 'radio' + } + }, + { + name: 'themeisle-blocks/form-input-select', + description: __( 'Insert a select field', 'otter-blocks' ), + icon: icon, + title: __( 'Select Field', 'otter-blocks' ), + attributes: { + type: 'select' + } + } + ], + edit, + save: () => null, + transforms: { + to: [ + { + type: 'block', + blocks: [ 'themeisle-blocks/form-input' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type', 'multipleChoice', 'options' ]); + return createBlock( 'themeisle-blocks/form-input', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-textarea' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type', 'multipleChoice', 'options' ]); + return createBlock( 'themeisle-blocks/form-textarea', { + ...attrs + }); + } + }, + { + type: 'block', + blocks: [ 'themeisle-blocks/form-file' ], + transform: ( attributes ) => { + const attrs = omit( attributes, [ 'type' ]); + return createBlock( 'themeisle-blocks/form-file', { + ...attrs + }); + } + } + ] + } +}); diff --git a/src/blocks/blocks/form/multiple-choice/inspector.js b/src/blocks/blocks/form/multiple-choice/inspector.js new file mode 100644 index 000000000..b7d130482 --- /dev/null +++ b/src/blocks/blocks/form/multiple-choice/inspector.js @@ -0,0 +1,140 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + InspectorControls, + PanelColorSettings +} from '@wordpress/block-editor'; + +import { + Button, + PanelBody, + SelectControl, + TextareaControl, + TextControl, + ToggleControl +} from '@wordpress/components'; + +import { getActiveStyle, changeActiveStyle } from '../../../helpers/helper-functions.js'; +import { fieldTypesOptions, HideFieldLabelToggle, switchFormFieldTo } from '../common'; +import { useContext } from '@wordpress/element'; +import { FormContext } from '../edit.js'; + +const styles = [ + { + label: __( 'Inline List', 'otter-blocks' ), + value: 'inline-list' + } +]; + +/** + * + * @param {import('./types').FormMultipleChoiceInputInspectorProps} props + * @returns {JSX.Element} + */ +const Inspector = ({ + attributes, + setAttributes, + clientId +}) => { + + const { + selectForm + } = useContext( FormContext ); + + return ( + + + + + { + if ( 'radio' === type || 'checkbox' === type || 'select' === type ) { + setAttributes({ type }); + return; + } + switchFormFieldTo( type, clientId, attributes ); + }} + /> + + setAttributes({ label }) } + /> + + + + 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 }) } + /> + + + setAttributes({ labelColor }), + label: __( 'Label Color', 'otter-blocks' ) + } + ] } + /> + + ); +}; + +export default Inspector; diff --git a/src/blocks/blocks/form/multiple-choice/save.js b/src/blocks/blocks/form/multiple-choice/save.js new file mode 100644 index 000000000..e13c26a26 --- /dev/null +++ b/src/blocks/blocks/form/multiple-choice/save.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + RichText, + useBlockProps +} from '@wordpress/block-editor'; + +const Save = ({ + attributes +}) => { + const blockProps = useBlockProps.save({ + id: attributes.id + }); + + return ( +
+ +
+ ); +}; + +export default Save; diff --git a/src/blocks/blocks/form/multiple-choice/types.d.ts b/src/blocks/blocks/form/multiple-choice/types.d.ts new file mode 100644 index 000000000..335bc706a --- /dev/null +++ b/src/blocks/blocks/form/multiple-choice/types.d.ts @@ -0,0 +1,15 @@ +import { + BlockProps, + InspectorProps +} from '../../../helpers/blocks'; +import { FormInputCommonProps } from '../common'; + +type Attributes = FormInputCommonProps & { + type: string + inputWidth: number + options: string, + multipleSelection: boolean, +} + +export type FormMultipleChoiceInputProps = BlockProps +export interface FormMultipleChoiceInputInspectorProps extends InspectorProps {} diff --git a/src/blocks/blocks/form/sortable-input-fields.tsx b/src/blocks/blocks/form/sortable-input-fields.tsx index 231df0a61..57d8c1454 100644 --- a/src/blocks/blocks/form/sortable-input-fields.tsx +++ b/src/blocks/blocks/form/sortable-input-fields.tsx @@ -23,7 +23,7 @@ const DragHandle = SortableHandle( () => { }); type SortableTabProps = { - inputField: FormInputProps + item: { parentClientId: string, inputField: FormInputProps} actions: { select?: ( clientId: string ) => void delete?: ( clientId: string ) => void @@ -35,11 +35,31 @@ const fieldNames: Record = { 'email': __( 'Email Field', 'otter-blocks' ), 'date': __( 'Date Field', 'otter-blocks' ), 'number': __( 'Number Field', 'otter-blocks' ), - 'textarea': __( 'Textarea Field', 'otter-blocks' ) + 'textarea': __( 'Textarea Field', 'otter-blocks' ), + 'select': __( 'Select Field', 'otter-blocks' ), + 'checkbox': __( 'Checkbox Field', 'otter-blocks' ), + 'radio': __( 'Radio Field', 'otter-blocks' ), + 'file': __( 'File Field', 'otter-blocks' ), + 'url': __( 'URL Field', 'otter-blocks' ) }; -export const SortableInputField = SortableElement( ({ inputField, actions } : SortableTabProps ) => { - const fieldName = 'themeisle-blocks/form-input' === inputField.name ? ( inputField.attributes.type ?? 'text' ) : 'textarea'; +const extractFieldName = ( input: FormInputProps ) => { + const tag = input?.name?.replace( 'themeisle-blocks/', '' ); + + if ( 'form-input' === tag || 'form-multiple-choice' === tag ) { + return input.attributes.type ?? 'text'; + } + + if ( 'form-file' === tag ) { + return 'file'; + } + + return 'textarea'; +}; + +export const SortableInputField = SortableElement( ({ item, actions } : SortableTabProps ) => { + const { inputField } = item; + const fieldName = extractFieldName( inputField ); return (
diff --git a/src/blocks/blocks/form/style.scss b/src/blocks/blocks/form/style.scss index 315927270..fec3f8149 100644 --- a/src/blocks/blocks/form/style.scss +++ b/src/blocks/blocks/form/style.scss @@ -1,5 +1,5 @@ .wp-block-themeisle-blocks-form { - + --border-radius: 4px; --border-color: inherit; --border-width: 1px; @@ -39,6 +39,10 @@ flex-wrap: wrap; gap: var(--inputs-gap); + &> div { + margin: 0px; + } + p { margin-bottom: 10px; } @@ -180,7 +184,7 @@ &.right > div { align-items: flex-end; - + .o-form-server-response { padding-right: 0px; } @@ -189,7 +193,7 @@ &.o-right-tablet > div { @media ( max-width: 1024px ) { align-items: flex-end; - + .o-form-server-response { padding-right: 0px; } @@ -199,7 +203,7 @@ &.o-right-mobile > div { @media ( max-width:640px ) { align-items: flex-end; - + .o-form-server-response { padding-right: 0px; } @@ -208,7 +212,7 @@ &.left > div { align-items: flex-start; - + .o-form-server-response { padding-left: 0px; } @@ -217,7 +221,7 @@ &.o-left-tablet > div { @media ( max-width: 1024px ) { align-items: flex-start; - + .o-form-server-response { padding-left: 0px; } @@ -227,7 +231,7 @@ &.o-left-mobile > div { @media ( max-width:640px ) { align-items: flex-start; - + .o-form-server-response { padding-left: 0px; } @@ -242,7 +246,7 @@ @media ( max-width: 1024px ) { align-items: center; } - } + } &.o-full-mobile > div, &.o-center-mobile > div { @media ( max-width:640px ) { @@ -253,7 +257,7 @@ } - .wp-block-themeisle-blocks-form-input, .wp-block-themeisle-blocks-form-textarea { + .wp-block-themeisle-blocks-form-input, .wp-block-themeisle-blocks-form-textarea, .wp-block-themeisle-blocks-form-multiple-choice, .wp-block-themeisle-blocks-form-file { display: flex; flex-direction: column; width: 100%; @@ -270,14 +274,30 @@ } } - .otter-form-input, .otter-form-textarea-input { + .otter-form-input, .otter-form-textarea-input, select { color: var(--input-color); width: var(--input-width); - padding: var(--padding); - border-radius: var(--border-radius); - border-width: var(--border-width); - border-color: var(--border-color); - border-style: solid; + font-size: var(--input-font-size); + + &:not(input[type="file"]) { + padding: var(--padding); + border-radius: var(--border-radius); + border-width: var(--border-width); + border-color: var(--border-color); + border-style: solid; + background-color: var(--input-bg-color); + } + + @media (max-width: 1024px) { + padding: var(--padding-tablet); + } + + @media (max-width: 640px) { + padding: var(--padding-mobile); + } + } + + .o-form-choice-label { font-size: var(--input-font-size); background: var(--input-bg-color); box-sizing: border-box; @@ -288,6 +308,48 @@ color: var(--help-label-color); margin-bottom: 0px; font-size: var(--help-font-size); + width: 100%; + } + + &.is-style-hidden-label { + .otter-form-input-label, .otter-form-textarea-label { + display: none; + } + } + + @media (min-width: 640px) { + flex-grow: 1; + + &:is(.is-style-o-c-three-quarters, .is-style-o-c-two-thirds, .is-style-o-c-half, .is-style-o-c-one-third, .is-style-o-c-one-quarter) { + box-sizing: border-box; + margin-left: 0px; + margin-right: 0px; + } + + &.is-style-o-c-three-quarters { + flex-basis: calc( 75% - var( --inputs-gap ) ); + max-width: 75%; + } + + &.is-style-o-c-two-thirds { + flex-basis: calc( 66.66666666666666% - var( --inputs-gap ) ); + max-width: 66.66666666666666%; + } + + &.is-style-o-c-half { + flex-basis: calc( 50% - var( --inputs-gap ) ); + max-width: 50%; + } + + &.is-style-o-c-one-third { + flex-basis: calc( 33.33333333333333% - var( --inputs-gap ) ); + max-width: 33.33333333333333%; + } + + &.is-style-o-c-one-quarter { + flex-basis: calc( 25% - var( --inputs-gap ) ); + max-width: 25%; + } } @media (min-width: 640px) { @@ -392,4 +454,42 @@ animation: spinner 0.6s linear infinite; } } + + .wp-block-themeisle-blocks-form-multiple-choice.is-style-inline-list:not(:has(.o-form-choices select)) .o-form-choices { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; + } + + .wp-block-themeisle-blocks-form-multiple-choice { + label, select { + color: var(--input-color); + } + + select { + width: var(--input-width); + border-color: var(--input-color); + padding: var(--padding); + + @media (max-width: 1024px) { + padding: var(--padding-tablet); + } + + @media (max-width: 640px) { + padding: var(--padding-mobile); + } + } + } + + .wp-block-themeisle-blocks-form-file input[type="file"] { + border: 0px; + } +} + +.o-form-multiple-choice-field { + display: flex; + align-items: center; + flex-direction: row; + gap: 5px; } diff --git a/src/blocks/blocks/form/textarea/edit.js b/src/blocks/blocks/form/textarea/edit.js index 2c370b903..ce17d5df5 100644 --- a/src/blocks/blocks/form/textarea/edit.js +++ b/src/blocks/blocks/form/textarea/edit.js @@ -10,19 +10,16 @@ import { import { Fragment, - useEffect, - useRef + useEffect } from '@wordpress/element'; -import { createBlock } from '@wordpress/blocks'; - /** * Internal dependencies */ import metadata from './block.json'; import { blockInit } from '../../../helpers/block-utility.js'; import Inspector from './inspector.js'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { _cssBlock } from '../../../helpers/helper-functions'; const { attributes: defaultAttributes } = metadata; @@ -43,68 +40,23 @@ const Edit = ({ const blockProps = useBlockProps(); - const labelRef = useRef( null ); - const inputRef = useRef( null ); - const helpRef = useRef( null ); - - const { - parentClientId - } = useSelect( select => { - const { - getBlock, - getBlockRootClientId - } = select( 'core/block-editor' ); - - if ( ! clientId ) { - return { - parentClientId: '' - }; - } - - const parentClientId = getBlockRootClientId( clientId ); - - return { - parentClientId: parentClientId ?? '' - }; - }, [ clientId ]); - - const { selectBlock, replaceBlock } = useDispatch( 'core/block-editor' ); - - const switchToInput = type => { - const block = createBlock( 'themeisle-blocks/form-input', { ...attributes, type: type }); - replaceBlock( clientId, block ); - }; - - useEffect( () => { - const per = x => x ? x + '%' : x; - - /** - * TODO: Refactor this based on #748 - */ - - if ( inputRef.current ) { - inputRef.current?.style?.setProperty( '--input-width', per( attributes.inputWidth ) ); - } - if ( labelRef.current ) { - labelRef.current?.style?.setProperty( '--label-color', attributes.labelColor || null ); - } - if ( helpRef.current ) { - helpRef.current?.style?.setProperty( '--label-color', attributes.labelColor || null ); - } - }, [ inputRef.current, labelRef.current, attributes ]); - return ( selectBlock( parentClientId ) } - switchToInput={ switchToInput } + clientId={ clientId } />
+