diff --git a/admin/src/api/types.ts b/admin/src/api/types.ts index 1e1d492a..16d11bcf 100644 --- a/admin/src/api/types.ts +++ b/admin/src/api/types.ts @@ -59,6 +59,14 @@ export type BulkResult = Schemas['cabana.BulkResult'] export type BulkAction = Schemas['cabana.BulkAction'] /** A record action the record response offers: name, label and confirm text. */ export type RecordAction = Schemas['cabana.RecordAction'] +/** The text input a prompted record action asks for in its confirm dialog. */ +export type RecordActionPrompt = Schemas['cabana.RecordActionPrompt'] +/** A section a plugin contributes to My account: label, status and offered actions. */ +export type AccountContribution = Schemas['cabana.AccountContributionView'] +/** One action of a contributed My account section. */ +export type AccountContributionAction = Schemas['cabana.AccountContributionActionView'] +/** An account contribution action's answer: the toast message and an optional URL. */ +export type AccountActionResult = Schemas['cabana.AccountActionResultView'] /** A declared bulk action's answer: the toast message and the affected count. */ export type BulkActionResult = Schemas['cabana.BulkActionResult'] /** Body of a widget action: the record id (absent on create) and fill values. */ diff --git a/admin/src/components/account/AccountContributions.vue b/admin/src/components/account/AccountContributions.vue new file mode 100644 index 00000000..b2572c7a --- /dev/null +++ b/admin/src/components/account/AccountContributions.vue @@ -0,0 +1,126 @@ + + + diff --git a/admin/src/components/form/RecordActions.vue b/admin/src/components/form/RecordActions.vue index e60bc4c5..f6c02f11 100644 --- a/admin/src/components/form/RecordActions.vue +++ b/admin/src/components/form/RecordActions.vue @@ -16,6 +16,8 @@ import { useConfirm } from '../ui/confirm' // disabled meanwhile. Labels, confirm texts and messages come from the server // and are rendered as text. A `danger` action (UI-SPEC F5) turns the dialog's // confirm button into the danger variant; the footer button stays outline. +// An action with a `prompt` asks for one text value in the dialog and posts +// it as the body key `payload`; every other action posts an empty body. const props = withDefaults( defineProps<{ source: ControllerParams @@ -39,6 +41,16 @@ const emit = defineEmits<{ const confirm = useConfirm() const running = ref(false) +/** The server's first message on the payload field of a 422, if any. */ +function payloadDetail(error: unknown): string { + const details = (error as { error?: { details?: Record } } | undefined)?.error?.details + const payload = details?.payload + if (Array.isArray(payload) && typeof payload[0] === 'string') { + return payload[0] + } + return '' +} + async function run(action: RecordAction): Promise { if (running.value || props.disabled) { return @@ -46,23 +58,41 @@ async function run(action: RecordAction): Promise { let status = 0 let ok = false let text = '' + let detail = '' const confirmed = await confirm.ask( { message: action.confirm || t('backend::lang.form.action_confirm', { action: action.label }), confirmLabel: action.label, danger: action.danger, + prompt: action.prompt + ? { + label: action.prompt.label, + placeholder: action.prompt.placeholder, + comment: action.prompt.comment, + maxLength: action.prompt.max_length, + } + : undefined, }, async () => { running.value = true emit('busy', true) try { - const result = await api.POST('/{vendor}/{plugin}/{controller}/{id}/actions/{action}', { - params: { path: { ...props.source, id: props.recordId, action: action.name } }, - body: {}, - }) + const params = { path: { ...props.source, id: props.recordId, action: action.name } } + // A prompted action posts its value as `payload`; every other action + // keeps the empty body the record action route expects. + const result = action.prompt + ? await api.POST('/{vendor}/{plugin}/{controller}/{id}/actions/{action}', { + params, + body: { payload: confirm.value.value.trim() }, + }) + : await api.POST('/{vendor}/{plugin}/{controller}/{id}/actions/{action}', { + params, + body: {}, + }) status = result.response.status ok = !!result.data text = result.data?.data.message ?? result.error?.error.message ?? '' + detail = payloadDetail(result.error) } catch { status = 0 } finally { @@ -91,6 +121,10 @@ async function run(action: RecordAction): Promise { showToast(text || t('backend::lang.list.action_forbidden'), 'danger') return } + if (status === 422 && detail) { + showToast(detail, 'danger') + return + } showToast(text || t('backend::lang.extension.action_failed'), 'danger') } @@ -114,6 +148,9 @@ async function run(action: RecordAction): Promise { :confirm-label="confirm.request.value?.confirmLabel" :danger="confirm.request.value?.danger" :busy="confirm.busy.value" + :prompt="confirm.request.value?.prompt ?? null" + :value="confirm.value.value" + @update:value="confirm.setValue" @confirm="confirm.confirm" @cancel="confirm.cancel" /> diff --git a/admin/src/components/ui/ConfirmDialog.vue b/admin/src/components/ui/ConfirmDialog.vue index a273aec4..b469ab0b 100644 --- a/admin/src/components/ui/ConfirmDialog.vue +++ b/admin/src/components/ui/ConfirmDialog.vue @@ -1,4 +1,5 @@