teqo-backup/src/lib/contentPieceBatch.ts
Francisco Solla 10327a7451 feat(C236): publicar, despublicar e apagar em lote na Central de Conteúdos
A lista /campanha/comunicacao/conteudos ganha um modo de seleção opt-in
que envolve os dois corpos (tabela e card list) com um estado client
compartilhado, chaveado pela URL canônica (a seleção morre em
página/filtro e nunca cresce para além da página visível). Com seleção
ativa, a barra flutuante aplica o mesmo veredito unitário à seleção:
Publicar/Despublicar agem direto com recibo honesto; Apagar sempre
confirma no CampaignDeleteDialog reusado (novo caminho onConfirm
aditivo), avisando os links públicos só quando a seleção carrega
publicadas.

O transporte é uma rota única (POST .../conteudos/lote,
campaignJsonMutationRoute, teto de 50 ids) que itera server-side as ações
donas: cada peça mantém a própria transação, o gate fresco e a limpeza
best-effort de mídia; a falha de uma nunca reverte as demais e o recibo
separa afetadas x falhas (2 de 3 peças apagadas. 1 falhou.).

Sem migration, sem Consent novo, sem rota pública (S27) e sem mudar a
semântica do C222. Cobertura: int do lote (papéis, dedupe, mídia, evento,
tag, falha parcial, schema), unit do builder/barra/seleção e e2e HTTP.

Design tier: DEGRADED (artefato de origem sem certificação frontier) —
crítica do designer aplicada no escopo do item; entrega bloqueada para
sign-off humano antes do PR. Related #1388
2026-09-29 22:26:26 -03:00

79 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* C236 — pure vocabulary and copy of the batch actions of the Central de
* Conteúdos: the three verbs, the selection counter, the delete confirmation
* and the honest receipt (affected × failures). Client-safe: the selection
* provider, the action bar, the confirmation dialog and the JSON route share
* this module.
*
* The batch is the unit gesture repeated — it never invents a rule of its own,
* so everything here is presentation of an outcome the unit actions produced.
*/
import type { ContentPieceStatus } from '@/lib/contentPiece'
export const CONTENT_PIECE_BATCH_ACTIONS = ['publicar', 'despublicar', 'apagar'] as const
export type ContentPieceBatchAction = (typeof CONTENT_PIECE_BATCH_ACTIONS)[number]
/** One item of the selection: the only fields the list projection needs. */
export type ContentPieceSelectionRow = {
id: number
status: ContentPieceStatus
/** The public `/conteudos/<slug>` when the piece has a slug; null otherwise. */
publicPath: string | null
}
type ContentPieceBatchFailure = {
contentPieceId: number
message: string
}
/** What the loop produced: how many pieces changed and which ones did not. */
export type ContentPieceBatchOutcome = {
action: ContentPieceBatchAction
affected: number
failures: ContentPieceBatchFailure[]
}
/** Transport failure of the whole request — never a per-piece failure. */
export const CONTENT_PIECE_BATCH_ERROR_MESSAGE =
'Não foi possível concluir a ação em lote. Tente novamente.'
/** The irreversible-only body of the mass delete (no published piece in the selection). */
export const CONTENT_PIECE_BATCH_DELETE_IRREVERSIBLE_MESSAGE = 'Esta ação não pode ser desfeita.'
/** The body that warns about the public links when the selection has published pieces. */
export const CONTENT_PIECE_BATCH_DELETE_LINKS_MESSAGE =
'Os links públicos das peças publicadas da seleção deixam de funcionar para quem já recebeu. Esta ação não pode ser desfeita.'
/** `3 selecionadas` / `1 selecionada` (the TourComposerForm precedent). */
export const contentPieceSelectionLabel = (count: number): string =>
`${count} ${count === 1 ? 'selecionada' : 'selecionadas'}`
const PARTICIPLES: Record<ContentPieceBatchAction, readonly [string, string]> = {
publicar: ['publicada', 'publicadas'],
despublicar: ['despublicada', 'despublicadas'],
apagar: ['apagada', 'apagadas'],
}
/**
* The honest receipt: an all-success outcome is one phrase; a partial one
* always separates affected from failures and never says "3 peças apagadas"
* when one failed.
*/
export const contentPieceBatchReceiptMessage = (outcome: ContentPieceBatchOutcome): string => {
const [singular, plural] = PARTICIPLES[outcome.action]
if (outcome.failures.length === 0) {
return `${outcome.affected} ${outcome.affected === 1 ? `peça ${singular}` : `peças ${plural}`}`
}
const total = outcome.affected + outcome.failures.length
const failed =
outcome.failures.length === 1 ? '1 falhou.' : `${outcome.failures.length} falharam.`
const noun = total === 1 ? 'peça' : 'peças'
return `${outcome.affected} de ${total} ${noun} ${total === 1 ? singular : plural}. ${failed}`
}
/** `Apagar 3 peças selecionadas?` — the singular reuses the C222 literal. */
export const contentPieceBatchDeleteTitle = (count: number): string =>
count === 1 ? 'Apagar esta peça?' : `Apagar ${count} peças selecionadas?`