@softize/opus 25.1.0 → 25.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/assistant-schema.d.mts +55 -0
- package/bin/lib/assistant-schema.mjs +286 -0
- package/bin/lib/check.mjs +173 -0
- package/bin/lib/copy.mjs +2 -0
- package/bin/lib/gen-manifest.mjs +4 -0
- package/bin/lib/gen-runner.mjs +31 -2
- package/bin/lib/gen-stubs.mjs +3 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
- package/docs/adr/0054-action-list-grouping-is-a-declared-server-cut.md +82 -0
- package/docs/protocol.md +59 -1
- package/package.json +5 -1
- package/registry/skills/create-opus-action/SKILL.md +8 -3
- package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
- package/registry/skills/create-opus-action/references/evaluations.md +3 -0
- package/registry/skills/test-opus-action/SKILL.md +4 -1
- package/registry/skills/test-opus-action/references/evaluations.md +2 -0
- package/registry/skills/test-opus-action/references/harness.md +14 -0
- package/src/ai/index.ts +3 -0
- package/src/ai/resource-context.ts +113 -0
- package/src/core/actions.ts +5 -2
- package/src/core/assistant.ts +545 -0
- package/src/core/contracts.ts +2 -0
- package/src/core/index.ts +23 -0
- package/src/core/list-grouping.ts +45 -0
- package/src/core/runtime.ts +138 -0
- package/src/core/types.ts +39 -0
- package/src/mcp/index.ts +3 -0
- package/src/ui/components/patterns/grouped-list.ts +88 -0
- package/src/ui/components/patterns/list.tsx +395 -61
- package/src/ui/docs/content/action-list.md +42 -9
- package/src/ui/docs/content/actions.md +97 -1
- package/src/ui/docs/content/ai.md +4 -2
- package/src/ui/docs/content/cli.md +2 -1
- package/src/ui/docs/content/mcp.md +3 -1
- package/src/ui/drivers/context.tsx +50 -0
- package/src/ui/drivers/react.tsx +9 -69
- package/src/ui/meta.ts +1 -1
|
@@ -215,6 +215,37 @@ periods: [
|
|
|
215
215
|
// presetRange('last7') → { from: 'YYYY-MM-DD', to: 'YYYY-MM-DD' } — o mesmo cálculo do pattern.
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
## Agrupamento
|
|
219
|
+
|
|
220
|
+
Declare `grouping.fields` quando uma dimensão fechada puder organizar a tabela sem mudar o recorte
|
|
221
|
+
de negócio. Cada campo precisa ser um filtro `select` com opções `static` ou `dictionary`; texto,
|
|
222
|
+
número, data e lookup não são dimensões de agrupamento. O filtro pode aceitar uma ou várias opções:
|
|
223
|
+
cada consulta de grupo envia um único valor na forma declarada. A lista continua plana por padrão,
|
|
224
|
+
e a pessoa escolhe **Agrupar por** em **Exibição**.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
filters: {
|
|
228
|
+
status: {
|
|
229
|
+
label: 'Status',
|
|
230
|
+
type: 'select',
|
|
231
|
+
options: { kind: 'dictionary', ref: 'workspace.status' },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
grouping: { fields: ['status'] },
|
|
235
|
+
paginate: 'offset',
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
O Opus consulta a mesma `ListAction` uma vez para cada opção, aplicando o valor ao filtro e
|
|
239
|
+
preservando busca, período, demais filtros e ordenação. Por isso, o handler precisa devolver
|
|
240
|
+
`total` e a action precisa usar `paginate: 'offset'`: o cabeçalho recolhível apresenta a contagem
|
|
241
|
+
autoritativa, e **Carregar mais** expande apenas aquele grupo. A soma aparece no rodapé, sem paginador
|
|
242
|
+
global. O limite é de doze opções por dimensão. Se um dicionário não estiver disponível ou exceder
|
|
243
|
+
esse limite, a dimensão não é oferecida em **Agrupar por**.
|
|
244
|
+
|
|
245
|
+
O agrupamento pertence à tabela derivada. Views alternativas e o modo `children` continuam planos.
|
|
246
|
+
`groupBy` integra `ActionListState`; `listStateToParams` o serializa como `group=<campo>`, e filtros
|
|
247
|
+
aplicados à mesma dimensão restringem os grupos exibidos.
|
|
248
|
+
|
|
218
249
|
## Paginação e carregamento
|
|
219
250
|
|
|
220
251
|
`ActionList` envia `limit`, definido por `pageSize`, e `page` no input. O handler aplica o recorte e
|
|
@@ -481,21 +512,23 @@ render(
|
|
|
481
512
|
|
|
482
513
|
## Declaração na ListAction
|
|
483
514
|
|
|
484
|
-
| Chave
|
|
485
|
-
|
|
|
486
|
-
| `columns`
|
|
487
|
-
| `filters`
|
|
488
|
-
| `text`
|
|
489
|
-
| `sort`
|
|
490
|
-
| `periods`
|
|
515
|
+
| Chave | O que declara |
|
|
516
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
517
|
+
| `columns` | `{ key, label, type?, sortable?, fit?, hidden?, dateFormat?, dictionary?, empty? }` — a tabela. `hidden` nasce oculta e pode ser exibida pelo seletor de colunas; `dictionary` nomeia o dicionário do provider; `empty` dá o significado da ausência. |
|
|
518
|
+
| `filters` | `{ [nome]: { label, type, options?, multiple?, presentation?, searchable?, allowEmpty?, placement?, section?, depends?, … } }` — a toolbar. `placement` classifica o controle como `inline`, `advanced` ou `external`; filtros avançados com `section` são agrupados no diálogo; `external` mantém estado e input, mas delega a apresentação ao consumidor. `presentation: 'toggle'` mostra todas as opções curtas de um `select` de escolha única em um `ToggleGroup`; sem a propriedade, o controle continua sendo `Select`. `searchable` habilita busca local em `select`; `allowEmpty: false` omite a opção vazia. `depends` desabilita/cascateia; options `kind: 'lookup'` busca em uma action. Valor aplicado entra no input com o mesmo nome. |
|
|
519
|
+
| `text` | `{ fields }` — liga a busca; convenção: param `q` no input. Ela aparece depois dos filtros inline e imediatamente antes do acesso aos filtros avançados. A busca participa da contagem de controles inline: com mais de um controle, a barra exibe as labels; quando ela é o único recorte, sua label é omitida. |
|
|
520
|
+
| `sort` | `{ fields, default }` — ordenação inicial; header ordenável escreve `sort: 'chave:dir'`. |
|
|
521
|
+
| `periods` | `{ value, label, default? }[]` — o controle de período (presets + Personalizado com calendário); `default: true` marca o preset inicial, senão vale o primeiro; materializa em `from`/`to` no input. |
|
|
522
|
+
| `grouping` | `{ fields }` — dimensões fechadas disponíveis em **Agrupar por**. Cada nome referencia um filtro `select` com opções estáticas ou de dicionário. Não define agrupamento inicial. |
|
|
491
523
|
|
|
492
524
|
## useListAction
|
|
493
525
|
|
|
494
|
-
Quando o layout não cabe no `ActionList`, `useListAction(action, input)` entrega o mesmo fetch
|
|
526
|
+
Quando o layout não cabe no `ActionList`, `useListAction(action, input, options?)` entrega o mesmo fetch
|
|
495
527
|
declarativo (`items`, `total`, `error`, `isLoading`, `isFetching`, `refetch`) sob o
|
|
496
528
|
`QueryClientProvider`, e `presetRange(preset)` materializa os presets de período (`today`, `last7`,
|
|
497
529
|
`thisMonth`…) em `{ from, to }` no formato `YYYY-MM-DD` — o mesmo cálculo que o pattern faz antes de
|
|
498
|
-
chamar a action.
|
|
530
|
+
chamar a action. `options.enabled` suspende a consulta enquanto for `false`; sem a opção, vale
|
|
531
|
+
`true`.
|
|
499
532
|
|
|
500
533
|
## Propriedades de ActionList
|
|
501
534
|
|
|
@@ -136,7 +136,7 @@ name, kind
|
|
|
136
136
|
→ public, requires, authorize, loads
|
|
137
137
|
→ handler
|
|
138
138
|
→ audit, confirm, invalidates, rateLimit, idempotency
|
|
139
|
-
→ ai, automatable, internal
|
|
139
|
+
→ ai, assistant, automatable, internal
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
Campos específicos de um kind, como `fields`, `paginate`, `columns`, `filters`, `expand`,
|
|
@@ -179,6 +179,102 @@ A autorização é fechada por padrão: sem `public: true`, a action exige usuá
|
|
|
179
179
|
`authorize`, exige também a permissão declarada. `automatable: true` permite que um
|
|
180
180
|
[schedule](/docs/scheduler) execute a action como o sistema, sem abri-la a chamadores anônimos.
|
|
181
181
|
|
|
182
|
+
## Declarar o assistente do recurso
|
|
183
|
+
|
|
184
|
+
Um produto pode oferecer, além do chat geral, uma conversa sobre um recurso específico: um cliente,
|
|
185
|
+
um relatório, um pedido. Essa conversa precisa ler o recurso com a permissão de quem conversa, seguir
|
|
186
|
+
o procedimento próprio daquele tipo de recurso e entregar ao modelo só os dados necessários. Os três
|
|
187
|
+
ficam declarados num lugar só: a action `view` que lê o recurso declara `assistant` e passa a ser a
|
|
188
|
+
**action dona** daquele tipo de recurso.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
export const customerViewContract = defineContract({
|
|
192
|
+
name: 'customer.view',
|
|
193
|
+
kind: 'view',
|
|
194
|
+
label: 'Ver cliente',
|
|
195
|
+
input: z.object({ id: z.string().uuid() }),
|
|
196
|
+
output: customerViewSchema,
|
|
197
|
+
personalData: true,
|
|
198
|
+
requires: 'customer.read',
|
|
199
|
+
authorize: (ctx) => ctx.can('customer.read'),
|
|
200
|
+
ai: true,
|
|
201
|
+
assistant: {
|
|
202
|
+
resource: 'customer',
|
|
203
|
+
skill: 'review-customer',
|
|
204
|
+
label: 'Assistente do cliente',
|
|
205
|
+
title: 'name',
|
|
206
|
+
context: ['kind', 'stage', 'sources', 'leads.totalCount', 'leads.latest.status'],
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
| Campo | O que declara |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| `resource` | O tipo de recurso de que a action é dona, em camelCase. Cada tipo tem uma única action dona. |
|
|
214
|
+
| `skill` | A chave do procedimento que a IA segue ao tratar do recurso, em minúsculas com hífen. O Opus não conhece o procedimento; o produto o mantém e confere que a chave existe. |
|
|
215
|
+
| `label` | O nome do assistente para pessoas. Entra no inventário do `opus copy`. |
|
|
216
|
+
| `title` | O caminho, na saída, do campo que nomeia o recurso. |
|
|
217
|
+
| `context` | Os caminhos, na saída, dos campos que o host pode entregar ao modelo como dado do recurso. |
|
|
218
|
+
| `maxContextChars` | Opcional. O teto do dado entregue, em caracteres de JSON: 4.000 por padrão, de 1.000 a 16.000. |
|
|
219
|
+
|
|
220
|
+
A action dona precisa ser `view`, estar publicada para a IA (`ai`), declarar `personalData` e
|
|
221
|
+
receber só `{ id }`: o `id`, do tipo texto, é o único campo obrigatório da entrada, e outros campos,
|
|
222
|
+
se houver, têm valor padrão ou são opcionais. Os caminhos usam ponto para descer em objetos; um
|
|
223
|
+
caminho que passa por uma lista vale para cada item, e um campo intermediário `null` aparece como
|
|
224
|
+
`null`. Declare em `context` só o que orienta a conversa: o exemplo deixa fora contato, documento e
|
|
225
|
+
endereço, e o nome já vai como `title`. Um caminho que termina num objeto ou numa lista leva tudo o
|
|
226
|
+
que está abaixo dele, inclusive campos acrescentados depois à saída; para conferir num teste o que
|
|
227
|
+
cada caminho alcança, use `assistantContextLeaves`, de `@softize/opus/assistant-schema`, sobre o
|
|
228
|
+
schema de saída do manifest. O `title` precisa apontar para texto ou número.
|
|
229
|
+
|
|
230
|
+
O `opus check` cobra a forma da declaração (regra `assistant-declaration`), o `opus gen` recusa
|
|
231
|
+
`title`, caminho de `context` ou entrada que não conferem com os schemas, e o registro recusa duas
|
|
232
|
+
actions donas do mesmo recurso. O manifest, `runtime.aiTools()` e o MCP (`com.softize.opus/assistant`
|
|
233
|
+
em `_meta`) levam a declaração, e o manifest passa a mostrar também a publicação para a IA.
|
|
234
|
+
|
|
235
|
+
### Ler o recurso pela action dona
|
|
236
|
+
|
|
237
|
+
O recurso de um assistente é lido, e o acesso a ele é conferido, somente pela action dona.
|
|
238
|
+
`runtime.readResource` executa a action com `{ id }` pelo mesmo `execute` de qualquer chamada; a
|
|
239
|
+
autorização, a auditoria e a marca de dado pessoal são as da action:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
const read = await runtime.readResource('customer', id, base, { context: true })
|
|
243
|
+
if (!read.ok) return notFound() // o erro da action, com a categoria dela
|
|
244
|
+
read.resource // { kind: 'customer', id, title: 'Maria Souza', label: 'Assistente do cliente', skill: 'review-customer' }
|
|
245
|
+
read.context // { data, truncated, omitted, personalData: true }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Sem `{ context: true }`, a leitura devolve só o descritor, que basta para conferir o acesso e
|
|
249
|
+
mostrar o nome. Um tipo sem action dona devolve `runtime.assistant_resource_unknown` sem executar
|
|
250
|
+
nada. `runtime.assistants()` lista as declarações com a action dona de cada uma, para o produto
|
|
251
|
+
montar rotas e telas sem lista paralela e para conferir, num teste, que cada `skill` existe no
|
|
252
|
+
registro de procedimentos dele. O Opus não enxerga as rotas HTTP que o produto registra por conta
|
|
253
|
+
própria: um teste do produto deve garantir que nenhuma rota confere acesso a recurso sem passar por
|
|
254
|
+
uma action.
|
|
255
|
+
|
|
256
|
+
### O dado que vai ao modelo
|
|
257
|
+
|
|
258
|
+
`context.data` traz só os caminhos declarados, com a estrutura da saída. Quando o JSON passa do
|
|
259
|
+
teto, o corte acontece por etapas, até caber, e fica registrado em `omitted`:
|
|
260
|
+
|
|
261
|
+
1. sai o último item da maior lista, repetidamente;
|
|
262
|
+
2. com as listas vazias, o texto mais longo é encurtado pela metade, até 200 caracteres;
|
|
263
|
+
3. por fim, saem campos inteiros, do último declarado para o primeiro.
|
|
264
|
+
|
|
265
|
+
Com qualquer corte, `truncated` vale `true`. O título fica fora do teto e é encurtado a 200
|
|
266
|
+
caracteres. Um caminho que falta na saída em execução é omitido com o motivo `missing` e gera um
|
|
267
|
+
aviso no log, sem falhar a leitura. Quem recebe a saída por outro canal, como o MCP, aplica a mesma
|
|
268
|
+
projeção com `projectAssistantContext(assistant, output)`.
|
|
269
|
+
|
|
270
|
+
Entregue o dado ao modelo como bloco delimitado, numa mensagem de dado, nunca no prompt de sistema.
|
|
271
|
+
`resourceDataBlock`, de `@softize/opus/ai`, monta o bloco com o tipo, o id, o título, os dados e,
|
|
272
|
+
quando houve corte ou ausência, `truncated` e `omitted`, e escapa dentro dos valores o que poderia
|
|
273
|
+
fechar o delimitador. Quando o modelo chama a action dona como ferramenta, recebe a saída completa
|
|
274
|
+
do contrato, como em qualquer chamada: a projeção governa só o que o host entrega por iniciativa
|
|
275
|
+
própria. Cada execução de uma action com dado pessoal é uma exposição registrada, então reaproveite
|
|
276
|
+
a leitura que já conferiu o acesso em vez de executar a action de novo só para obter o contexto.
|
|
277
|
+
|
|
182
278
|
## Prototipar com dados simulados
|
|
183
279
|
|
|
184
280
|
Para desenhar a interface antes de o backend existir, rode o app em modo design. Nesse modo, toda
|
|
@@ -86,8 +86,10 @@ const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
|
|
|
86
86
|
usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
|
|
87
87
|
`description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
|
|
88
88
|
`metadata.dataProducts` lista os [Produtos de Dados](/docs/data-products) relacionados e
|
|
89
|
-
`metadata.dataProductLabels` associa cada identificador ao seu nome legível.
|
|
90
|
-
|
|
89
|
+
`metadata.dataProductLabels` associa cada identificador ao seu nome legível. Na action dona de um
|
|
90
|
+
tipo de recurso, `metadata.assistant` traz o [assistente declarado](/docs/actions): o recurso, a
|
|
91
|
+
chave do procedimento, o nome, o campo do título, os campos do dado ao modelo e o teto. Esses
|
|
92
|
+
metadados servem ao host: não entram no prompt e não concedem acesso.
|
|
91
93
|
|
|
92
94
|
O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
|
|
93
95
|
para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
|
|
@@ -21,7 +21,8 @@ opus seed check # bindings, dependências, ciclos e scripts paralelos de s
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
`opus check` lê o código-fonte sem executá-lo e aplica regras em quatro grupos: actions
|
|
24
|
-
(`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind`
|
|
24
|
+
(`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind` e
|
|
25
|
+
`assistant-declaration`, que cobra a forma do [assistente do recurso](/docs/actions)), UI
|
|
25
26
|
(`ui-structure`, `ui-structure-split`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`),
|
|
26
27
|
[Produtos de Dados](/docs/data-products) (`data-product-export`, `-id`, `-duplicate`, `-version`,
|
|
27
28
|
`-interfaces`, `-interface`, `-entities` e `-entity`, todas com o prefixo `data-product`) e
|
|
@@ -60,7 +60,9 @@ Quando a action declara o que faz com os dados, a tool traz também as anotaçõ
|
|
|
60
60
|
`readOnlyHint` vem de `effect`. Numa action que escreve, `destructiveHint` vai sempre explícito, a
|
|
61
61
|
partir de `ai.destructive`, porque o MCP trata a ausência numa escrita como destrutiva.
|
|
62
62
|
Em `_meta`, `com.softize.opus/effect` repete o efeito e `com.softize.opus/personal-data` diz se a
|
|
63
|
-
resposta contém dado pessoal.
|
|
63
|
+
resposta contém dado pessoal. Na action dona de um tipo de recurso, `com.softize.opus/assistant`
|
|
64
|
+
traz o assistente declarado, para o host conhecê-lo pelo catálogo que já consulta e aplicar à saída
|
|
65
|
+
a mesma projeção com `projectAssistantContext`. Veja [Actions](/docs/actions).
|
|
64
66
|
|
|
65
67
|
`CallTool` recusa com erro qualquer nome fora dessa lista; os demais seguem por `runtime.execute`.
|
|
66
68
|
A exposição declarada em `ai` decide o que o cliente MCP pode executar, e a autorização do contexto
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from 'react'
|
|
2
|
+
import type { ClientAdapter } from '../../core/index.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dicionário estrutural pro resolvedor de opções: qualquer coisa com `options()`
|
|
6
|
+
* satisfaz o contrato sem este driver importar o driver de schema.
|
|
7
|
+
*/
|
|
8
|
+
export interface DictLike {
|
|
9
|
+
options(): Array<{ value: string; label: string; icon?: string }>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface OpusContextValue {
|
|
13
|
+
client: ClientAdapter
|
|
14
|
+
dicts: Record<string, DictLike>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const OpusContext = createContext<OpusContextValue | null>(null)
|
|
18
|
+
|
|
19
|
+
export interface OpusProviderProps {
|
|
20
|
+
client: ClientAdapter
|
|
21
|
+
/** Dicionários por ref usados por filtros e campos declarativos. */
|
|
22
|
+
dicts?: Record<string, DictLike>
|
|
23
|
+
children: ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function OpusProvider({ client, dicts, children }: OpusProviderProps): ReactNode {
|
|
27
|
+
const value = useMemo<OpusContextValue>(() => ({ client, dicts: dicts ?? {} }), [client, dicts])
|
|
28
|
+
return <OpusContext.Provider value={value}>{children}</OpusContext.Provider>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Acesso interno ao adapter; não é reexportado por `@softize/opus/ui/react`. */
|
|
32
|
+
export function useClient(hook: string): ClientAdapter {
|
|
33
|
+
const ctx = useContext(OpusContext)
|
|
34
|
+
if (ctx === null) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`${hook} foi chamado fora de um <OpusProvider> e não tem como executar a action. ` +
|
|
37
|
+
'Os hooks de action e os componentes que os usam, como ActionForm, ActionList e ActionView, ' +
|
|
38
|
+
'precisam de um <OpusProvider client={...}> acima deles na árvore. Envolva a aplicação no provider; ' +
|
|
39
|
+
'em testes e previews, envolva o componente renderizado.',
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
return ctx.client
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const NO_DICTS: Record<string, DictLike> = {}
|
|
46
|
+
|
|
47
|
+
/** Dicionários do provider; fora dele, o resolvedor usa os fallbacks do contrato. */
|
|
48
|
+
export function useDicts(): Record<string, DictLike> {
|
|
49
|
+
return useContext(OpusContext)?.dicts ?? NO_DICTS
|
|
50
|
+
}
|
package/src/ui/drivers/react.tsx
CHANGED
|
@@ -12,12 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
createContext,
|
|
16
15
|
useCallback,
|
|
17
|
-
useContext,
|
|
18
16
|
useMemo,
|
|
19
17
|
useState,
|
|
20
|
-
type ReactNode,
|
|
21
18
|
} from 'react'
|
|
22
19
|
import {
|
|
23
20
|
useForm,
|
|
@@ -32,7 +29,6 @@ import type {
|
|
|
32
29
|
ActionContract,
|
|
33
30
|
ActionDef,
|
|
34
31
|
ActionError,
|
|
35
|
-
ClientAdapter,
|
|
36
32
|
FormContract,
|
|
37
33
|
Paginated,
|
|
38
34
|
} from '../../core/index.ts'
|
|
@@ -42,74 +38,16 @@ import {
|
|
|
42
38
|
idleAction,
|
|
43
39
|
idleLookup,
|
|
44
40
|
} from '../index.ts'
|
|
41
|
+
import { useClient } from './context.tsx'
|
|
42
|
+
export {
|
|
43
|
+
OpusProvider,
|
|
44
|
+
useDicts,
|
|
45
|
+
type DictLike,
|
|
46
|
+
type OpusProviderProps,
|
|
47
|
+
} from './context.tsx'
|
|
45
48
|
import { zodErrorMapPtBr } from '../lib/zod-pt-br.ts'
|
|
46
49
|
import { objectSchemaShape } from '../lib/object-schema.ts'
|
|
47
50
|
|
|
48
|
-
// =============================================================================
|
|
49
|
-
// Provider
|
|
50
|
-
// =============================================================================
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Dicionário estrutural pro resolvedor de `options: { kind: 'dictionary', ref }`:
|
|
54
|
-
* qualquer coisa com `options()` → `{ value, label, icon? }`. O `DictType` do `t.dict`
|
|
55
|
-
* satisfaz direto — sem este driver importar o driver de schema. A metadata opcional
|
|
56
|
-
* continua sendo declarada pelo dicionário; o provider não inventa apresentação.
|
|
57
|
-
*/
|
|
58
|
-
export interface DictLike {
|
|
59
|
-
options(): Array<{ value: string; label: string; icon?: string }>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface OpusContextValue {
|
|
63
|
-
client: ClientAdapter
|
|
64
|
-
dicts: Record<string, DictLike>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const OpusContext = createContext<OpusContextValue | null>(null)
|
|
68
|
-
|
|
69
|
-
export interface OpusProviderProps {
|
|
70
|
-
client: ClientAdapter
|
|
71
|
-
/** Dicionários por ref — resolvem `options: { kind: 'dictionary', ref }` de
|
|
72
|
-
* filters/fields (ActionList/ActionForm). Campo de dict sem registro aqui ainda
|
|
73
|
-
* resolve pelo fallback (a meta do `t.dict` viaja no schema do contrato). */
|
|
74
|
-
dicts?: Record<string, DictLike>
|
|
75
|
-
children: ReactNode
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function OpusProvider({
|
|
79
|
-
client,
|
|
80
|
-
dicts,
|
|
81
|
-
children,
|
|
82
|
-
}: OpusProviderProps): ReactNode {
|
|
83
|
-
const value = useMemo<OpusContextValue>(() => ({ client, dicts: dicts ?? {} }), [client, dicts])
|
|
84
|
-
return <OpusContext.Provider value={value}>{children}</OpusContext.Provider>
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* O adapter do provider. `hook` nomeia quem chamou, para a mensagem apontar o hook que a
|
|
89
|
-
* pessoa vê no stack — muitas vezes chamado por dentro de ActionForm, ActionList ou
|
|
90
|
-
* ActionView, e não diretamente pela tela.
|
|
91
|
-
*/
|
|
92
|
-
function useClient(hook: string): ClientAdapter {
|
|
93
|
-
const ctx = useContext(OpusContext)
|
|
94
|
-
if (ctx === null) {
|
|
95
|
-
throw new Error(
|
|
96
|
-
`${hook} foi chamado fora de um <OpusProvider> e não tem como executar a action. ` +
|
|
97
|
-
'Os hooks de action e os componentes que os usam, como ActionForm, ActionList e ActionView, ' +
|
|
98
|
-
'precisam de um <OpusProvider client={...}> acima deles na árvore. Envolva a aplicação no provider; ' +
|
|
99
|
-
'em testes e previews, envolva o componente renderizado.',
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
return ctx.client
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const NO_DICTS: Record<string, DictLike> = {}
|
|
106
|
-
|
|
107
|
-
/** Dicts do provider. Default {} — inclusive fora do provider: o resolvedor cai
|
|
108
|
-
* nos fallbacks (estáticas do spec, meta do `t.dict`) sem exigir registry. */
|
|
109
|
-
export function useDicts(): Record<string, DictLike> {
|
|
110
|
-
return useContext(OpusContext)?.dicts ?? NO_DICTS
|
|
111
|
-
}
|
|
112
|
-
|
|
113
51
|
// =============================================================================
|
|
114
52
|
// useAction
|
|
115
53
|
// =============================================================================
|
|
@@ -265,6 +203,7 @@ export interface UseListActionResult<TItem> {
|
|
|
265
203
|
export function useListAction<TItem = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(
|
|
266
204
|
action: { name: string; kind: 'list' },
|
|
267
205
|
input?: TInput,
|
|
206
|
+
options?: { enabled?: boolean },
|
|
268
207
|
): UseListActionResult<TItem> {
|
|
269
208
|
const client = useClient('useListAction')
|
|
270
209
|
|
|
@@ -278,6 +217,7 @@ export function useListAction<TItem = unknown, TInput extends Record<string, unk
|
|
|
278
217
|
// Input mudou (filtro/página) → mantém os itens na tela enquanto busca: o
|
|
279
218
|
// ActionList esmaece em vez de piscar skeleton. isPending = só o 1º load.
|
|
280
219
|
placeholderData: keepPreviousData,
|
|
220
|
+
enabled: options?.enabled ?? true,
|
|
281
221
|
})
|
|
282
222
|
|
|
283
223
|
const refetch = useCallback(async () => {
|
package/src/ui/meta.ts
CHANGED
|
@@ -388,7 +388,7 @@ export const componentMeta = {
|
|
|
388
388
|
name: "action-list",
|
|
389
389
|
ancestry: "opus",
|
|
390
390
|
whenToUse:
|
|
391
|
-
"Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período e
|
|
391
|
+
"Apresente uma coleção pesquisável a partir de uma ListAction. O contrato declara colunas, filtros, período, busca e dimensões fechadas de agrupamento; ActionList coordena visualizações, paginação, seleção em lote e sincronização com a URL. A tabela derivada não cria moldura própria. Em uma página, envolva a lista com Surface para manter título e criação junto da coleção. Para um único recurso, use ActionView.",
|
|
392
392
|
},
|
|
393
393
|
"action-trigger": {
|
|
394
394
|
name: "action-trigger",
|