Pular para o conteúdo

1. Definir o módulo

Neste tutorial você escreve um módulo próprio, avisos: avisos curtos do site, cada um com título, texto, tipo, validade e uma chave que liga ou desliga. São três páginas.

  1. Definir o módulo (esta página): o manifesto, a coleção, os campos e as permissões.
  2. Tela, rota e migração: uma tela própria, uma rota pública, uma migração e um gancho, e a instalação no projeto.
  3. Conformidade e instalação: o kit de conformidade e o que a 1.1.0 não faz.

Você precisa de um projeto blog gerado com slash create meu-blog --template blog. Os módulos de um projeto ficam em src/modules/<id>/; o seu vai em src/modules/avisos/module.ts. Todo exemplo do tutorial é um arquivo completo desse caminho: cada passo substitui o anterior e passa sozinho na checagem de tipos e no kit de conformidade.

Um módulo é o resultado de defineModule. Ela confere o manifesto e devolve o mesmo objeto; se algo está errado, lança um ManifestError que nomeia o módulo e o caminho do problema.

src/modules/avisos/module.ts
import { collection, defineModule, field } from "../../panel/contract/index";
/** Who holds each permission of the module, by role. */
const permissions = {
read: ["admin", "editor"],
write: ["admin", "editor"],
remove: ["admin", "editor"],
} as const;
/** Minimal module: one collection of notices with a title and a text. */
export const avisos = defineModule({
id: "avisos",
version: "1.0.0",
core: "^1.0.0",
title: "Avisos",
description: "Avisos curtos do site.",
permissions,
permissionLabels: {
read: "Ver avisos",
write: "Criar e editar avisos",
remove: "Excluir avisos",
},
menu: [{ group: "organizacao", label: "Avisos", collection: "avisos", order: 30 }],
collections: {
avisos: collection({
label: "Avisos",
labelSingular: "Aviso",
fields: {
titulo: field.text({ label: "Título", required: true }),
texto: field.longText({ label: "Texto" }),
},
list: { columns: ["titulo"], search: ["titulo"], sort: { field: "titulo", direction: "asc" } },
permissions: { read: "read", create: "write", update: "write", delete: "remove" },
}),
},
});
export { avisos as module };

O que cada chave faz:

  • id é o nome do módulo e o prefixo das suas permissões, telas e rotas. Usa minúsculas, números e hífen entre partes (meu-modulo). O núcleo reserva alguns nomes porque já usa /painel/<nome> para as telas dele: c, entrar, usuarios, midia, backup, api, core e outros. usuarios como id dá Manifesto do módulo «usuarios», em id: «usuarios» é reservado pelo núcleo.
  • version é MAJOR.MINOR.PATCH. 1.0 dá versão «1.0» inválida: use MAJOR.MINOR.PATCH.
  • core é a faixa de versões do núcleo do painel em que o módulo funciona (^, ~ ou exata). O projeto blog roda o núcleo 1.0.0, então ^1.0.0 serve. Uma faixa que não cabe derruba o servidor na partida, com exige o núcleo <faixa>, mas este núcleo é 1.0.0.
  • title e description são os textos do módulo; o título é obrigatório e não pode ficar em branco.
  • menu coloca uma entrada no menu lateral. group é um dos grupos do núcleo (conteudo, comunidade, organizacao, site, operacao; aqui, Organização) ou um grupo novo que o módulo declara em menuGroups. order posiciona a entrada dentro do grupo; entradas sem order vêm depois das que têm. Uma entrada abre uma tela do módulo (screen) ou a lista gerada de uma coleção (collection), nunca as duas.
  • collections declara os dados. avisos é a chave da coleção; o núcleo gera a lista, o formulário de criação, o de edição e a exclusão.

O arquivo termina com export { avisos as module }. O índice de módulos do projeto importa o nome module de cada pasta, e é por ele que o painel encontra o seu.

A coleção do passo 1 guarda título e texto. Agora o aviso ganha um tipo, uma validade e a chave de ligado.

src/modules/avisos/module.ts
import { collection, defineModule, field } from "../../panel/contract/index";
/** Who holds each permission of the module, by role. */
const permissions = {
read: ["admin", "editor"],
write: ["admin", "editor"],
remove: ["admin", "editor"],
} as const;
/** The kinds of notice, in the order the select offers them (the first one is the default). */
const TIPOS = [
{ value: "info", label: "Informação" },
{ value: "atencao", label: "Atenção" },
{ value: "urgente", label: "Urgente" },
];
/** Module with the fields of a notice: its text, its kind, until when it is valid and whether it is on. */
export const avisos = defineModule({
id: "avisos",
version: "1.0.0",
core: "^1.0.0",
title: "Avisos",
description: "Avisos curtos do site.",
permissions,
permissionLabels: {
read: "Ver avisos",
write: "Criar e editar avisos",
remove: "Excluir avisos",
},
menu: [{ group: "organizacao", label: "Avisos", collection: "avisos", order: 30 }],
collections: {
avisos: collection({
label: "Avisos",
labelSingular: "Aviso",
fields: {
titulo: field.text({ label: "Título", required: true }),
texto: field.longText({ label: "Texto" }),
tipo: field.select({ label: "Tipo", options: TIPOS, default: "info", required: true }),
validade: field.date({ label: "Vale até" }),
ativo: field.boolean({ label: "Ativo", default: true }),
},
list: {
columns: ["titulo", "tipo", "validade", "ativo"],
filters: ["tipo", "ativo"],
search: ["titulo"],
sort: { field: "createdAt", direction: "desc" },
},
permissions: { read: "read", create: "write", update: "write", delete: "remove" },
}),
},
});
export { avisos as module };

Cada campo nasce de field.<tipo>(...). Os tipos são text, longText, richText, slug, email, url, number, boolean, date, datetime, select, multiSelect, relation, media e json. As opções comuns:

  • label é o rótulo no formulário e na lista; sem ele, o formulário usa a chave do campo.
  • required: true recusa o campo em branco com Campo obrigatório.
  • default é o que o controle mostra quando não há valor guardado. No formulário de criação deste módulo, Tipo vem com Informação marcado e Ativo vem ligado.
  • select pede options: texto puro (valor e rótulo iguais) ou { value, label }. O valor é o que fica guardado; o rótulo é o que a pessoa lê.

Em list, columns escolhe as colunas da lista, filters os campos que viram filtro, search onde a busca procura e sort a ordem inicial. Todo nome citado precisa ser um campo da coleção (ou um campo de sistema: id, version, createdAt, updatedAt); um nome que não existe é recusado quando o módulo é definido.

Falta dizer quem pode o quê.

src/modules/avisos/module.ts
import { collection, defineModule, field } from "../../panel/contract/index";
/** Who holds each permission of the module, by role: authors only read, and only admins delete. */
const permissions = {
read: ["admin", "editor", "autor"],
write: ["admin", "editor"],
remove: ["admin"],
} as const;
/** The kinds of notice, in the order the select offers them (the first one is the default). */
const TIPOS = [
{ value: "info", label: "Informação" },
{ value: "atencao", label: "Atenção" },
{ value: "urgente", label: "Urgente" },
];
/** Module whose permissions follow the roles: each action of the collection answers to one of the three keys. */
export const avisos = defineModule({
id: "avisos",
version: "1.0.0",
core: "^1.0.0",
title: "Avisos",
description: "Avisos curtos do site.",
permissions,
permissionLabels: {
read: "Ver avisos",
write: "Criar e editar avisos",
remove: "Excluir avisos",
},
menu: [{ group: "organizacao", label: "Avisos", collection: "avisos", order: 30 }],
collections: {
avisos: collection({
label: "Avisos",
labelSingular: "Aviso",
fields: {
titulo: field.text({ label: "Título", required: true }),
texto: field.longText({ label: "Texto" }),
tipo: field.select({ label: "Tipo", options: TIPOS, default: "info", required: true }),
validade: field.date({ label: "Vale até" }),
ativo: field.boolean({ label: "Ativo", default: true }),
},
list: {
columns: ["titulo", "tipo", "validade", "ativo"],
filters: ["tipo", "ativo"],
search: ["titulo"],
sort: { field: "createdAt", direction: "desc" },
},
permissions: { read: "read", create: "write", update: "write", delete: "remove" },
}),
},
});
export { avisos as module };

permissions lista as ações do módulo e os papéis que as têm por padrão. Os papéis são admin, editor e autor. O nome completo de uma permissão é <módulo>.<ação>: aqui, avisos.read, avisos.write e avisos.remove. É esse nome que aparece em grants e na matriz de permissões da tela Usuários; veja Usuários e papéis para o lado de quem opera o painel.

permissionLabels dá a frase em português de cada permissão para essa matriz; sem ela, a matriz mostra a chave.

Na coleção, permissions liga cada ação (read, create, update, delete) a uma das permissões do módulo. Sem esse bloco, só administradores alcançam a coleção. Citar uma permissão que o módulo não declara é um erro de manifesto: collections.avisos.permissions.read: permissão «ver» não declarada em permissions.

Neste passo, autor só lê, editor lê, cria e edita, e só admin exclui. Cada pessoa ainda pode receber permissões a mais ou a menos na tela Usuários.

Um módulo com uma coleção, cinco campos, três permissões e um item de menu. Ele ainda não está no projeto: a próxima página acrescenta a tela, a rota, a migração e o gancho, e instala o módulo.