import {useState} from 'react';
import {__, sprintf} from '@wordpress/i18n';
import {createInterpolateElement} from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import ModalDialog from '@givewp/components/AdminUI/ModalDialog';
import RowAction from '@givewp/components/ListTable/RowAction';
import {CheckCircle} from '@givewp/components/AdminUI/Icons';
import {AsyncPaginate} from 'react-select-async-paginate';
import {
AsyncSelectEntity,
asyncSelectEntities,
useCampaignAsyncSelect,
} from '@givewp/components/ListTable/CustomFilter/useAsyncCampaigns';
import {CampaignOption} from '@givewp/components/ListTable/CustomFilter/utils';
import {Interweave} from 'interweave';
import selectStyles from '@givewp/components/ListTable/CustomFilter/styles.module.scss';
import styles from './AssignCampaignModal.module.scss';
type AssignCampaignProps = {
formId: number;
formTitle: string;
/** The campaign the form belongs to now, or 0 for a standalone form. */
campaignId: number;
campaignTitle: string;
/** The campaign's default form cannot be moved or removed. */
isDefaultCampaignForm: boolean;
/** Called when the modal closes after a change, so the list can refresh. */
onChanged: () => Promise;
};
type AssignCampaignModalProps = AssignCampaignProps & {
isOpen: boolean;
handleClose: () => void;
};
type Step = 'pick' | 'confirmRemove' | 'moved' | 'removed';
/* A form cannot be assigned to an archived campaign, so the picker leaves those out. */
const assignableCampaigns: AsyncSelectEntity = {
...asyncSelectEntities.campaign,
listQuery: {status: 'active,draft'},
};
/**
* Row action that owns the modal state. It is a real component so the parent row-actions
* function stays hook-free and the row count can change without breaking React's hook order.
* A standalone form gets "Assign campaign"; a form that already has one gets "Change campaign".
*
* @since 4.19.0
*/
export default function AssignCampaignRowAction(props: AssignCampaignProps) {
const [isOpen, setOpen] = useState(false);
return (
<>
setOpen(true)}
actionId={props.formId}
displayText={props.campaignId ? __('Change campaign', 'give') : __('Assign campaign', 'give')}
hiddenText={props.formTitle}
/>
{isOpen && setOpen(false)} />}
>
);
}
/**
* One modal for everything about a form's campaign: assign a standalone form, move a linked form
* to another campaign, or remove it from its campaign. The picker only offers campaigns that are
* not archived and never the current one. The server refuses to move or remove a campaign's
* default form and explains why.
*
* @since 4.19.0
*/
function AssignCampaignModal({
isOpen,
handleClose,
formId,
formTitle,
campaignId: currentCampaignId,
campaignTitle: currentCampaignTitle,
isDefaultCampaignForm,
onChanged,
}: AssignCampaignModalProps) {
const hasCampaign = currentCampaignId > 0;
const [step, setStep] = useState('pick');
const [selected, setSelected] = useState(null);
const [isSaving, setSaving] = useState(false);
const [error, setError] = useState('');
const {loadOptions, mapOptionsForMenu} = useCampaignAsyncSelect(null, assignableCampaigns);
const campaignId = selected?.value ?? 0;
const title = hasCampaign ? __('Change campaign', 'give') : __('Assign campaign', 'give');
const campaignUrl = `edit.php?post_type=give_forms&page=give-campaigns&id=${campaignId}&tab=overview&action=edit`;
/*
* The list refresh re-renders the rows and unmounts this modal, so it waits until the
* modal closes. Otherwise the success view would vanish the moment it appeared.
*/
const close = async () => {
handleClose();
if (step === 'moved' || step === 'removed') {
await onChanged();
}
};
const loadOtherCampaigns = async (search: string) => {
const result = await loadOptions(search);
return {...result, options: result.options.filter((option) => option.value !== currentCampaignId)};
};
const request = async (path: string, data: object, nextStep: Step) => {
setSaving(true);
setError('');
try {
await apiFetch({path, method: 'POST', data});
setStep(nextStep);
} catch (e) {
setError(e?.message ?? __('Something went wrong. Please try again.', 'give'));
} finally {
setSaving(false);
}
};
const assign = () =>
request('/givewp/v3/associate-forms-with-campaign', {campaignId, formIDs: [formId]}, 'moved');
const remove = () => request('/givewp/v3/detach-forms-from-campaign', {formIDs: [formId]}, 'removed');
const strong = (content: string) => (
);
return (
{(step === 'moved' || step === 'removed') && (
{step === 'moved'
? hasCampaign
? createInterpolateElement(
sprintf(
/* translators: 1: form title, 2: new campaign title, 3: previous campaign title */
__('
now belongs to %2$s. Its past donations stay with %3$s.', 'give'),
formTitle,
selected?.label ?? '',
currentCampaignTitle
),
{form: strong(formTitle), campaign: strong(selected?.label ?? ''), previous: strong(currentCampaignTitle)}
)
: createInterpolateElement(
/* translators: 1: form title, 2: campaign title */
sprintf(__(' now belongs to %2$s.', 'give'), formTitle, selected?.label ?? ''),
{form: strong(formTitle), campaign: strong(selected?.label ?? '')}
)
: createInterpolateElement(
/* translators: 1: form title, 2: campaign title */
sprintf(__(' is no longer part of %2$s.', 'give'), formTitle, currentCampaignTitle),
{form: strong(formTitle), campaign: strong(currentCampaignTitle)}
)}
{createInterpolateElement(
/* translators: 1: form title, 2: campaign title */
sprintf(
__('Remove
from %2$s? The form keeps working on its own, and its past donations stay with the campaign.', 'give'),
formTitle,
currentCampaignTitle
),
{form: strong(formTitle), campaign: strong(currentCampaignTitle)}
)}