import React, { useState } from 'react'; import { useDispatch, useSelect } from '@wordpress/data'; import { decodeEntities } from '@wordpress/html-entities'; import { __, sprintf } from '@wordpress/i18n'; import { Blueprint, BlueprintVariant } from '../types'; import { Select as AudiencePicker } from '@/audiences/ui'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Body, Caption, SectionTitle } from '@/components/ui/typography'; interface EditorProps { blueprint: Blueprint; onClose: () => void; } const emptyVariant = (): BlueprintVariant => ( { title: '', audience: 0, fallback: false, percentage: null, } ); /** * Audience picker for a variant row. * * Uses the audience selector's `button` render-prop so it opens the audience * manager as a popup (the default inline control renders poorly here), and * shows the chosen audience's name on the trigger. */ function VariantAudiencePicker( { audience, onSelect }: { audience: number, onSelect: ( id: number ) => void } ) { const audiencePost = useSelect( ( select: any ) => ( audience ? select( 'audience' ).getPost( audience ) : null ), [ audience ] ); const title = audiencePost?.title?.rendered || audiencePost?.title?.raw || ''; return ( void } ) => ( ) } onSelect={ ( id: number ) => onSelect( id ) } /> ); } /** * Create / edit a single blueprint. * * Inline (master-detail) rather than a modal, so the audience picker — which * opens its own full-screen modal — never has to nest inside another modal. */ export default function Editor( { blueprint, onClose }: EditorProps ) { const [ draft, setDraft ] = useState( () => { // Guarantee a fallback variant at index 0. const variants = blueprint.variants && blueprint.variants.length > 0 ? blueprint.variants : [ { title: __( 'Default', 'altis' ), audience: 0, fallback: true, percentage: null, } ]; return { ...blueprint, variants, }; } ); const { createBlueprint, updateBlueprint } = useDispatch( 'blueprints' ); const isUpdating = useSelect( ( s: any ) => s( 'blueprints' ).getIsUpdating(), [] ); const [ saveError, setSaveError ] = useState( null ); const isNew = ! draft.id; const setVariant = ( index: number, changes: Partial ) => { setDraft( current => ( { ...current, variants: current.variants.map( ( v, i ) => ( i === index ? { ...v, ...changes, } : v ) ), } ) ); }; const addVariant = () => { setDraft( current => ( { ...current, variants: [ ...current.variants, emptyVariant() ], } ) ); }; const removeVariant = ( index: number ) => { setDraft( current => ( { ...current, // Never remove the fallback at index 0. variants: current.variants.filter( ( _, i ) => i !== index || i === 0 ), } ) ); }; const canSave = draft.title.trim() !== '' && draft.variants.slice( 1 ).every( v => v.audience > 0 ) && draft.variants.length >= 2 && ! isUpdating; const save = async () => { if ( ! canSave ) { return; } setSaveError( null ); try { if ( isNew ) { await createBlueprint( draft ); } else { await updateBlueprint( draft ); } onClose(); } catch ( error: any ) { // Keep the editor open with the draft intact — server-side // validation errors (missing audience etc.) carry a message. setSaveError( error?.message || __( 'Could not save the blueprint.', 'altis' ) ); } }; return (
{ isNew ? __( 'New blueprint', 'altis' ) : __( 'Edit blueprint', 'altis' ) }
setDraft( { ...draft, title: e.target.value, } ) } /> { __( 'Shown in the block "…" menu when applying this blueprint to selected blocks.', 'altis' ) }
setDraft( { ...draft, status: checked ? 'published' : 'draft', } ) } />
{ __( 'Variants', 'altis' ) } { __( 'The default is shown to everyone who does not match a variant audience. Each variant targets one audience.', 'altis' ) }
{ draft.variants.map( ( variant, index ) => (
setVariant( index, { title: e.target.value } ) } />
{ index === 0 ? ( { __( 'Everyone else (default)', 'altis' ) } ) : ( setVariant( index, { audience: id } ) } /> ) }
{ index !== 0 && ( ) }
) ) }
{ saveError && ( { saveError } ) } { draft.variants.slice( 1 ).some( v => v.audience === 0 ) && ( { sprintf( /* translators: %d: number of variants missing an audience. */ __( '%d variant(s) need an audience.', 'altis' ), draft.variants.slice( 1 ).filter( v => v.audience === 0 ).length ) } ) }
); }