mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
Add admin CSV export for signatures and contacts.
Enables @payloadcms/plugin-import-export on signature (flattened contact/petition columns) and contact (native fields) so operations can download filtered lists for external ESP/SMS workflows.
This commit is contained in:
parent
a909e5a275
commit
d93e2aa72f
14 changed files with 11616 additions and 5 deletions
|
|
@ -57,6 +57,8 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
|
|||
|
||||
**Recently resolved (2026-07-21):** **Discovery "Inteligência de campanha"** — ciclo literatura→persona→entrevista sintética aprovado. Embasamento canônico de produto/domínio em **`docs/research/`** (compêndio de ~67 fontes sobre campanha de DF na Bahia; relatório com kernel, playbook de 25 padrões dado→decisão, anti-goals): agentes devem consultá-lo antes de propor métricas/análises territoriais novas. Programa registrado no roadmap: **E8–E15 + B13 + C12** (plano-mestre `docs/plans/inteligencia-campanha.md`) — conta da cadeira (metas derivadas sobre válidos projetados), fila de alocação, registro-fundação (versions em `votePledge`, sinais tipados, `allocationDecision`), classificação relativa (substitui limiares 35/20/10 p/ DF), escala relativa no mapa (quantis/LQ/rank + símbolo proporcional), níveis N0–N4 staff-only, motor de sugestões com humano no loop, camada TI (salvaguardas MAUP), planejador de giros, backtest pós-eleição. Anti-metas travadas: % estadual absoluto como KPI, contagem bruta de cadastros, cartograma, "caça ao alienado", sugestão automática sem decisão humana.
|
||||
|
||||
**Recently resolved (2026-07-23):** **Export CSV de assinaturas e contatos** no admin Payload — `@payloadcms/plugin-import-export@3.82.0` habilitado em `signature` e `contact` (`import: false`, export CSV síncrono); flatten de `contact`/`petition` via `toCSV` em [`src/utilities/signatureExport.ts`](src/utilities/signatureExport.ts) (só assinaturas); `consent` excluído do export de assinaturas; collections `exports`/`imports` com access `isPayloadAdmin`; migration `20260723_025513_add_import_export_plugin`. Plano: [`docs/plans/exportar-csv-assinaturas.md`](docs/plans/exportar-csv-assinaturas.md).
|
||||
|
||||
## Posts & Tags (news / publications)
|
||||
|
||||
The public news/publications system is backed by two collections (both in the `Publicações` admin group) plus the helpers in `src/utilities/posts.ts`.
|
||||
|
|
|
|||
152
docs/plans/exportar-csv-assinaturas.md
Normal file
152
docs/plans/exportar-csv-assinaturas.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Exportar CSV das assinaturas e contatos (admin Payload)
|
||||
|
||||
Status: entregue 2026-07-23
|
||||
Atualizado em: 2026-07-23
|
||||
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Admin Payload — export CSV de `signature` e `contact`)
|
||||
Impeccable: B — encaixe na list view admin via plugin oficial (sem rota `/campanha` nova)
|
||||
Appetite: ~0,5–1 dia eng; plugin `@3.82.0` alinhado ao core (sem bump) + flatten de `contact`/`petition` em `signature` + migration das collections do plugin
|
||||
Responsável: —
|
||||
|
||||
_Revisão 2026-07-23: implementação com `@payloadcms/plugin-import-export@3.82.0` (peer dep bate com Payload 3.82 — bump desnecessário). API 3.82 usa `toCSV` nos campos (não `hooks.beforeExport` da doc ≥3.85). Migration `20260723_025513_add_import_export_plugin`; helpers em `src/utilities/signatureExport.ts`. Pós-entrega: export CSV também em `contact` (campos nativos, sem flatten) via `adminCsvExportCollection()` em `payload.config.ts`._
|
||||
|
||||
## Design (Impeccable)
|
||||
|
||||
Âncoras: admin Payload nativo (grupo `Abaixo-assinados`); **não** Field Desk / `data-theme='campaign'`. Labels pt-BR nas strings visíveis do plugin quando configuráveis.
|
||||
|
||||
Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (smoke no `/admin/collections/signature`: filtrar petição → Exportar → CSV abrível). Sem shape longo.
|
||||
|
||||
Brief compacto:
|
||||
|
||||
- **Persona / contexto:** operação/marketing no `/admin` precisa da lista de assinantes (e-mail/celular) para disparo externo sobre o abaixo-assinado.
|
||||
- **Job principal:** baixar um CSV filtrável das assinaturas com PII de contato já achatada, sem sair do Payload.
|
||||
- **Estratégia de cor:** Restrained (chrome do admin Payload; sem paleta nova).
|
||||
- **Edit where you see:** N/A (só leitura/export; mutação de assinaturas não é o job).
|
||||
- **Anti-goals:** import em massa de assinaturas; botão custom fora do plugin; disparo de e-mail/SMS **dentro** do Teqo; segundo design system.
|
||||
|
||||
## Dados → decisão → apresentação
|
||||
|
||||
- **Vou apresentar dados?** Sim, superfície neste item (arquivo CSV baixado no admin).
|
||||
- **Decisões desbloqueadas:**
|
||||
- Operação/marketing: “estes assinantes entram no disparo de e-mail/SMS desta petição?”
|
||||
- Operação: “filtro por abaixo-assinado X antes de exportar — conjunto certo?”
|
||||
- **Forma escolhida:** tabela CSV (degrau tabela/lista) — **por quê:** ferramentas externas de e-mail/SMS consomem CSV/planilha; é o contrato operacional. **Rejeitado:** dashboard/KPI de assinaturas nesta fatia; chart; export só JSON; página pública de download.
|
||||
- **Profile:** categórico + PII textual; granularidade = 1 linha por `signature`; tamanho típico = milhares (não centenas de mil); absoluto (lista nominal, não %).
|
||||
- **Anti-goals de dado:** sem vanity “total de leads” como substituto do CSV; sem incluir campos internos irrelevantes ao disparo (`consent` richText, locks); sem vazar para quem não é `users` admin.
|
||||
|
||||
## Contexto
|
||||
|
||||
Assinaturas de abaixo-assinados vivem na collection `signature` (`src/collections/Signature.ts`): join `contact` + `petition` + `consent` (+ `comment` opcional). O PII útil para disparo (nome, e-mail, celular, UF/cidade) está em `contact` (`src/collections/Contact.ts`), não nas colunas da assinatura. Hoje o admin lista relacionamentos por ID/título, **sem** botão de export.
|
||||
|
||||
O time pediu (2026-07-22) um botão no admin Payload para exportar CSV das assinaturas, a ser usado em disparos de e-mail e SMS **fora** deste app. O usuário suspeitou corretamente de feature nativa: Payload oferece [`@payloadcms/plugin-import-export`](https://payloadcms.com/docs/plugins/import-export) (CSV/JSON na list view; saiu de beta no release 3.85). O app está em Payload `3.82.0` — a implementação deve alinhar a versão do plugin ao core (preferir bump ≥3.85 se o pacote estável exigir).
|
||||
|
||||
Este app **não** dispara e-mail/SMS em massa (roadmap: fora de escopo WhatsApp Business / disparo em massa; Res. TSE 23.610). O produto só entrega o arquivo; o canal externo fica com a operação.
|
||||
|
||||
## Objetivos
|
||||
|
||||
- Na list view de **Assinaturas** (`/admin/collections/signature`): controles de export CSV (download direto).
|
||||
- Na list view de **Contatos** (`/admin/collections/contact`): export CSV com campos nativos (`name`, `email`, `phone`, `gender`, `state`, `city`, `postalCode`, `createdAt`) — sem flatten.
|
||||
- CSV de assinaturas com colunas achatadas de `contact` úteis a ESP/SMS (no mínimo nome, e-mail, telefone; preferir também UF/cidade, id/título da petição, `createdAt`, comentário).
|
||||
- Respeitar filtros da list view (exportar só o conjunto filtrado — tipicamente um `petition` em assinaturas).
|
||||
- **Só export** em `signature` e `contact` (`import: false`); não habilitar o plugin em collections irrelevantes.
|
||||
- Guardrails: access só para sessão `users` do admin; migration para collections `exports`/`imports` que o plugin cria; `push: false`; sem novo `Consent` key (titular já consentiu na assinatura); sem jobs queue nesta fatia se evitável (`disableJobsQueue: true`).
|
||||
|
||||
## Decisões travadas
|
||||
|
||||
- **Usar o plugin oficial `@payloadcms/plugin-import-export`, não endpoint/botão caseiro.** Já resolve UI admin, preview, CSV, depth de relationships e (opcional) fila. **Rejeitado:** custom `admin.components` + route handler CSV (duplica o que o plugin faz); CLI-only (`pnpm` script) — fora do fluxo “botão no admin”; plugin community antigo `madaxen86/payload-plugin-import-export` — preferir o oficial alinhado ao core.
|
||||
- **Escopo v1 = `signature` + `contact`, com `import: false` e `export.format: 'csv'`.** Import de assinaturas reinventaria opt-in/Consent e risco de duplicata Contact. `contact` exporta campos planos; `signature` achata relationships via `toCSV`. **Rejeitado:** plugin em todas as collections; import habilitado “por se”; JSON como formato default (CSV é o pedido operacional).
|
||||
- **Achatamento de `contact`/`petition` em `signature` via `toCSV` do plugin** (relationship → colunas `contact_name`, `contact_email`, `contact_phone`, …). **Rejeitado:** exportar só IDs de relacionamento (inútil para ESP); denormalizar PII na row `signature` (migration cara, duplicação).
|
||||
- **v1 síncrono (`disableJobsQueue: true`)** — o projeto ainda não tem `jobs.autoRun`/runner; fila deixaria exports em “pending”. **Rejeitado:** montar cron/jobs só para este botão nesta fatia. Gatilho para fila: volume que estoure timeout de request ou pedido explícito de save-to-`exports`.
|
||||
- **Sem disparo in-app.** CSV é handoff para ferramenta externa. **Rejeitado:** Resend em massa no Teqo; integração Twilio/SMS; “campanha” de e-mail no Payload.
|
||||
- **i18n e naming:** identificadores em inglês (`importExportPlugin`, hooks `beforeExport`); labels admin em pt-BR (“Assinaturas”, “Exportar”).
|
||||
|
||||
## Questões em aberto
|
||||
|
||||
- **Bump de Payload 3.82 → ≥3.85 junto com o plugin?** **Resolvido 2026-07-23:** `@payloadcms/plugin-import-export@3.82.0` instalado sem bump — peer dep `payload@3.82.0` confirmado no npm.
|
||||
- **Conjunto exato de colunas do CSV?** **Opções:** mínimo (nome/e-mail/telefone) | operacional (mínimo + petition id/title + createdAt + city/state + comment). **Recomendação:** operacional — um export serve e-mail e SMS sem segundo passo. Confirmar com quem dispara se e-mail vazio deve aparecer como linha (telefone é required no Contact; e-mail é opcional).
|
||||
- **Visibilidade das collections `exports`/`imports` no sidebar?** **Opções:** ocultas (default do plugin) | grupo admin “Abaixo-assinados” / “Ferramentas”. **Recomendação:** manter ocultas no v1 (`disableSave: true` se só download direto); abrir sidebar só se operação pedir histórico de arquivos salvos.
|
||||
|
||||
## Abordagem proposta
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Admin["/admin Assinaturas<br/>filtros petition"]
|
||||
Plugin["importExportPlugin<br/>signature export CSV"]
|
||||
Hooks["beforeExport<br/>flatten contact"]
|
||||
CSV["Download CSV"]
|
||||
Ext["ESP / SMS externo"]
|
||||
|
||||
Admin --> Plugin
|
||||
Plugin --> Hooks
|
||||
Hooks --> CSV
|
||||
CSV --> Ext
|
||||
```
|
||||
|
||||
Componentes:
|
||||
|
||||
- **`importExportPlugin`** em `src/payload.config.ts`: `adminCsvExportCollection('signature' | 'contact')` com `export: { format: 'csv', disableJobsQueue: true, disableSave: true }`, `import: false`. Access nas collections geradas via `overrideExportCollection` restrito a `users` autenticados (espelhar padrão admin).
|
||||
- **Hooks de campo / collection** em `src/collections/Signature.ts` (ou helper `src/utilities/signatureExport.ts` se o arquivo da collection ficar gordo): `custom['plugin-import-export'].hooks.beforeExport` no relationship `contact` (e, se útil, `petition`) para colunas planas; `consent` com `disabled: true` no custom do plugin (não exportar texto jurídico versionado).
|
||||
- **Depth / populate:** garantir que o export carregue `contact` e `petition` populados o suficiente para o flatten (seguir default do plugin; ajustar se IDs crus aparecerem no preview).
|
||||
- **Bump de pacotes** (se necessário): `payload` + `@payloadcms/*` alinhados; `pnpm generate:types` / `generate:importmap`.
|
||||
- **Migration:** o plugin adiciona collections `exports` / `imports` (uploads) — `pnpm migrate:create add_import_export_plugin` (nome final na implementação) e `pnpm migrate` local. Sem alterar schema de `signature`/`contact`.
|
||||
- **Testes:** unit do flatten (dado `contact` populado → colunas esperadas); smoke manual admin (filtro por petição → CSV). Int opcional se houver harness de plugin; não bloquear appetite com e2e Playwright do admin.
|
||||
|
||||
## Dependências
|
||||
|
||||
- Nenhuma dura de outro plano de trilha A/B/C/D.
|
||||
- Suave: RBAC em `users` (bloqueador AGENTS #1) — hoje todo admin `users` já tem acesso pleno; quando houver roles, reaplicar access no override das collections do plugin.
|
||||
- Suave: textos finais de Consent / privacidade (Onda 0) — não bloqueiam o botão; o titular já optou na assinatura. Operação deve usar o CSV só para fins compatíveis com o consentimento coletado _(assumido — validar com jurídico se o texto atual cobre comunicação sobre o abaixo-assinado)_.
|
||||
|
||||
## Não escopo
|
||||
|
||||
- Disparo de e-mail/SMS dentro do Teqo (fora de escopo do roadmap / Res. TSE).
|
||||
- Export de `subscription`, `supporter`, `leadership` (itens separados se pedido).
|
||||
- Import CSV de assinaturas / reimport.
|
||||
- CAPI / Pixel (já em [pixel-meta-abaixo-assinado.md](pixel-meta-abaixo-assinado.md)).
|
||||
- Jobs queue / cron / save histórico em `exports` com blob (adiado abaixo).
|
||||
- UI em `/campanha` para assinaturas públicas.
|
||||
|
||||
## Rabbit holes
|
||||
|
||||
- **Habilitar `import: true` “só para reprocessar”.** Explode Consent, dedup de telefone e joins. **Mitigação:** `import: false` travado neste plano.
|
||||
- **Plugin em todas as collections “já que instalou”.** Expõe PII de campanha sem pedido. **Mitigação:** whitelist só `signature` e `contact`.
|
||||
- **Montar `jobs.autoRun` + runner Vercel só para export.** Fora do appetite. **Mitigação:** `disableJobsQueue: true`; gatilho abaixo.
|
||||
- **Planilha tipo CRM no admin (colunas editáveis, bulk).** Não é export. **Mitigação:** CSV download only.
|
||||
|
||||
## Adiado com gatilho
|
||||
|
||||
- **Jobs queue + save em `exports` (Vercel Blob).** Revisitar quando: export síncrono estourar timeout / operação pedir arquivo versionado no admin.
|
||||
- **Access fino por role de `users`.** Revisitar quando: item “RBAC em `users`” do Admin Payload for implementado.
|
||||
- **Export de `subscription` (base WhatsApp/site).** Revisitar quando: operação pedir lista transversal fora de petição específica.
|
||||
- **`defaultPopulate` nas collections relacionadas (`contact`, `petition`, `consent`).** Revisitar quando: export síncrono ficar lento ou estourar timeout — evitar carregar `petition.body` / `consent.text` no `depth:1` default do plugin.
|
||||
- **`contactFromRelationship` compartilhado com `supporterViewModels`.** Revisitar quando: 3º call site de “contact populado de relationship”.
|
||||
- **Constante `importExportConsentDisabled` para fields `consent`.** Revisitar quando: export de `subscription` (2º campo `consent` com `disabled: true`).
|
||||
|
||||
## Já resolvido no simplify (não reabrir)
|
||||
|
||||
- Consolidar flatten em `signatureContactToCSV` / `signaturePetitionToCSV` (sem wrappers `flatten*` exportados).
|
||||
- Testes via `runToCSV` nos handlers `toCSV` de produção.
|
||||
- Nullable consistente (`?? ''`) nos campos de contato do export de assinaturas.
|
||||
- `withImportExportAdminAccess` + `importExportAdminOnly` (access DRY nas collections `exports`/`imports`).
|
||||
- `adminCsvExportCollection()` (config DRY entre `signature` e `contact`).
|
||||
- `isPopulatedRelationship<Contact>` direto (sem `isPopulatedContact` redundante).
|
||||
|
||||
## Explicitamente fora (triage pós-entrega 2026-07-23)
|
||||
|
||||
- **`export.limit` no plugin** — sem evidência de abuso em v1; reabrir com profiling.
|
||||
- **Populate de `consent` no depth do plugin** — excluído do CSV; custo marginal do batch do plugin.
|
||||
- **Factory genérica `plugin-import-export` nos 3 fields de `Signature`** — YAGNI (1 collection).
|
||||
- **Módulo `importExportPlugin.ts` separado do `payload.config`** — organizacional; parcialmente coberto por `adminCsvExportCollection`.
|
||||
- **Variante `isPopulatedRelationship` para id string (`Petition`)** — 1 consumidor; guard local em `signatureExport.ts` suficiente.
|
||||
- **Colunas `contact_*` compartilhadas entre export de `contact` e `signature`** — schemas intencionalmente diferentes (`name` vs `contact_name`).
|
||||
- **Usar `canManageContacts` no access do plugin** — semântica errada; manteve `isPayloadAdmin`.
|
||||
|
||||
## Referências
|
||||
|
||||
- `docs/roadmap.md` (Admin Payload; Site público — abaixo-assinados)
|
||||
- [Payload Import Export Plugin](https://payloadcms.com/docs/plugins/import-export) — config `export`/`import`, `disableJobsQueue`, `beforeExport`
|
||||
- `src/collections/Signature.ts` — collection alvo
|
||||
- `src/collections/Contact.ts` — campos a achatar
|
||||
- `src/app/(frontend)/actions/submitPetitionSignature.ts` — origem dos dados (Contact + Signature + Subscription na mesma TX)
|
||||
- `src/payload.config.ts` — registro do plugin
|
||||
- `docs/plans/pixel-meta-abaixo-assinado.md` — precedente recente da vertical abaixo-assinados no admin
|
||||
- AGENTS.md — naming; `push: false` + migrations; LGPD/Consent (sem nova key); Known Gap #1 RBAC admin
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Roadmap — Teqo
|
||||
|
||||
Atualizado em: 2026-07-21 (programa **Inteligência de campanha** E8–E15/B13/C12 registrado pós-discovery — [plano-mestre](plans/inteligencia-campanha.md), embasamento [docs/research/](research/); B12/B6/B8 F1/C8 F4/A9+/B7/B9–B11/filtros-auto entregues)
|
||||
Atualizado em: 2026-07-23 (Admin Payload: export CSV de assinaturas e contatos entregue — [plano](plans/exportar-csv-assinaturas.md))
|
||||
|
||||
Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc).
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ Paralelizáveis agora: fill-ins (O0+, RS+, …). ~~**A9** / **A9+** / **B9** / *
|
|||
|
||||
**Não cortáveis:** Onda 0 (jurídico/Consent); R1–R2 (sem eles a vertical não reflete a operação real); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**A9**~~ (total esperado da Praça — entregue 2026-07-21); **E8**+**E9**+**C12** (a conta da cadeira, a fila e o registro ex-ante são o mínimo de "inteligência, não planilha" — e C12 é irrecuperável se não registrar durante a campanha).
|
||||
|
||||
**Cortes seguros** (se o prazo apertar, nesta ordem): **E13** planejador de giros (agenda segue manual com J-A/J-B como guia); **E12** camada TI (rollup manual por lista); **E15** (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter fila E9 sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** (manter `priority` alta/normal); R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; **B8** (F2 polígonos; manter F1 bairros na Praça se já entregue — mapa continua agregado no município); débitos/fill-ins. ~~**B9** / **B10** / **B11** / **B6** / **B12**~~ (entregues — não cortar).
|
||||
**Cortes seguros** (se o prazo apertar, nesta ordem): **E13** planejador de giros (agenda segue manual com J-A/J-B como guia); **E12** camada TI (rollup manual por lista); **E15** (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter fila E9 sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** (manter `priority` alta/normal); R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; **B8** (F2 polígonos; manter F1 bairros na Praça se já entregue — mapa continua agregado no município); débitos/fill-ins. ~~**B9** / **B10** / **B11** / **B6** / **B12**~~ / ~~**export CSV de assinaturas/contatos**~~ (entregues — não cortar).
|
||||
|
||||
## Já entregue (resumo)
|
||||
|
||||
|
|
@ -165,6 +165,7 @@ Paralelizáveis agora: fill-ins (O0+, RS+, …). ~~**A9** / **A9+** / **B9** / *
|
|||
- **B8 F1 (2026-07-21)** — **Catálogo bairros das Praças-zona** (`plazaZoneNeighborhoods`: Salvador TRE-BA RA 02/2017 + Camaçari curado; fixture+int; card **Bairros desta Praça** no overview de Praças `kind=zona`; sem migration) — [plano](plans/poligonos-pracas-zona.md). **F2 pendente** (F2 prep catálogo ~½ dia + TopoJSON + mapa). Débitos pós-`/simplify`: hidratação via `plazaCatalog`, canônico JSON→TS, teste ordem slugs → F2 prep no plano (S6 CSS compartilhado adiado até F2/R6).
|
||||
- **Fill-in filtros-auto (2026-07-21)** — **Filtros auto-aplicados na lista de Praças** (`PlazaFilters`: debounce 1s no `q`, Enter imediato, selects imediatos, remove Buscar; `useTransition` + pending a11y; `shouldUpdatePlazaSearchUrl` + no-op via `buildPlazaFiltersKey`; sem migration) — [plano](plans/filtros-auto-pracas.md). Débitos pós-`/simplify`: sync back/forward `search`↔`state.q` e shell pending compartilhado — gatilhos no plano.
|
||||
- **Fill-in C8 F4 (2026-07-21)** — **DRY plaza staff form actions** (`plazaStaffFormActions.ts`: votos estimados, tendência e assessores compartilhados entre lista e `/editar`; `editar/formActions.ts` mantém só `updatePlazaStrategy`; sem migration) — [plano](plans/escala-dry-pos-c6.md).
|
||||
- **Admin Payload (2026-07-23)** — **Export CSV de assinaturas e contatos** (`@payloadcms/plugin-import-export@3.82.0` em `signature` + `contact`; export CSV síncrono; flatten de `contact`/`petition` via `toCSV` em assinaturas; campos nativos em contatos; `consent` excluído; access `exports`/`imports` restrito a `users`; migration `20260723_025513_add_import_export_plugin`) — [plano](plans/exportar-csv-assinaturas.md).
|
||||
|
||||
## Supersedidos pela remodelagem (2026-07-20)
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
"@payloadcms/email-resend": "3.82.0",
|
||||
"@payloadcms/live-preview-react": "3.82.0",
|
||||
"@payloadcms/next": "3.82.0",
|
||||
"@payloadcms/plugin-import-export": "3.82.0",
|
||||
"@payloadcms/richtext-lexical": "3.82.0",
|
||||
"@payloadcms/storage-vercel-blob": "3.82.0",
|
||||
"@payloadcms/ui": "3.82.0",
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ importers:
|
|||
'@payloadcms/next':
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@payloadcms/plugin-import-export':
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(@payloadcms/ui@3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@payloadcms/richtext-lexical':
|
||||
specifier: 3.82.0
|
||||
version: 3.82.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.29)
|
||||
|
|
@ -1730,6 +1733,12 @@ packages:
|
|||
react: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
|
||||
|
||||
'@payloadcms/plugin-import-export@3.82.0':
|
||||
resolution: {integrity: sha512-wcGr0Zzl4bqq8S+z66dXBV4P4wjFyo0jO9Q/cfPUZWNFQ+pGFErjyVKilNguz/uvPKO0OewSbIHp90QlgsXloQ==}
|
||||
peerDependencies:
|
||||
'@payloadcms/ui': 3.82.0
|
||||
payload: 3.82.0
|
||||
|
||||
'@payloadcms/richtext-lexical@3.82.0':
|
||||
resolution: {integrity: sha512-hdLeG7C1fXe7xUJcYf2Nwhq1OzMl7bt5qCqp6ynnXFuFlCJwUJA0lcCxNeJWCtgABFC7BiSXGnCvIYrk1DKN1Q==}
|
||||
engines: {node: ^18.20.2 || >=20.9.0}
|
||||
|
|
@ -3756,9 +3765,15 @@ packages:
|
|||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
csv-parse@5.6.0:
|
||||
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
|
||||
|
||||
csv-parse@7.0.1:
|
||||
resolution: {integrity: sha512-+2z7Ar0APQ7Uu6fX4cn+pitRmxjZ1WPBcGmZFKmA74FCyi7Et/XZx8cjNQ5CjbZ4HCOxXCOpRBYvYH08Qa003A==}
|
||||
|
||||
csv-stringify@6.5.2:
|
||||
resolution: {integrity: sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
|
|
@ -8207,6 +8222,19 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@payloadcms/plugin-import-export@3.82.0(@payloadcms/ui@3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
'@faceless-ui/modal': 3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@payloadcms/translations': 3.82.0
|
||||
'@payloadcms/ui': 3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
csv-parse: 5.6.0
|
||||
csv-stringify: 6.5.2
|
||||
payload: 3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0)
|
||||
qs-esm: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
- react-dom
|
||||
|
||||
'@payloadcms/richtext-lexical@3.82.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.82.0(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@types/react@19.2.14)(@typescript/typescript6@6.0.2)(monaco-editor@0.55.1)(next@15.4.11(@babel/core@7.29.0)(@playwright/test@1.58.2)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.82.0(@typescript/typescript6@6.0.2)(graphql@16.12.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.29)':
|
||||
dependencies:
|
||||
'@faceless-ui/modal': 3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -10241,8 +10269,12 @@ snapshots:
|
|||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
csv-parse@5.6.0: {}
|
||||
|
||||
csv-parse@7.0.1: {}
|
||||
|
||||
csv-stringify@6.5.2: {}
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,21 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
|
|||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ExportListMenuItem as ExportListMenuItem_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { FormatField as FormatField_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { LimitField as LimitField_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { Page as Page_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { SortBy as SortBy_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { SortOrder as SortOrder_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { SelectionToUseField as SelectionToUseField_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { FieldsToExport as FieldsToExport_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { CollectionField as CollectionField_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { ExportPreview as ExportPreview_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { ExportSaveButton as ExportSaveButton_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { ImportPreview as ImportPreview_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { ImportSaveButton as ImportSaveButton_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { VercelBlobClientUploadHandler as VercelBlobClientUploadHandler_16c82c5e25f430251a3e3ba57219ff4e } from '@payloadcms/storage-vercel-blob/client'
|
||||
import { ImportExportProvider as ImportExportProvider_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
export const importMap = {
|
||||
|
|
@ -48,6 +62,20 @@ export const importMap = {
|
|||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/plugin-import-export/rsc#ExportListMenuItem": ExportListMenuItem_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#FormatField": FormatField_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#LimitField": LimitField_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#Page": Page_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#SortBy": SortBy_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#SortOrder": SortOrder_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#SelectionToUseField": SelectionToUseField_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#FieldsToExport": FieldsToExport_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#CollectionField": CollectionField_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#ExportPreview": ExportPreview_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#ExportSaveButton": ExportSaveButton_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#ImportPreview": ImportPreview_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/plugin-import-export/rsc#ImportSaveButton": ImportSaveButton_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/storage-vercel-blob/client#VercelBlobClientUploadHandler": VercelBlobClientUploadHandler_16c82c5e25f430251a3e3ba57219ff4e,
|
||||
"@payloadcms/plugin-import-export/rsc#ImportExportProvider": ImportExportProvider_cdf7e044479f899a31f804427d568b36,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { signatureContactToCSV, signaturePetitionToCSV } from '@/utilities/signatureExport'
|
||||
import { CollectionConfig } from 'payload'
|
||||
|
||||
export const Signature: CollectionConfig = {
|
||||
|
|
@ -16,6 +17,11 @@ export const Signature: CollectionConfig = {
|
|||
label: 'Contato',
|
||||
required: true,
|
||||
relationTo: 'contact',
|
||||
custom: {
|
||||
'plugin-import-export': {
|
||||
toCSV: signatureContactToCSV,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
|
|
@ -23,6 +29,11 @@ export const Signature: CollectionConfig = {
|
|||
label: 'Abaixo-assinado',
|
||||
required: true,
|
||||
relationTo: 'petition',
|
||||
custom: {
|
||||
'plugin-import-export': {
|
||||
toCSV: signaturePetitionToCSV,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
|
|
@ -30,6 +41,11 @@ export const Signature: CollectionConfig = {
|
|||
label: 'Consentimento',
|
||||
required: true,
|
||||
relationTo: 'consent',
|
||||
custom: {
|
||||
'plugin-import-export': {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'comment',
|
||||
|
|
|
|||
10899
src/migrations/20260723_025513_add_import_export_plugin.json
Normal file
10899
src/migrations/20260723_025513_add_import_export_plugin.json
Normal file
File diff suppressed because it is too large
Load diff
98
src/migrations/20260723_025513_add_import_export_plugin.ts
Normal file
98
src/migrations/20260723_025513_add_import_export_plugin.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
CREATE TYPE "public"."enum_exports_format" AS ENUM('csv', 'json');
|
||||
CREATE TYPE "public"."enum_exports_sort_order" AS ENUM('asc', 'desc');
|
||||
CREATE TYPE "public"."enum_exports_drafts" AS ENUM('yes', 'no');
|
||||
CREATE TYPE "public"."enum_imports_import_mode" AS ENUM('create', 'update', 'upsert');
|
||||
CREATE TYPE "public"."enum_imports_status" AS ENUM('pending', 'completed', 'partial', 'failed');
|
||||
ALTER TYPE "public"."enum_payload_jobs_log_task_slug" ADD VALUE 'createCollectionExport' BEFORE 'schedulePublish';
|
||||
ALTER TYPE "public"."enum_payload_jobs_log_task_slug" ADD VALUE 'createCollectionImport' BEFORE 'schedulePublish';
|
||||
ALTER TYPE "public"."enum_payload_jobs_task_slug" ADD VALUE 'createCollectionExport' BEFORE 'schedulePublish';
|
||||
ALTER TYPE "public"."enum_payload_jobs_task_slug" ADD VALUE 'createCollectionImport' BEFORE 'schedulePublish';
|
||||
CREATE TABLE "exports" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar,
|
||||
"format" "enum_exports_format" DEFAULT 'csv' NOT NULL,
|
||||
"limit" numeric,
|
||||
"page" numeric DEFAULT 1,
|
||||
"sort" varchar,
|
||||
"sort_order" "enum_exports_sort_order",
|
||||
"drafts" "enum_exports_drafts" DEFAULT 'yes',
|
||||
"collection_slug" varchar DEFAULT 'signature' NOT NULL,
|
||||
"where" jsonb DEFAULT '{}'::jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"url" varchar,
|
||||
"thumbnail_u_r_l" varchar,
|
||||
"filename" varchar,
|
||||
"mime_type" varchar,
|
||||
"filesize" numeric,
|
||||
"width" numeric,
|
||||
"height" numeric,
|
||||
"focal_x" numeric,
|
||||
"focal_y" numeric
|
||||
);
|
||||
|
||||
CREATE TABLE "exports_texts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer NOT NULL,
|
||||
"parent_id" integer NOT NULL,
|
||||
"path" varchar NOT NULL,
|
||||
"text" varchar
|
||||
);
|
||||
|
||||
CREATE TABLE "imports" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"collection_slug" varchar NOT NULL,
|
||||
"import_mode" "enum_imports_import_mode",
|
||||
"match_field" varchar DEFAULT 'id',
|
||||
"status" "enum_imports_status" DEFAULT 'pending',
|
||||
"summary_imported" numeric,
|
||||
"summary_updated" numeric,
|
||||
"summary_total" numeric,
|
||||
"summary_issues" numeric,
|
||||
"summary_issue_details" jsonb,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"url" varchar,
|
||||
"thumbnail_u_r_l" varchar,
|
||||
"filename" varchar,
|
||||
"mime_type" varchar,
|
||||
"filesize" numeric,
|
||||
"width" numeric,
|
||||
"height" numeric,
|
||||
"focal_x" numeric,
|
||||
"focal_y" numeric
|
||||
);
|
||||
|
||||
ALTER TABLE "exports_texts" ADD CONSTRAINT "exports_texts_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."exports"("id") ON DELETE cascade ON UPDATE no action;
|
||||
CREATE INDEX "exports_updated_at_idx" ON "exports" USING btree ("updated_at");
|
||||
CREATE INDEX "exports_created_at_idx" ON "exports" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "exports_filename_idx" ON "exports" USING btree ("filename");
|
||||
CREATE INDEX "exports_texts_order_parent" ON "exports_texts" USING btree ("order","parent_id");
|
||||
CREATE INDEX "imports_updated_at_idx" ON "imports" USING btree ("updated_at");
|
||||
CREATE INDEX "imports_created_at_idx" ON "imports" USING btree ("created_at");
|
||||
CREATE UNIQUE INDEX "imports_filename_idx" ON "imports" USING btree ("filename");`)
|
||||
}
|
||||
|
||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`
|
||||
DROP TABLE "exports" CASCADE;
|
||||
DROP TABLE "exports_texts" CASCADE;
|
||||
DROP TABLE "imports" CASCADE;
|
||||
ALTER TABLE "payload_jobs_log" ALTER COLUMN "task_slug" SET DATA TYPE text;
|
||||
DROP TYPE "public"."enum_payload_jobs_log_task_slug";
|
||||
CREATE TYPE "public"."enum_payload_jobs_log_task_slug" AS ENUM('inline', 'schedulePublish');
|
||||
ALTER TABLE "payload_jobs_log" ALTER COLUMN "task_slug" SET DATA TYPE "public"."enum_payload_jobs_log_task_slug" USING "task_slug"::"public"."enum_payload_jobs_log_task_slug";
|
||||
ALTER TABLE "payload_jobs" ALTER COLUMN "task_slug" SET DATA TYPE text;
|
||||
DROP TYPE "public"."enum_payload_jobs_task_slug";
|
||||
CREATE TYPE "public"."enum_payload_jobs_task_slug" AS ENUM('inline', 'schedulePublish');
|
||||
ALTER TABLE "payload_jobs" ALTER COLUMN "task_slug" SET DATA TYPE "public"."enum_payload_jobs_task_slug" USING "task_slug"::"public"."enum_payload_jobs_task_slug";
|
||||
DROP TYPE "public"."enum_exports_format";
|
||||
DROP TYPE "public"."enum_exports_sort_order";
|
||||
DROP TYPE "public"."enum_exports_drafts";
|
||||
DROP TYPE "public"."enum_imports_import_mode";
|
||||
DROP TYPE "public"."enum_imports_status";`)
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import * as migration_20260719_061302_add_campaign_user_avatar from './20260719_
|
|||
import * as migration_20260721_020109_remodel_plazas from './20260721_020109_remodel_plazas';
|
||||
import * as migration_20260721_133444_add_plaza_expected_votes from './20260721_133444_add_plaza_expected_votes';
|
||||
import * as migration_20260721_133531_add_petition_facebook_pixel_id from './20260721_133531_add_petition_facebook_pixel_id';
|
||||
import * as migration_20260723_025513_add_import_export_plugin from './20260723_025513_add_import_export_plugin';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -120,4 +121,9 @@ export const migrations = [
|
|||
down: migration_20260721_133531_add_petition_facebook_pixel_id.down,
|
||||
name: '20260721_133531_add_petition_facebook_pixel_id',
|
||||
},
|
||||
{
|
||||
up: migration_20260723_025513_add_import_export_plugin.up,
|
||||
down: migration_20260723_025513_add_import_export_plugin.down,
|
||||
name: '20260723_025513_add_import_export_plugin'
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ export interface Config {
|
|||
subscription: Subscription;
|
||||
post: Post;
|
||||
tag: Tag;
|
||||
exports: Export;
|
||||
imports: Import;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-jobs': PayloadJob;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
|
|
@ -122,6 +124,8 @@ export interface Config {
|
|||
subscription: SubscriptionSelect<false> | SubscriptionSelect<true>;
|
||||
post: PostSelect<false> | PostSelect<true>;
|
||||
tag: TagSelect<false> | TagSelect<true>;
|
||||
exports: ExportsSelect<false> | ExportsSelect<true>;
|
||||
imports: ImportsSelect<false> | ImportsSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
|
|
@ -151,6 +155,8 @@ export interface Config {
|
|||
user: User | CampaignUser;
|
||||
jobs: {
|
||||
tasks: {
|
||||
createCollectionExport: TaskCreateCollectionExport;
|
||||
createCollectionImport: TaskCreateCollectionImport;
|
||||
schedulePublish: TaskSchedulePublish;
|
||||
inline: {
|
||||
input: unknown;
|
||||
|
|
@ -892,6 +898,80 @@ export interface Tag {
|
|||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "exports".
|
||||
*/
|
||||
export interface Export {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
format: 'csv' | 'json';
|
||||
limit?: number | null;
|
||||
page?: number | null;
|
||||
sort?: string | null;
|
||||
sortOrder?: ('asc' | 'desc') | null;
|
||||
drafts?: ('yes' | 'no') | null;
|
||||
selectionToUse?: ('currentSelection' | 'currentFilters' | 'all') | null;
|
||||
fields?: string[] | null;
|
||||
collectionSlug: string;
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "imports".
|
||||
*/
|
||||
export interface Import {
|
||||
id: number;
|
||||
collectionSlug: string;
|
||||
importMode?: ('create' | 'update' | 'upsert') | null;
|
||||
matchField?: string | null;
|
||||
status?: ('pending' | 'completed' | 'partial' | 'failed') | null;
|
||||
summary?: {
|
||||
imported?: number | null;
|
||||
updated?: number | null;
|
||||
total?: number | null;
|
||||
issues?: number | null;
|
||||
issueDetails?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv".
|
||||
|
|
@ -961,7 +1041,7 @@ export interface PayloadJob {
|
|||
| {
|
||||
executedAt: string;
|
||||
completedAt: string;
|
||||
taskSlug: 'inline' | 'schedulePublish';
|
||||
taskSlug: 'inline' | 'createCollectionExport' | 'createCollectionImport' | 'schedulePublish';
|
||||
taskID: string;
|
||||
input?:
|
||||
| {
|
||||
|
|
@ -994,7 +1074,7 @@ export interface PayloadJob {
|
|||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
taskSlug?: ('inline' | 'schedulePublish') | null;
|
||||
taskSlug?: ('inline' | 'createCollectionExport' | 'createCollectionImport' | 'schedulePublish') | null;
|
||||
queue?: string | null;
|
||||
waitUntil?: string | null;
|
||||
processing?: boolean | null;
|
||||
|
|
@ -1640,6 +1720,64 @@ export interface TagSelect<T extends boolean = true> {
|
|||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "exports_select".
|
||||
*/
|
||||
export interface ExportsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
format?: T;
|
||||
limit?: T;
|
||||
page?: T;
|
||||
sort?: T;
|
||||
sortOrder?: T;
|
||||
drafts?: T;
|
||||
selectionToUse?: T;
|
||||
fields?: T;
|
||||
collectionSlug?: T;
|
||||
where?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "imports_select".
|
||||
*/
|
||||
export interface ImportsSelect<T extends boolean = true> {
|
||||
collectionSlug?: T;
|
||||
importMode?: T;
|
||||
matchField?: T;
|
||||
status?: T;
|
||||
summary?:
|
||||
| T
|
||||
| {
|
||||
imported?: T;
|
||||
updated?: T;
|
||||
total?: T;
|
||||
issues?: T;
|
||||
issueDetails?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
|
|
@ -1881,6 +2019,81 @@ export interface CollectionsWidget {
|
|||
};
|
||||
width: 'full';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskCreateCollectionExport".
|
||||
*/
|
||||
export interface TaskCreateCollectionExport {
|
||||
input: {
|
||||
id: string;
|
||||
name: string;
|
||||
batchSize?: number | null;
|
||||
collectionSlug:
|
||||
| 'users'
|
||||
| 'campaignUser'
|
||||
| 'campaignInvite'
|
||||
| 'plaza'
|
||||
| 'leadership'
|
||||
| 'organization'
|
||||
| 'votePledge'
|
||||
| 'campaignDemand'
|
||||
| 'supporter'
|
||||
| 'supporterImportBatch'
|
||||
| 'plazaUpdate'
|
||||
| 'actionPlan'
|
||||
| 'electionTally'
|
||||
| 'electionCandidateVote'
|
||||
| 'electionCandidate'
|
||||
| 'media'
|
||||
| 'petition'
|
||||
| 'contact'
|
||||
| 'consent'
|
||||
| 'signature'
|
||||
| 'subscription'
|
||||
| 'post'
|
||||
| 'tag'
|
||||
| 'exports'
|
||||
| 'imports';
|
||||
drafts?: ('yes' | 'no') | null;
|
||||
exportCollection: string;
|
||||
fields?: string[] | null;
|
||||
format: 'csv' | 'json';
|
||||
limit?: number | null;
|
||||
locale?: string | null;
|
||||
maxLimit?: number | null;
|
||||
page?: number | null;
|
||||
sort?: string | null;
|
||||
userCollection?: string | null;
|
||||
userID?: string | null;
|
||||
where?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
};
|
||||
output?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskCreateCollectionImport".
|
||||
*/
|
||||
export interface TaskCreateCollectionImport {
|
||||
input: {
|
||||
importId: string;
|
||||
importCollection: string;
|
||||
userID?: string | null;
|
||||
userCollection?: string | null;
|
||||
batchSize?: number | null;
|
||||
debug?: boolean | null;
|
||||
defaultVersionStatus?: ('draft' | 'published') | null;
|
||||
maxLimit?: number | null;
|
||||
};
|
||||
output?: unknown;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TaskSchedulePublish".
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { buildConfig, type Access, type CollectionConfig } from 'payload'
|
||||
import sharp from 'sharp'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { resendAdapter } from '@payloadcms/email-resend'
|
||||
import { importExportPlugin } from '@payloadcms/plugin-import-export'
|
||||
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
|
||||
import { pt } from 'payload/i18n/pt'
|
||||
import { ActionPlan } from './collections/ActionPlan'
|
||||
|
|
@ -35,6 +36,7 @@ import { HomePage } from './globals/HomePage'
|
|||
import { Metadata } from './globals/Metadata'
|
||||
import { PrivacyPolicy } from './globals/PrivacyPolicy'
|
||||
import { SiteSettings } from './globals/SiteSettings'
|
||||
import { isPayloadAdmin } from './utilities/campaignAccess'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
|
@ -43,6 +45,29 @@ const campaignEmailFromAddress =
|
|||
process.env.CAMPAIGN_EMAIL_FROM?.trim() || 'campanha@jorgesolla.com.br'
|
||||
const campaignEmailFromName = process.env.CAMPAIGN_EMAIL_FROM_NAME?.trim() || 'Campanha Jorge Solla'
|
||||
|
||||
const importExportAdminOnly: Access = ({ req }) => isPayloadAdmin(req.user)
|
||||
|
||||
const withImportExportAdminAccess = ({ collection }: { collection: CollectionConfig }) => ({
|
||||
...collection,
|
||||
access: {
|
||||
...collection.access,
|
||||
read: importExportAdminOnly,
|
||||
create: importExportAdminOnly,
|
||||
update: importExportAdminOnly,
|
||||
delete: importExportAdminOnly,
|
||||
},
|
||||
})
|
||||
|
||||
const adminCsvExportCollection = (slug: 'signature' | 'contact') => ({
|
||||
slug,
|
||||
import: false as const,
|
||||
export: {
|
||||
format: 'csv' as const,
|
||||
disableJobsQueue: true,
|
||||
disableSave: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default buildConfig({
|
||||
admin: {
|
||||
user: Users.slug,
|
||||
|
|
@ -108,6 +133,11 @@ export default buildConfig({
|
|||
},
|
||||
token: process.env.BLOB_READ_WRITE_TOKEN,
|
||||
}),
|
||||
importExportPlugin({
|
||||
collections: [adminCsvExportCollection('signature'), adminCsvExportCollection('contact')],
|
||||
overrideExportCollection: withImportExportAdminAccess,
|
||||
overrideImportCollection: withImportExportAdminAccess,
|
||||
}),
|
||||
],
|
||||
i18n: {
|
||||
fallbackLanguage: 'pt', // use 'pt' (not 'pt-BR') for Payload admin language code
|
||||
|
|
|
|||
46
src/utilities/signatureExport.ts
Normal file
46
src/utilities/signatureExport.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import type { Contact, Petition } from '@/payload-types'
|
||||
import { isPopulatedRelationship } from '@/utilities/relationship'
|
||||
import type { ToCSVFunction } from '@payloadcms/plugin-import-export/types'
|
||||
|
||||
type ExportRow = Record<string, unknown>
|
||||
|
||||
const emptyContactColumns = (row: ExportRow): void => {
|
||||
row.contact_name = ''
|
||||
row.contact_email = ''
|
||||
row.contact_phone = ''
|
||||
row.contact_state = ''
|
||||
row.contact_city = ''
|
||||
}
|
||||
|
||||
const isPopulatedPetition = (value: unknown): value is Pick<Petition, 'id' | 'title'> => {
|
||||
if (value === null || typeof value !== 'object') return false
|
||||
|
||||
const record = value as Record<string, unknown>
|
||||
return typeof record.id === 'string' && typeof record.title === 'string'
|
||||
}
|
||||
|
||||
export const signatureContactToCSV: ToCSVFunction = ({ value, row }) => {
|
||||
if (!isPopulatedRelationship<Contact>(value)) {
|
||||
emptyContactColumns(row)
|
||||
return undefined
|
||||
}
|
||||
|
||||
row.contact_name = value.name
|
||||
row.contact_email = value.email ?? ''
|
||||
row.contact_phone = value.phone ?? ''
|
||||
row.contact_state = value.state ?? ''
|
||||
row.contact_city = value.city ?? ''
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const signaturePetitionToCSV: ToCSVFunction = ({ value, row }) => {
|
||||
if (!isPopulatedPetition(value)) {
|
||||
row.petition_id = ''
|
||||
row.petition_title = ''
|
||||
return undefined
|
||||
}
|
||||
|
||||
row.petition_id = value.id
|
||||
row.petition_title = value.title
|
||||
return undefined
|
||||
}
|
||||
87
tests/unit/signatureExport.unit.spec.ts
Normal file
87
tests/unit/signatureExport.unit.spec.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// @vitest-environment node
|
||||
|
||||
import type { ToCSVFunction } from '@payloadcms/plugin-import-export/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { signatureContactToCSV, signaturePetitionToCSV } from '@/utilities/signatureExport'
|
||||
|
||||
const runToCSV = (toCSV: ToCSVFunction, value: unknown) => {
|
||||
const row: Record<string, unknown> = {}
|
||||
toCSV({
|
||||
columnName: 'field',
|
||||
data: row,
|
||||
doc: row,
|
||||
row,
|
||||
siblingDoc: {},
|
||||
value,
|
||||
} as unknown as Parameters<ToCSVFunction>[0])
|
||||
return row
|
||||
}
|
||||
|
||||
describe('signatureExport', () => {
|
||||
describe('signatureContactToCSV', () => {
|
||||
it('writes operational contact columns from a populated relationship', () => {
|
||||
expect(
|
||||
runToCSV(signatureContactToCSV, {
|
||||
id: 1,
|
||||
name: 'Maria Silva',
|
||||
email: 'maria@example.com',
|
||||
phone: '71999990000',
|
||||
state: 'BA',
|
||||
city: 'Salvador',
|
||||
}),
|
||||
).toEqual({
|
||||
contact_name: 'Maria Silva',
|
||||
contact_email: 'maria@example.com',
|
||||
contact_phone: '71999990000',
|
||||
contact_state: 'BA',
|
||||
contact_city: 'Salvador',
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps SMS-only rows with empty email', () => {
|
||||
const row = runToCSV(signatureContactToCSV, {
|
||||
id: 2,
|
||||
name: 'João Souza',
|
||||
email: null,
|
||||
phone: '71988887777',
|
||||
state: 'BA',
|
||||
city: 'Feira de Santana',
|
||||
})
|
||||
|
||||
expect(row.contact_email).toBe('')
|
||||
expect(row.contact_phone).toBe('71988887777')
|
||||
})
|
||||
|
||||
it('writes empty cells when contact is not populated', () => {
|
||||
expect(runToCSV(signatureContactToCSV, 42)).toEqual({
|
||||
contact_name: '',
|
||||
contact_email: '',
|
||||
contact_phone: '',
|
||||
contact_state: '',
|
||||
contact_city: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('signaturePetitionToCSV', () => {
|
||||
it('writes petition id and title from a populated relationship', () => {
|
||||
expect(
|
||||
runToCSV(signaturePetitionToCSV, {
|
||||
id: 'saude-baiana',
|
||||
title: 'Saúde para toda a Bahia',
|
||||
}),
|
||||
).toEqual({
|
||||
petition_id: 'saude-baiana',
|
||||
petition_title: 'Saúde para toda a Bahia',
|
||||
})
|
||||
})
|
||||
|
||||
it('writes empty cells when petition is not populated', () => {
|
||||
expect(runToCSV(signaturePetitionToCSV, 'saude-baiana')).toEqual({
|
||||
petition_id: '',
|
||||
petition_title: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue