Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b1093c0
fix(fields): normalize custom select values
danieliser Sep 13, 2026
e2ec1e7
feat(fields): add contextual CTA field help
danieliser Sep 13, 2026
4433819
fix(cta-editor): honor schema defaults and composite errors
danieliser Sep 13, 2026
b764785
docs: note CTA field schema improvements
danieliser Sep 19, 2026
9222acf
fix: address CTA field review feedback
danieliser Sep 19, 2026
4818590
fix: initialize CTA defaults across lazy tabs
danieliser Sep 19, 2026
2da8e0e
fix: initialize defaults from the first CTA tab
danieliser Sep 19, 2026
796ea6c
fix: materialize CTA defaults only when saving
danieliser Sep 19, 2026
edb69d3
fix: prepare CTA defaults exactly once
danieliser Sep 19, 2026
03e1ae8
fix: normalize legacy CTA field metadata
danieliser Sep 19, 2026
d24b768
fix: normalize CTA defaults consistently
danieliser Sep 19, 2026
c88debe
style: format CTA visibility tests
danieliser Sep 19, 2026
9b9df73
fix: normalize legacy CTA field props
danieliser Sep 19, 2026
fced04f
fix: materialize legacy multi-value defaults
danieliser Sep 19, 2026
467aa40
fix: preserve legacy CTA field behavior
danieliser Sep 19, 2026
2f7b163
fix: include primary field in tab errors
danieliser Sep 19, 2026
52e2eb9
fix: ignore null CTA field defaults
danieliser Sep 19, 2026
f05c494
fix: complete legacy CTA field normalization
danieliser Sep 19, 2026
a02e97a
fix: preserve legacy field default shapes
danieliser Sep 19, 2026
46ea30f
style: format field default normalization
danieliser Sep 19, 2026
16d3edc
fix: normalize absent multi-value defaults
danieliser Sep 19, 2026
394da99
fix: normalize remaining CTA field defaults
danieliser Sep 20, 2026
d4180ef
fix: preserve CTA select value contracts
danieliser Sep 20, 2026
43e0be5
fix: keep CTA save callbacks canonical
danieliser Sep 20, 2026
340f9f3
fix: align CTA defaults with field contracts
danieliser Sep 20, 2026
8da3de0
fix: normalize CTA selector defaults
danieliser Sep 20, 2026
10be9f2
fix: preserve multicheck option value types
danieliser Sep 21, 2026
2ab238b
fix: normalize single object selector defaults
danieliser Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

**Developers**

- Improved Call to Action field schema support for add-ons.

## v1.25.0 - 2026-09-10

**Security**
Expand Down
19 changes: 14 additions & 5 deletions packages/components/src/lib/smart-token-control/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
width: 100%;

.component-smart-token-control__input {
padding: 5px;
align-items: center;
box-sizing: border-box;
min-height: 40px;
padding: 4px 8px;
box-shadow: 0 0 0 transparent;
border-radius: 4px;
border-radius: 2px;
border: 1px solid #8c8f94;
margin: 0 1px;
margin: 0;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
Expand Down Expand Up @@ -58,8 +61,8 @@
padding: 0;
margin: 0;
width: 100%;
line-height: 1.2;
min-height: auto;
line-height: 1.4;
min-height: 30px;
cursor: pointer;
}

Expand All @@ -76,6 +79,12 @@
margin: 0;
}
}

&.is-focused .component-smart-token-control__input {
border-color: var(--wp-admin-theme-color);
box-shadow: 0 0 0 1px var(--wp-admin-theme-color);
outline: 2px solid transparent;
}
}

.component-smart-token-control__suggestions {
Expand Down
52 changes: 35 additions & 17 deletions packages/core-data/src/call-to-actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { compare as jsonpatchCompare } from 'fast-json-patch';
import { __, sprintf } from '@popup-maker/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { decodeEntities } from '@wordpress/html-entities';
import { applyFilters } from '@wordpress/hooks';

import { ACTION_TYPES, NOTICE_CONTEXT } from './constants';

Expand Down Expand Up @@ -35,6 +36,12 @@ const {
INVALIDATE_RESOLUTION,
} = ACTION_TYPES;

/** Apply editor-provided normalization immediately before validation and I/O. */
const prepareCallToActionForSave = < T extends Partial< EditableCta > >(
callToAction: T
): T =>
applyFilters( 'popupMaker.callToAction.prepareForSave', callToAction ) as T;

/**
* Helper function to handle field-specific validation errors using WordPress notices
*
Expand Down Expand Up @@ -134,9 +141,9 @@ const entityActions = {
/**
* Create a new entity record. Values sent to the server immediately.
*
* @param {Editable} callToAction The entity to create.
* @param {boolean} validate An optional validation function.
* @param {boolean} withNotices Whether to show notices.
* @param {Partial<EditableCta>} callToAction The entity to create.
* @param {boolean} validate An optional validation function.
* @param {boolean} withNotices Whether to show notices.
* @return {Promise<CallToAction< 'edit' > | false>} The created entity or false if validation fails.
*/
createCallToAction:
Expand All @@ -157,7 +164,8 @@ const entityActions = {
},
} );

const { id, ...newCta } = callToAction;
const { id, ...rawCta } = callToAction;
const newCta = prepareCallToActionForSave( rawCta );

if ( validate ) {
const validation = validateCallToAction( newCta );
Expand Down Expand Up @@ -281,18 +289,24 @@ const entityActions = {
* @param {PartialEditableCta} callToAction The entity to update.
* @param {boolean} validate An optional validation function.
* @param {boolean} withNotices Whether to show notices.
* @return {Promise<T | boolean>} The updated entity or false if validation fails.
* @param {boolean} prepare Whether to run save preparation.
* @return {Promise<CallToAction<'edit'> | false>} The updated entity or false if validation fails.
*/
updateCallToAction:
(
callToAction: PartialEditableCta,
validate: boolean = true,
withNotices: boolean = true
withNotices: boolean = true,
prepare: boolean = true
): ThunkAction< CallToAction< 'edit' > | false > =>
async ( { select, dispatch, registry } ) => {
const action = 'updateCallToAction';

try {
const preparedCallToAction = prepare
? prepareCallToActionForSave( callToAction )
: callToAction;

dispatch( {
type: CHANGE_ACTION_STATUS,
payload: {
Expand All @@ -302,7 +316,8 @@ const entityActions = {
} );

if ( validate ) {
const validation = validateCallToAction( callToAction );
const validation =
validateCallToAction( preparedCallToAction );

if ( true !== validation ) {
dispatch( {
Expand All @@ -328,7 +343,7 @@ const entityActions = {
}

const canonicalCallToAction = await select.getCallToAction(
callToAction.id
preparedCallToAction.id
);

if ( ! canonicalCallToAction ) {
Expand Down Expand Up @@ -366,7 +381,7 @@ const entityActions = {
`ctas/${ canonicalCallToAction.id }`,
{
method: 'POST',
data: callToAction,
data: preparedCallToAction,
}
);

Expand Down Expand Up @@ -726,10 +741,12 @@ const editorActions = {

return false;
}
const preparedCallToAction =
prepareCallToActionForSave( editedCallToAction );

if ( editedCallToAction && validate ) {
if ( preparedCallToAction && validate ) {
const validation =
validateCallToAction( editedCallToAction );
validateCallToAction( preparedCallToAction );

if ( true !== validation ) {
registry.batch( async () => {
Expand Down Expand Up @@ -757,9 +774,10 @@ const editorActions = {
}

const result = await dispatch.updateCallToAction(
editedCallToAction,
preparedCallToAction,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
danieliser marked this conversation as resolved.
false,
withNotices
withNotices,
false
);

if ( result ) {
Expand Down Expand Up @@ -1023,7 +1041,7 @@ const editorActions = {
* Opens the editor immediately. If the entity isn't in the store yet it is
* resolved in the background while consumers render a loading state.
*
* @param {EditorId} editorId The editor ID.
* @param {CtaEditorId} editorId The editor ID.
* @return {Promise<void>}
*/
changeEditorId:
Expand Down Expand Up @@ -1284,9 +1302,9 @@ const resolutionActions = {
/**
* Change status of a dispatch action request.
*
* @param {CallToActionsStore[ 'ActionNames' ]} actionName Action name to change status of.
* @param {Statuses} status New status.
* @param {string|undefined} message Optional error message.
* @param {string} actionName Action name to change status of.
* @param {DispatchStatus} status New status.
* @param {string|undefined} message Optional error message.
* @return {Object} Action object.
*/
changeActionStatus:
Expand Down
69 changes: 56 additions & 13 deletions packages/cta-editor/src/components/field-with-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import { Field } from '@popup-maker/fields';
import { URLControl } from '@popup-maker/components';
import { FieldWrapper } from './field-wrapper';
import { useFieldError } from '../hooks';
import {
getFieldDefault,
normalizeFieldDefault,
} from '../registries/fields/field-visibility';

import type { FieldProps } from '@popup-maker/fields';
import type { FieldProps, OldFieldBase } from '@popup-maker/fields';

interface FieldWithErrorProps {
fieldId: string;
field: FieldProps;
field: FieldProps & { errorFieldIds?: string[] };
value: any;
onChange: ( value: any ) => void;
}
Expand All @@ -29,7 +33,10 @@ export const FieldWithError: React.FC< FieldWithErrorProps > = ( {
value,
onChange,
} ) => {
const { error, clearError } = useFieldError( fieldId );
const { error, clearError } = useFieldError(
fieldId,
field.errorFieldIds ?? []
);

// Clear field error when value changes
const handleChange = ( newValue: any ) => {
Expand All @@ -40,21 +47,57 @@ export const FieldWithError: React.FC< FieldWithErrorProps > = ( {
onChange( newValue );
};

const legacyDescription = (
field as FieldProps & Pick< OldFieldBase, 'desc' >
).desc;
const { heading: _heading, help, description, ...controlField } = field;
const effectiveValue =
value ?? normalizeFieldDefault( getFieldDefault( field ), field );
Comment thread
danieliser marked this conversation as resolved.
const fieldHelp = help ?? description ?? legacyDescription;
const needsExternalHelp = [
'color',
'html',
'multicheck',
'rangeslider',
'tokenselect',
Comment thread
danieliser marked this conversation as resolved.
].includes( field.type );
Comment thread
danieliser marked this conversation as resolved.
const fieldControl =
field.type === 'url' ? (
<>
<URLControl
{ ...controlField }
value={ effectiveValue }
onChange={ ( urlValue ) => handleChange( urlValue.url ) }
/>
{ fieldHelp && (
<p className="components-base-control__help">
{ fieldHelp }
</p>
) }
</>
) : (
<>
<Field
{ ...controlField }
help={ fieldHelp }
description={ description }
value={ effectiveValue }
onChange={ handleChange }
/>
{ needsExternalHelp && fieldHelp && (
<p className="components-base-control__help">
{ fieldHelp }
</p>
) }
</>
);
return (
<FieldWrapper
fieldId={ fieldId }
title={ field.label ?? '' }
title={ field.heading ?? field.label ?? '' }
error={ error }
>
{ field.type === 'url' ? (
<URLControl
{ ...field }
value={ value }
onChange={ ( urlValue ) => handleChange( urlValue.url ) }
/>
) : (
<Field { ...field } value={ value } onChange={ handleChange } />
) }
{ fieldControl }
</FieldWrapper>
);
};
Expand Down
22 changes: 11 additions & 11 deletions packages/cta-editor/src/editor/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ $gap4: $gap * 4;
svg {
margin-right: 3px;
fill: #fff;
stroke: green;
stroke: #008000;
}
}
}
Expand All @@ -114,6 +114,11 @@ $gap4: $gap * 4;
&.is-active {
background-color: #f0f0f0;
}

&.error {
background-color: #f4a2a2;
border-left: 4px solid #cc1818;
}
}
}

Expand Down Expand Up @@ -204,7 +209,7 @@ $gap4: $gap * 4;
}

&:not(:focus) {
box-shadow: inset 0 0 0 2px #dddddd;
box-shadow: inset 0 0 0 2px #ddd;
}
}
}
Expand Down Expand Up @@ -322,15 +327,10 @@ $gap4: $gap * 4;
}
}

.editor-tabs {
.components-tab-panel__tabs {
.components-tab-panel__tabs-item {
&.error {
background-color: #f4a2a2;
border-left: 4px solid #cc1818;
}
}
}
.pum-field__selected-help {
border-left: 2px solid #ddd;
margin-top: 8px;
padding-left: 10px;
}

// Loading placeholder layout while the record resolves.
Expand Down
17 changes: 14 additions & 3 deletions packages/cta-editor/src/editor/hocs/with-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const withModal = (
closeOnSave = true,
showDocumentationLink = true,
showActions = true,
onSave,
onClose,
// @ts-ignore It exists but is not typed.
onRequestClose,
Expand Down Expand Up @@ -193,8 +194,11 @@ export const withModal = (
return;
}

// Call the onSave callback if it exists
componentProps?.onSave?.( values );
// Read the canonical record after save preparation and persistence.
const savedValues = storeSelectors.getCurrentEditorValues();
if ( savedValues ) {
onSave?.( savedValues );
}

const hasRemainingEdits = storeSelectors.hasEdits(
values.id
Expand All @@ -209,7 +213,14 @@ export const withModal = (
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[ closeOnSave, closeModal, storeSelectors, hasAnyError, values ]
[
closeOnSave,
closeModal,
storeSelectors,
hasAnyError,
onSave,
values,
]
);

return (
Expand Down
Loading
Loading