rl-core-front 0.14.1 → 0.14.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rl-core-front",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "description": "Telas e componentes Next.js do core: login com 2FA, usuários, RBAC, auditoria, logs e listagens com filtro dinâmico",
5
5
  "author": "Rodrigo Liberti",
6
6
  "license": "MIT",
@@ -17,6 +17,15 @@ export interface ConfirmDialogProps {
17
17
  open: boolean;
18
18
  title: string;
19
19
  description?: string;
20
+ /**
21
+ * Segundo parágrafo, quando a consequência muda por tela.
22
+ *
23
+ * Separado da `description` porque a explicação do que a ação faz é a mesma
24
+ * em todo lugar, e o que acontece com o que depende do registro não é: numa
25
+ * tela a exclusão passa e o histórico se mantém, na outra ela é recusada
26
+ * enquanto houver algo em uso. Um texto único para as duas mentiria numa.
27
+ */
28
+ note?: string;
20
29
  confirmLabel: string;
21
30
  cancelLabel: string;
22
31
  /** Pinta o botão de confirmar como destrutivo (excluir, resetar). */
@@ -34,6 +43,7 @@ export function ConfirmDialog({
34
43
  open,
35
44
  title,
36
45
  description,
46
+ note,
37
47
  confirmLabel,
38
48
  cancelLabel,
39
49
  destructive = false,
@@ -48,9 +58,21 @@ export function ConfirmDialog({
48
58
  <DialogContent className="max-w-sm" expandable={false}>
49
59
  <DialogHeader>
50
60
  <DialogTitle>{title}</DialogTitle>
51
- {description && <DialogDescription>{description}</DialogDescription>}
52
61
  </DialogHeader>
53
62
  <DialogForm onSubmit={onConfirm}>
63
+ {/* A explicação fica no corpo, e não no `DialogHeader`.
64
+ Ali dentro ela virava subtítulo: o `DialogContent` arranca o
65
+ cabeçalho para dividir a primeira linha com os botões de ampliar
66
+ e fechar, então a descrição era espremida numa coluna estreita ao
67
+ lado do X — enquanto o corpo do modal ficava vazio. Numa
68
+ confirmação a explicação **é** o conteúdo: ela precisa do corpo,
69
+ com respiro, para ser lida antes do clique. */}
70
+ {(description || note) && (
71
+ <div className="space-y-2 text-sm text-muted-foreground">
72
+ {description && <DialogDescription>{description}</DialogDescription>}
73
+ {note && <p>{note}</p>}
74
+ </div>
75
+ )}
54
76
  <DialogFooter className="gap-2">
55
77
  <Button type="button" variant="outline" onClick={onCancel}>
56
78
  {cancelLabel}
@@ -5,16 +5,32 @@ import { useCallback, useState } from "react";
5
5
 
6
6
  import { ConfirmDialog } from "#core/components/ui";
7
7
  import { useI18n } from "#core/contexts";
8
+ import { RequestOperation, useRequest } from "#core/hooks/use-request";
8
9
 
9
10
  export interface ConfirmOptions {
10
11
  title: string;
11
12
  description?: string;
13
+ /** Segundo parágrafo — a consequência que muda por tela. */
14
+ note?: string;
12
15
  confirmLabel?: string;
13
16
  cancelLabel?: string;
14
17
  /** Ação sem volta (excluir, resetar 2FA) — pinta o botão de vermelho. */
15
18
  destructive?: boolean;
16
19
  }
17
20
 
21
+ export interface ConfirmedActionOptions extends ConfirmOptions {
22
+ /**
23
+ * O que recarregar depois que a ação deu certo.
24
+ *
25
+ * Fica aqui e não no `.then()` de quem chamou porque `acting` só volta a
26
+ * `false` quando isto termina: sem esperar a recarga, a tabela destrava
27
+ * mostrando ainda a linha que acabou de ser excluída.
28
+ */
29
+ reload?: () => Promise<unknown> | unknown;
30
+ /** Toast de sucesso; `RequestOperation.Delete` dá o texto de exclusão. */
31
+ success?: RequestOperation | string;
32
+ }
33
+
18
34
  interface Pending {
19
35
  options: ConfirmOptions;
20
36
  resolve: (confirmed: boolean) => void;
@@ -23,6 +39,18 @@ interface Pending {
23
39
  export interface UseConfirmResult {
24
40
  /** Aguarda a decisão do usuário: `true` confirmou, `false` cancelou. */
25
41
  confirm: (options: ConfirmOptions) => Promise<boolean>;
42
+ /**
43
+ * Pergunta, executa e recarrega — o ciclo inteiro numa chamada.
44
+ *
45
+ * `true` quando a ação rodou até o fim; `false` se o usuário desistiu ou se
46
+ * a requisição falhou (o toast de erro é do `useRequest`).
47
+ */
48
+ confirmAndRun: (
49
+ options: ConfirmedActionOptions,
50
+ action: () => Promise<unknown>,
51
+ ) => Promise<boolean>;
52
+ /** Enquanto a ação confirmada roda — para travar a tabela que ela muda. */
53
+ acting: boolean;
26
54
  /** Renderize no final da tela — é o modal em si. */
27
55
  confirmDialog: JSX.Element;
28
56
  }
@@ -34,9 +62,14 @@ export interface UseConfirmResult {
34
62
  *
35
63
  * A promise resolve quando o usuário decide — cancelar (ou fechar no `Esc`)
36
64
  * resolve `false`, então o fluxo de quem chamou não muda.
65
+ *
66
+ * Quando a resposta do "sim" é sempre a mesma — chamar o serviço, recarregar a
67
+ * listagem e travar a tela no meio —, use o `confirmAndRun`: é o passo que
68
+ * cada tela remontava à mão, e onde mora a armadilha do parágrafo seguinte.
37
69
  */
38
70
  export function useConfirm(): UseConfirmResult {
39
71
  const { t } = useI18n();
72
+ const { run, loading: acting } = useRequest();
40
73
  const [pending, setPending] = useState<Pending | null>(null);
41
74
 
42
75
  const confirm = useCallback(
@@ -53,11 +86,39 @@ export function useConfirm(): UseConfirmResult {
53
86
  [pending],
54
87
  );
55
88
 
89
+ const confirmAndRun = useCallback(
90
+ async (
91
+ options: ConfirmedActionOptions,
92
+ action: () => Promise<unknown>,
93
+ ): Promise<boolean> => {
94
+ if (!(await confirm(options))) {
95
+ return false;
96
+ }
97
+
98
+ const done = await run(action, { success: options.success });
99
+
100
+ // A falha do `run` é `null`, e só ela. `undefined` é o que uma rota 204
101
+ // resolve **em caso de sucesso** — e exclusão é justamente 204. Quem
102
+ // monta este passo na tela erra aqui: compara com `undefined`, lê o
103
+ // sucesso como erro e sai sem recarregar, e a linha some do banco sem
104
+ // sumir da tela.
105
+ if (done === null) {
106
+ return false;
107
+ }
108
+
109
+ await options.reload?.();
110
+
111
+ return true;
112
+ },
113
+ [confirm, run],
114
+ );
115
+
56
116
  const confirmDialog = (
57
117
  <ConfirmDialog
58
118
  open={!!pending}
59
119
  title={pending?.options.title ?? ""}
60
120
  description={pending?.options.description}
121
+ note={pending?.options.note}
61
122
  confirmLabel={pending?.options.confirmLabel ?? t("common.confirm")}
62
123
  cancelLabel={pending?.options.cancelLabel ?? t("common.cancel")}
63
124
  destructive={pending?.options.destructive}
@@ -66,5 +127,5 @@ export function useConfirm(): UseConfirmResult {
66
127
  />
67
128
  );
68
129
 
69
- return { confirm, confirmDialog };
130
+ return { confirm, confirmAndRun, acting, confirmDialog };
70
131
  }
package/src/index.ts CHANGED
@@ -72,7 +72,11 @@ export {
72
72
  // ---------------------------------------------------------------------------
73
73
  export type { UseColumnVisibilityResult } from "#core/hooks/use-column-visibility";
74
74
  export { useColumnVisibility } from "#core/hooks/use-column-visibility";
75
- export type { ConfirmOptions, UseConfirmResult } from "#core/hooks/use-confirm";
75
+ export type {
76
+ ConfirmedActionOptions,
77
+ ConfirmOptions,
78
+ UseConfirmResult,
79
+ } from "#core/hooks/use-confirm";
76
80
  export { useConfirm } from "#core/hooks/use-confirm";
77
81
  export type { UseFilterSchemaResult } from "#core/hooks/use-filter-schema";
78
82
  export { useFilterSchema } from "#core/hooks/use-filter-schema";