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 @@ + 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/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, + }; +} diff --git a/src/Campaigns/ServiceProvider.php b/src/Campaigns/ServiceProvider.php index 729ea3719e..c6c7f6282c 100644 --- a/src/Campaigns/ServiceProvider.php +++ b/src/Campaigns/ServiceProvider.php @@ -47,6 +47,7 @@ public function boot(): void $this->registerMigrations(); $this->registerRoutes(); $this->registerCampaignEntity(); + $this->registerCampaignBlocks(); $this->setupCampaignForms(); } @@ -148,4 +149,13 @@ 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('rest_api_init', Actions\RegisterCampaignIdRestField::class); + Hooks::addAction('init', Actions\RegisterCampaignBlocks::class); + } } 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; +}