From 3a88fa4ca5551c3dac61aeb254629f8b6c876e9a Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Wed, 11 Dec 2024 23:40:15 -0300 Subject: [PATCH 1/4] feature: add dynamic entry to webpack for campaign blocks compiling --- wordpress-scripts-webpack.config.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/wordpress-scripts-webpack.config.js b/wordpress-scripts-webpack.config.js index f8dced076f..ffd188d3b5 100644 --- a/wordpress-scripts-webpack.config.js +++ b/wordpress-scripts-webpack.config.js @@ -1,6 +1,7 @@ /** * External Dependencies */ +const fs = require('fs'); const path = require('path'); /** @@ -63,6 +64,7 @@ module.exports = { campaignEntity: srcPath('Campaigns/resources/entity.ts'), campaignDetails: srcPath('Campaigns/resources/admin/campaign-details.tsx'), adminBlocks: path.resolve(process.cwd(), 'blocks', 'load.js'), + ...getDynamicEntries(srcPath('Campaigns/Blocks'), 'Campaigns/Blocks/'), }, }; @@ -75,3 +77,22 @@ module.exports = { function srcPath(relativePath) { return path.resolve(process.cwd(), 'src', relativePath); } + +function getDynamicEntries(basePath, destinationPath = '') { + const entries = {}; + const directories = fs.readdirSync(basePath, {withFileTypes: true}); + + directories.forEach((dir) => { + if (dir.isDirectory()) { + const blockPath = path.join(basePath, dir.name); + const indexPath = path.join(blockPath, 'index.ts'); + const entryKey = dir.name.charAt(0).toLowerCase() + dir.name.slice(1); + + if (fs.existsSync(indexPath)) { + entries[`${destinationPath}${entryKey}`] = indexPath; + } + } + }); + + return entries; +} From 27e03ecbd8f0aebea7e7b5dabe4c7615dcb52b88 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Wed, 11 Dec 2024 23:41:16 -0300 Subject: [PATCH 2/4] feature: register campaign blocks dynamically --- src/Campaigns/Actions/RegisterCampaignBlocks.php | 15 +++++++++++++++ src/Campaigns/ServiceProvider.php | 9 +++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/Campaigns/Actions/RegisterCampaignBlocks.php diff --git a/src/Campaigns/Actions/RegisterCampaignBlocks.php b/src/Campaigns/Actions/RegisterCampaignBlocks.php new file mode 100644 index 0000000000..6da9da1d24 --- /dev/null +++ b/src/Campaigns/Actions/RegisterCampaignBlocks.php @@ -0,0 +1,15 @@ +registerMigrations(); $this->registerRoutes(); $this->registerCampaignEntity(); + $this->registerCampaignBlocks(); $this->setupCampaignForms(); } @@ -148,4 +149,12 @@ private function setupCampaignForms() Hooks::addAction('givewp_donation_form_created', AddCampaignFormFromRequest::class, 'visualFormBuilder'); Hooks::addAction('givewp_campaign_created', CreateDefaultCampaignForm::class); } + + /** + * @unreleased + */ + private function registerCampaignBlocks() + { + Hooks::addAction('init', Actions\RegisterCampaignBlocks::class); + } } From b19aabd06d3d75135b2b9e75e3d916f8f4e2c36e Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Wed, 11 Dec 2024 23:41:57 -0300 Subject: [PATCH 3/4] feature: register campaignId as an field in campaign page rest api --- .../Actions/RegisterCampaignIdRestField.php | 27 +++++++++++++++++++ src/Campaigns/ServiceProvider.php | 1 + 2 files changed, 28 insertions(+) create mode 100644 src/Campaigns/Actions/RegisterCampaignIdRestField.php diff --git a/src/Campaigns/Actions/RegisterCampaignIdRestField.php b/src/Campaigns/Actions/RegisterCampaignIdRestField.php new file mode 100644 index 0000000000..8f0a79acab --- /dev/null +++ b/src/Campaigns/Actions/RegisterCampaignIdRestField.php @@ -0,0 +1,27 @@ + function ($object) { + return get_post_meta($object['id'], 'campaignId', true); + }, + 'update_callback' => function ($value, $object) { + return update_post_meta($object->ID, 'campaignId', (int) $value); + }, + 'schema' => [ + 'description' => 'Campaign ID', + 'type' => 'string', + 'context' => ['view', 'edit'], + ], + ] + ); + } +} diff --git a/src/Campaigns/ServiceProvider.php b/src/Campaigns/ServiceProvider.php index 314c9d66f6..c6c7f6282c 100644 --- a/src/Campaigns/ServiceProvider.php +++ b/src/Campaigns/ServiceProvider.php @@ -155,6 +155,7 @@ private function setupCampaignForms() */ private function registerCampaignBlocks() { + Hooks::addAction('rest_api_init', Actions\RegisterCampaignIdRestField::class); Hooks::addAction('init', Actions\RegisterCampaignBlocks::class); } } From d3f8c5a59ca0f715b36d7fb0e94579b1483ee628 Mon Sep 17 00:00:00 2001 From: Paulo Iankoski Date: Wed, 11 Dec 2024 23:42:16 -0300 Subject: [PATCH 4/4] feature: add Campaign Title block --- .../Blocks/CampaignTitleBlock/block.json | 26 ++++++++ .../Blocks/CampaignTitleBlock/edit.tsx | 62 +++++++++++++++++++ .../Blocks/CampaignTitleBlock/editor.scss | 13 ++++ .../Blocks/CampaignTitleBlock/index.ts | 8 +++ .../Blocks/CampaignTitleBlock/render.php | 23 +++++++ .../shared/components/CampaignDropdown.tsx | 48 ++++++++++++++ .../shared/components/CampaignSelector.tsx | 28 +++++++++ .../Blocks/shared/hooks/useCampaign.ts | 11 ++++ .../Blocks/shared/hooks/useCampaignId.ts | 18 ++++++ .../Blocks/shared/hooks/useCampaigns.ts | 11 ++++ 10 files changed, 248 insertions(+) create mode 100644 src/Campaigns/Blocks/CampaignTitleBlock/block.json create mode 100644 src/Campaigns/Blocks/CampaignTitleBlock/edit.tsx create mode 100644 src/Campaigns/Blocks/CampaignTitleBlock/editor.scss create mode 100644 src/Campaigns/Blocks/CampaignTitleBlock/index.ts create mode 100644 src/Campaigns/Blocks/CampaignTitleBlock/render.php create mode 100644 src/Campaigns/Blocks/shared/components/CampaignDropdown.tsx create mode 100644 src/Campaigns/Blocks/shared/components/CampaignSelector.tsx create mode 100644 src/Campaigns/Blocks/shared/hooks/useCampaign.ts create mode 100644 src/Campaigns/Blocks/shared/hooks/useCampaignId.ts create mode 100644 src/Campaigns/Blocks/shared/hooks/useCampaigns.ts diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/block.json b/src/Campaigns/Blocks/CampaignTitleBlock/block.json new file mode 100644 index 0000000000..23643da1e8 --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/block.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "givewp/campaign-title-block", + "version": "1.0.0", + "title": "Campaign Title", + "category": "give", + "icon": "heading", + "description": "Displays the title of the campaign.", + "supports": { + "html": false + }, + "attributes": { + "campaignId": { + "type": "integer" + }, + "headingLevel": { + "type": "number", + "default": 1 + } + }, + "textdomain": "give", + "editorScript": "file:./../../../../build/Campaigns/Blocks/campaignTitleBlock.js", + "editorStyle": "file:./../../../../build/Campaigns/Blocks/campaignTitleBlock.css", + "render": "file:./render.php" +} diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/edit.tsx b/src/Campaigns/Blocks/CampaignTitleBlock/edit.tsx new file mode 100644 index 0000000000..db4a746bb7 --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/edit.tsx @@ -0,0 +1,62 @@ +import {BlockControls, HeadingLevelDropdown, InspectorControls, useBlockProps} from '@wordpress/block-editor'; +import {BaseControl, Icon, PanelBody, TextareaControl} from '@wordpress/components'; +import ServerSideRender from '@wordpress/server-side-render'; +import {CampaignSelector} from '../shared/components/CampaignSelector'; +import useCampaign from '../shared/hooks/useCampaign'; +import {__} from '@wordpress/i18n'; +import {useSelect} from '@wordpress/data'; +import {external} from '@wordpress/icons'; + +import './editor.scss'; + +export default function Edit({attributes, setAttributes}) { + const blockProps = useBlockProps(); + const {campaign, hasResolved} = useCampaign(attributes.campaignId); + + const adminBaseUrl = useSelect( + (select) => select('core').getSite()?.url + '/wp-admin/edit.php?post_type=give_forms&page=give-campaigns', + [] + ); + + const editCampaignUrl = `${adminBaseUrl}&id=${attributes.campaignId}&tab=settings`; + + return ( +
+ + + + + {hasResolved && campaign && ( + + + + null} + help={ + + {__('Edit campaign title', 'give')} + + + } + /> + + + + )} + + + setAttributes({headingLevel: newLevel})} + /> + +
+ ); +} diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/editor.scss b/src/Campaigns/Blocks/CampaignTitleBlock/editor.scss new file mode 100644 index 0000000000..031ddc43f7 --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/editor.scss @@ -0,0 +1,13 @@ +.givewp-campaign-title-block { + &__edit-campaign-link { + display: inline-flex; + align-items: center; + gap: 0.125rem; + + svg { + fill: currentColor; + height: 1.25rem; + width: 1.25rem; + } + } +} diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/index.ts b/src/Campaigns/Blocks/CampaignTitleBlock/index.ts new file mode 100644 index 0000000000..f376f33a5a --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/index.ts @@ -0,0 +1,8 @@ +import {registerBlockType} from '@wordpress/blocks'; +import metadata from './block.json'; +import Edit from './edit'; + +// @ts-ignore +registerBlockType(metadata.name, { + edit: Edit +}); diff --git a/src/Campaigns/Blocks/CampaignTitleBlock/render.php b/src/Campaigns/Blocks/CampaignTitleBlock/render.php new file mode 100644 index 0000000000..a351cf633a --- /dev/null +++ b/src/Campaigns/Blocks/CampaignTitleBlock/render.php @@ -0,0 +1,23 @@ +getById($attributes['campaignId']); + +if (! $campaign) { + return; +} + +$headingLevel = isset($attributes['headingLevel']) ? (int) $attributes['headingLevel'] : 1; +$headingTag = 'h' . min(6, max(1, $headingLevel)); +?> + +< > +title); ?> +> diff --git a/src/Campaigns/Blocks/shared/components/CampaignDropdown.tsx b/src/Campaigns/Blocks/shared/components/CampaignDropdown.tsx new file mode 100644 index 0000000000..f7c543aef2 --- /dev/null +++ b/src/Campaigns/Blocks/shared/components/CampaignDropdown.tsx @@ -0,0 +1,48 @@ +import {PanelBody, SelectControl} from '@wordpress/components'; +import {InspectorControls} from '@wordpress/block-editor'; +import useCampaigns from '../hooks/useCampaigns'; +import {Campaign} from '@givewp/campaigns/admin/components/types'; +import {__} from '@wordpress/i18n'; + +export default function CampaignDropdown({campaignId, setAttributes, placement = 'sidebar'}) { + const {campaigns, hasResolved} = useCampaigns(); + + const options = (() => { + if (!hasResolved) { + return [{label: __('Loading...', 'give'), value: ''}]; + } + + if (campaigns.length) { + const campaignOptions = campaigns.map((campaign) => ({ + label: campaign.title, + value: campaign.id.toString(), + })); + + return [{label: __('Select...', 'give'), value: ''}, ...campaignOptions]; + } + + return [{label: __('No campaigns found.', 'give'), value: ''}]; + })(); + + const dropdown = ( + setAttributes({campaignId: newValue ? parseInt(newValue) : null})} + /> + ); + + if (placement === 'sidebar') { + return ( + + + {dropdown} + + + ); + } + + return dropdown; +} diff --git a/src/Campaigns/Blocks/shared/components/CampaignSelector.tsx b/src/Campaigns/Blocks/shared/components/CampaignSelector.tsx new file mode 100644 index 0000000000..2350e93e0a --- /dev/null +++ b/src/Campaigns/Blocks/shared/components/CampaignSelector.tsx @@ -0,0 +1,28 @@ +import useCampaignId from '../hooks/useCampaignId'; +import CampaignDropdown from './CampaignDropdown'; + +export function CampaignSelector({attributes, setAttributes, children}) { + const campaignId = useCampaignId(attributes, setAttributes); + + return ( + <> + {!campaignId && !attributes?.campaignId && ( + + )} + + {!campaignId && ( + + )} + + {attributes?.campaignId && children} + + ); +} diff --git a/src/Campaigns/Blocks/shared/hooks/useCampaign.ts b/src/Campaigns/Blocks/shared/hooks/useCampaign.ts new file mode 100644 index 0000000000..c0219a44fb --- /dev/null +++ b/src/Campaigns/Blocks/shared/hooks/useCampaign.ts @@ -0,0 +1,11 @@ +import {useEntityRecord} from '@wordpress/core-data'; +import {Campaign} from '@givewp/campaigns/admin/components/types'; + +export default function useCampaign(campaignId) { + const data = useEntityRecord('givewp', 'campaign', campaignId); + + return { + campaign: data?.record as Campaign, + hasResolved: data?.hasResolved, + }; +} diff --git a/src/Campaigns/Blocks/shared/hooks/useCampaignId.ts b/src/Campaigns/Blocks/shared/hooks/useCampaignId.ts new file mode 100644 index 0000000000..c44101de0f --- /dev/null +++ b/src/Campaigns/Blocks/shared/hooks/useCampaignId.ts @@ -0,0 +1,18 @@ +import {useSelect} from '@wordpress/data'; + +export default function useCampaignId(attributes, setAttributes) { + const campaignIdFromContext = useSelect((select) => { + const postType = select('core/editor').getCurrentPostType(); + + if (postType === 'give_campaign_page') { + return select('core/editor').getEditedPostAttribute('campaignId'); + } + return null; + }, []); + + if (campaignIdFromContext && campaignIdFromContext !== attributes?.campaignId) { + setAttributes({campaignId: campaignIdFromContext}); + } + + return campaignIdFromContext; +} diff --git a/src/Campaigns/Blocks/shared/hooks/useCampaigns.ts b/src/Campaigns/Blocks/shared/hooks/useCampaigns.ts new file mode 100644 index 0000000000..3764ad139d --- /dev/null +++ b/src/Campaigns/Blocks/shared/hooks/useCampaigns.ts @@ -0,0 +1,11 @@ +import {useEntityRecords} from '@wordpress/core-data'; +import {Campaign} from '@givewp/campaigns/admin/components/types'; + +export default function useCampaigns() { + const data = useEntityRecords('givewp', 'campaign'); + + return { + campaigns: data?.records as Campaign[], + hasResolved: data?.hasResolved, + }; +}