rl-core-front 0.16.7 → 0.17.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/package.json +1 -1
- package/src/components/breadcrumbs.tsx +6 -0
- package/src/components/ui/combobox.tsx +80 -7
- package/src/components/ui/export-button.tsx +11 -11
- package/src/hooks/use-document-title.ts +40 -0
- package/src/hooks/use-export.ts +52 -67
- package/src/i18n/messages/en.ts +2 -1
- package/src/i18n/messages/pt.ts +2 -1
- package/src/index.ts +6 -1
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
breadcrumbTrail,
|
|
10
10
|
} from "#core/_utils/breadcrumb-trail";
|
|
11
11
|
import { useI18n } from "#core/contexts";
|
|
12
|
+
import { useDocumentTitle } from "#core/hooks/use-document-title";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Rótulo das telas do próprio core — o projeto não precisa cadastrá-las.
|
|
@@ -36,6 +37,11 @@ export function Breadcrumbs({ navItems }: BreadcrumbsProps): JSX.Element {
|
|
|
36
37
|
|
|
37
38
|
const trail = breadcrumbTrail(pathname, navItems, SEGMENT_LABELS);
|
|
38
39
|
|
|
40
|
+
// A aba do navegador acompanha a migalha: o último degrau é a tela, e é o
|
|
41
|
+
// mesmo nome nos dois lugares — inclusive nas telas do próprio core.
|
|
42
|
+
const ultimo = trail[trail.length - 1];
|
|
43
|
+
useDocumentTitle(ultimo ? t(ultimo.label) : null);
|
|
44
|
+
|
|
39
45
|
return (
|
|
40
46
|
<nav className="mb-4 flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
41
47
|
<button
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { Check, ChevronDown, X } from "lucide-react";
|
|
3
|
+
import { Check, ChevronDown, Plus, X } from "lucide-react";
|
|
4
4
|
import type { JSX } from "react";
|
|
5
5
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
6
6
|
|
|
@@ -39,8 +39,26 @@ export interface ComboboxProps {
|
|
|
39
39
|
searchPlaceholder?: string;
|
|
40
40
|
emptyMessage?: string;
|
|
41
41
|
disabled?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Deixa criar o que foi digitado quando nenhuma opção tem exatamente esse
|
|
44
|
+
* nome. A linha "Criar" aparece no fim da lista e o Enter a escolhe quando
|
|
45
|
+
* ela é a apontada. Quem recebe o texto é o `onCreate` — o componente não
|
|
46
|
+
* sabe se isso vira POST ou só um valor no formulário.
|
|
47
|
+
*/
|
|
48
|
+
creatable?: boolean;
|
|
49
|
+
/** Chamado com o texto digitado, já sem espaços nas pontas. */
|
|
50
|
+
onCreate?: (text: string) => void;
|
|
51
|
+
/** Texto da linha de criação. Ausente, `Criar "{text}"`. */
|
|
52
|
+
createLabel?: (text: string) => string;
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
/** Id da linha de criação na lista apontável — nenhuma opção real o usa. */
|
|
56
|
+
const CREATE_ID = "\u0000create";
|
|
57
|
+
|
|
58
|
+
/** Id DOM do item, para o `aria-activedescendant`. O caractere nulo não vai para o HTML. */
|
|
59
|
+
const idDoItem = (lista: string, id: string): string =>
|
|
60
|
+
`${lista}-${id === CREATE_ID ? "create" : id}`;
|
|
61
|
+
|
|
44
62
|
/**
|
|
45
63
|
* Escolha com busca, de um valor ou de vários.
|
|
46
64
|
*
|
|
@@ -52,8 +70,10 @@ export interface ComboboxProps {
|
|
|
52
70
|
* marcar: a busca, a lista e o painel são os mesmos. Dois componentes seriam o
|
|
53
71
|
* mesmo arquivo duas vezes.
|
|
54
72
|
*
|
|
55
|
-
*
|
|
56
|
-
* na origem, e um "criar" aqui daria um botão que falha na
|
|
73
|
+
* Por padrão só escolhe — não cria. Quem cadastra o item nem sempre é quem
|
|
74
|
+
* pode cadastrar na origem, e um "criar" aqui daria um botão que falha na
|
|
75
|
+
* cara de quem clicou. Com `creatable`, a decisão é de quem usa: a lista
|
|
76
|
+
* ganha a linha "Criar" e o texto sai pelo `onCreate`.
|
|
57
77
|
*
|
|
58
78
|
* **O foco nunca sai do campo de busca.** Quem aponta o item é o `ativo`, e não
|
|
59
79
|
* o foco do navegador: dentro do menu do Radix, foco em item entrega as teclas
|
|
@@ -70,6 +90,9 @@ export function Combobox({
|
|
|
70
90
|
searchPlaceholder,
|
|
71
91
|
emptyMessage,
|
|
72
92
|
disabled,
|
|
93
|
+
creatable = false,
|
|
94
|
+
onCreate,
|
|
95
|
+
createLabel,
|
|
73
96
|
}: ComboboxProps): JSX.Element {
|
|
74
97
|
const { t } = useI18n();
|
|
75
98
|
const [open, setOpen] = useState(false);
|
|
@@ -98,7 +121,7 @@ export function Combobox({
|
|
|
98
121
|
);
|
|
99
122
|
|
|
100
123
|
const termo = search.trim();
|
|
101
|
-
const
|
|
124
|
+
const filtradas = useMemo(
|
|
102
125
|
() =>
|
|
103
126
|
options.filter((option) =>
|
|
104
127
|
option.name.toLowerCase().includes(termo.toLowerCase()),
|
|
@@ -106,6 +129,21 @@ export function Combobox({
|
|
|
106
129
|
[options, termo],
|
|
107
130
|
);
|
|
108
131
|
|
|
132
|
+
/**
|
|
133
|
+
* A linha "Criar" só quando há texto e nenhuma opção tem exatamente esse
|
|
134
|
+
* nome — com "Kronos" na lista, digitar "kronos" deve escolher, não criar.
|
|
135
|
+
*/
|
|
136
|
+
const podeCriar =
|
|
137
|
+
creatable &&
|
|
138
|
+
termo !== "" &&
|
|
139
|
+
!options.some((option) => option.name.toLowerCase() === termo.toLowerCase());
|
|
140
|
+
|
|
141
|
+
/** O que se aponta pelo teclado: as opções filtradas e, no fim, a de criar. */
|
|
142
|
+
const visiveis = useMemo(
|
|
143
|
+
() => (podeCriar ? [...filtradas, { id: CREATE_ID, name: termo }] : filtradas),
|
|
144
|
+
[filtradas, podeCriar, termo],
|
|
145
|
+
);
|
|
146
|
+
|
|
109
147
|
/** A posição do apontado; sem ele — ou fora da busca — é a do primeiro. */
|
|
110
148
|
const ativo = Math.max(
|
|
111
149
|
visiveis.findIndex((option) => option.id === apontado),
|
|
@@ -113,6 +151,15 @@ export function Combobox({
|
|
|
113
151
|
);
|
|
114
152
|
|
|
115
153
|
const alternar = (id: string): void => {
|
|
154
|
+
if (id === CREATE_ID) {
|
|
155
|
+
onCreate?.(termo);
|
|
156
|
+
setSearch("");
|
|
157
|
+
if (!multiple) {
|
|
158
|
+
setOpen(false);
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
116
163
|
if (!multiple) {
|
|
117
164
|
onChange([id]);
|
|
118
165
|
setOpen(false);
|
|
@@ -296,7 +343,7 @@ export function Combobox({
|
|
|
296
343
|
// Quem lê a tela precisa saber qual item as setas apontam: o foco
|
|
297
344
|
// fica aqui, então é este atributo que conta a navegação.
|
|
298
345
|
aria-activedescendant={
|
|
299
|
-
visiveis[ativo] ?
|
|
346
|
+
visiveis[ativo] ? idDoItem(idLista, visiveis[ativo].id) : undefined
|
|
300
347
|
}
|
|
301
348
|
onChange={(event) => setSearch(event.target.value)}
|
|
302
349
|
/*
|
|
@@ -348,10 +395,10 @@ export function Combobox({
|
|
|
348
395
|
/>
|
|
349
396
|
</div>
|
|
350
397
|
|
|
351
|
-
{
|
|
398
|
+
{filtradas.map((option, indice) => (
|
|
352
399
|
<DropdownMenuItem
|
|
353
400
|
key={option.id}
|
|
354
|
-
id={
|
|
401
|
+
id={idDoItem(idLista, option.id)}
|
|
355
402
|
ref={(node) => {
|
|
356
403
|
itensRef.current[option.id] = node;
|
|
357
404
|
}}
|
|
@@ -383,6 +430,32 @@ export function Combobox({
|
|
|
383
430
|
</DropdownMenuItem>
|
|
384
431
|
))}
|
|
385
432
|
|
|
433
|
+
{podeCriar && (
|
|
434
|
+
<DropdownMenuItem
|
|
435
|
+
id={idDoItem(idLista, CREATE_ID)}
|
|
436
|
+
ref={(node) => {
|
|
437
|
+
itensRef.current[CREATE_ID] = node;
|
|
438
|
+
}}
|
|
439
|
+
className={cn(
|
|
440
|
+
"flex w-full items-center gap-2",
|
|
441
|
+
ativo === filtradas.length && "bg-accent text-accent-foreground",
|
|
442
|
+
)}
|
|
443
|
+
onPointerMove={(event) => {
|
|
444
|
+
event.preventDefault();
|
|
445
|
+
setApontado(CREATE_ID);
|
|
446
|
+
}}
|
|
447
|
+
onSelect={(event) => {
|
|
448
|
+
event.preventDefault();
|
|
449
|
+
alternar(CREATE_ID);
|
|
450
|
+
}}
|
|
451
|
+
>
|
|
452
|
+
<Plus className="h-4 w-4 shrink-0" />
|
|
453
|
+
<span className="truncate">
|
|
454
|
+
{createLabel ? createLabel(termo) : t("common.createNamed", { text: termo })}
|
|
455
|
+
</span>
|
|
456
|
+
</DropdownMenuItem>
|
|
457
|
+
)}
|
|
458
|
+
|
|
386
459
|
{visiveis.length === 0 && (
|
|
387
460
|
<p className="px-2 py-3 text-center text-sm text-muted-foreground">
|
|
388
461
|
{emptyMessage ?? t("common.noResults")}
|
|
@@ -14,28 +14,30 @@ export interface ExportButtonProps extends Omit<ButtonProps, "children"> {
|
|
|
14
14
|
query?: ExportQuery;
|
|
15
15
|
/** Texto do botão. Ausente, "Exportar". */
|
|
16
16
|
label?: string;
|
|
17
|
+
/** O que o painel de jobs mostra enquanto gera. Ausente, "Relatório". */
|
|
18
|
+
jobLabel?: string;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Baixa a listagem em planilha.
|
|
21
23
|
*
|
|
22
|
-
* O relatório é gerado fora da requisição, então o botão não trava a tela:
|
|
23
|
-
*
|
|
24
|
-
* Fica desabilitado durante a geração porque um segundo pedido só faria a
|
|
25
|
-
* planilha duas vezes. A falha vira toast aqui dentro — a tela que usa o
|
|
26
|
-
* não escreve tratamento de erro nenhum.
|
|
24
|
+
* O relatório é gerado fora da requisição, então o botão não trava a tela: o
|
|
25
|
+
* andamento aparece no painel de jobs do canto, e o arquivo baixa sozinho no
|
|
26
|
+
* fim. Fica desabilitado durante a geração porque um segundo pedido só faria a
|
|
27
|
+
* mesma planilha duas vezes. A falha vira toast aqui dentro — a tela que usa o
|
|
28
|
+
* botão não escreve tratamento de erro nenhum.
|
|
27
29
|
*/
|
|
28
30
|
const ExportButton = React.forwardRef<HTMLButtonElement, ExportButtonProps>(
|
|
29
|
-
({ handler, query, label, variant = "outline", ...props }, ref) => {
|
|
31
|
+
({ handler, query, label, jobLabel, variant = "outline", ...props }, ref) => {
|
|
30
32
|
const { t } = useI18n();
|
|
31
33
|
const { notify } = useToast();
|
|
32
|
-
const { request, generating
|
|
34
|
+
const { request, generating } = useExport();
|
|
33
35
|
|
|
34
36
|
// O aviso é do botão, não da tela: quem usa o componente não deveria
|
|
35
37
|
// precisar escrever tratamento de erro para ganhar o comportamento.
|
|
36
38
|
const run = async (): Promise<void> => {
|
|
37
39
|
try {
|
|
38
|
-
await request(handler, query);
|
|
40
|
+
await request(handler, query, { label: jobLabel });
|
|
39
41
|
} catch (error) {
|
|
40
42
|
notify((error as Error).message || t("export.failed"), "error");
|
|
41
43
|
}
|
|
@@ -52,9 +54,7 @@ const ExportButton = React.forwardRef<HTMLButtonElement, ExportButtonProps>(
|
|
|
52
54
|
{generating ? (
|
|
53
55
|
<>
|
|
54
56
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
55
|
-
{
|
|
56
|
-
? t("export.progress", { rows: processed.toLocaleString() })
|
|
57
|
-
: t("export.starting")}
|
|
57
|
+
{t("export.starting")}
|
|
58
58
|
</>
|
|
59
59
|
) : (
|
|
60
60
|
<>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* O título com que a página carregou — o que o `metadata` do projeto mandou.
|
|
7
|
+
*
|
|
8
|
+
* Lido uma vez por carregamento e nunca reescrito: é o sufixo de todos os
|
|
9
|
+
* títulos daqui em diante, e o projeto não precisa dizer duas vezes como se
|
|
10
|
+
* chama (o prefixo "Dev - " que ele põe no `metadata` vem junto de graça).
|
|
11
|
+
*/
|
|
12
|
+
let baseTitle: string | null = null;
|
|
13
|
+
|
|
14
|
+
/** O que fica entre o nome da tela e o do sistema. */
|
|
15
|
+
const SEPARATOR = " · ";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A aba do navegador diz em que tela se está.
|
|
19
|
+
*
|
|
20
|
+
* Com cinco abas do sistema abertas, todas se chamavam só pelo nome dele — e
|
|
21
|
+
* achar a do financeiro era clicar uma a uma. A tela vem **antes** do nome do
|
|
22
|
+
* sistema, e não depois: a aba trunca pelo fim, e "ControlX - Controle
|
|
23
|
+
* fina…" vira "ControlX" em todas.
|
|
24
|
+
*
|
|
25
|
+
* `null` devolve o título de origem — é o que a tela sem migalha faz.
|
|
26
|
+
*/
|
|
27
|
+
export const useDocumentTitle = (title: string | null): void => {
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (baseTitle === null) {
|
|
30
|
+
baseTitle = document.title;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!title) {
|
|
34
|
+
document.title = baseTitle;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
document.title = baseTitle ? `${title}${SEPARATOR}${baseTitle}` : title;
|
|
39
|
+
}, [title]);
|
|
40
|
+
};
|
package/src/hooks/use-export.ts
CHANGED
|
@@ -1,44 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback,
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import { api } from "#core/_services/api/axios.factory";
|
|
6
|
+
import { useI18n } from "#core/contexts/i18n-context";
|
|
7
|
+
import { useJobs } from "#core/contexts/jobs-context";
|
|
6
8
|
|
|
7
|
-
/**
|
|
8
|
-
const POLL_INTERVAL_MS = 1500;
|
|
9
|
-
|
|
10
|
-
/** O recorte que vai junto — o mesmo da listagem. */
|
|
9
|
+
/** O recorte que vai junto — o mesmo da listagem, ou os parâmetros do documento. */
|
|
11
10
|
export interface ExportQuery {
|
|
12
11
|
filter?: string;
|
|
13
12
|
search?: string;
|
|
14
13
|
sortBy?: string;
|
|
15
14
|
sortDir?: "ASC" | "DESC";
|
|
15
|
+
/**
|
|
16
|
+
* Parâmetros de um relatório em **documento** (mês, ids escolhidos…),
|
|
17
|
+
* validados pelo handler ainda na rota. Planilha ignora.
|
|
18
|
+
*/
|
|
19
|
+
params?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ExportOptions {
|
|
23
|
+
/** O que o painel de jobs mostra enquanto gera. Ausente, "Relatório". */
|
|
24
|
+
label?: string;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
/** O que o processador devolve — o nome do arquivo é o que o download usa. */
|
|
28
|
+
interface ExportJobSummary {
|
|
29
|
+
fileName: string;
|
|
21
30
|
totalRows: number | null;
|
|
22
|
-
fileName: string | null;
|
|
23
|
-
errorMessage: string | null;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export interface UseExportResult {
|
|
27
|
-
/** Pede o relatório. Devolve quando ele
|
|
28
|
-
request: (
|
|
29
|
-
|
|
34
|
+
/** Pede o relatório. Devolve quando ele foi baixado — ou lança, se falhou. */
|
|
35
|
+
request: (
|
|
36
|
+
handler: string,
|
|
37
|
+
query?: ExportQuery,
|
|
38
|
+
options?: ExportOptions,
|
|
39
|
+
) => Promise<void>;
|
|
40
|
+
/** Há um relatório pedido por este hook sendo gerado agora. */
|
|
30
41
|
generating: boolean;
|
|
31
|
-
/** Linhas já escritas. Sem total: em stream ele só é conhecido no fim. */
|
|
32
|
-
processed: number;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
/**
|
|
36
45
|
* Pede um relatório e o entrega quando fica pronto.
|
|
37
46
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
47
|
+
* O acompanhamento é do `JobsProvider`: o pedido vira uma sessão no painel
|
|
48
|
+
* de jobs do canto — o mesmo do upload de imagem — que recebe progresso pelo
|
|
49
|
+
* socket e se reconcilia por HTTP quando a conexão cai. Sair da tela não
|
|
50
|
+
* interrompe o acompanhamento; o download automático, sim, porque a promessa
|
|
51
|
+
* dele morre com quem a esperava — o arquivo continua na pasta até a faxina.
|
|
42
52
|
*
|
|
43
53
|
* ```tsx
|
|
44
54
|
* const { request, generating } = useExport();
|
|
@@ -46,23 +56,18 @@ export interface UseExportResult {
|
|
|
46
56
|
* ```
|
|
47
57
|
*/
|
|
48
58
|
export function useExport(): UseExportResult {
|
|
59
|
+
const { t } = useI18n();
|
|
60
|
+
const { start } = useJobs();
|
|
49
61
|
const [generating, setGenerating] = useState(false);
|
|
50
|
-
const [processed, setProcessed] = useState(0);
|
|
51
|
-
|
|
52
|
-
// Evita continuar perguntando por um relatório de uma tela que já saiu.
|
|
53
|
-
const activeRef = useRef(true);
|
|
54
|
-
|
|
55
|
-
useEffect(() => {
|
|
56
|
-
activeRef.current = true;
|
|
57
|
-
return () => {
|
|
58
|
-
activeRef.current = false;
|
|
59
|
-
};
|
|
60
|
-
}, []);
|
|
61
62
|
|
|
62
63
|
const request = useCallback(
|
|
63
|
-
async (
|
|
64
|
+
async (
|
|
65
|
+
handler: string,
|
|
66
|
+
query: ExportQuery = {},
|
|
67
|
+
options: ExportOptions = {},
|
|
68
|
+
): Promise<void> => {
|
|
64
69
|
setGenerating(true);
|
|
65
|
-
|
|
70
|
+
const job = start<ExportJobSummary>(options.label ?? t("export.job"));
|
|
66
71
|
|
|
67
72
|
try {
|
|
68
73
|
const { data } = await api.post<{ jobId: string }>(
|
|
@@ -70,46 +75,29 @@ export function useExport(): UseExportResult {
|
|
|
70
75
|
query,
|
|
71
76
|
);
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
job.bind(data.jobId);
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (status.state === "failed") {
|
|
79
|
-
throw new Error(status.errorMessage ?? "A geração do relatório falhou");
|
|
80
|
-
}
|
|
80
|
+
const summary = await job.done;
|
|
81
81
|
|
|
82
|
-
await download(data.jobId,
|
|
82
|
+
await download(data.jobId, summary.fileName);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// `fail` é idempotente para quem chama: se o socket já trouxe a
|
|
85
|
+
// falha, isto não duplica nada — mas se foi o HTTP que recusou, é o
|
|
86
|
+
// que tira a sessão do painel.
|
|
87
|
+
job.fail(messageOf(error, t("export.failed")));
|
|
88
|
+
throw error;
|
|
83
89
|
} finally {
|
|
84
90
|
setGenerating(false);
|
|
85
91
|
}
|
|
86
92
|
},
|
|
87
|
-
[],
|
|
93
|
+
[start, t],
|
|
88
94
|
);
|
|
89
95
|
|
|
90
|
-
return { request, generating
|
|
96
|
+
return { request, generating };
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
active: { current: boolean },
|
|
96
|
-
onProgress: (processed: number) => void,
|
|
97
|
-
): Promise<ExportStatus | null> => {
|
|
98
|
-
for (;;) {
|
|
99
|
-
if (!active.current) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const { data } = await api.get<ExportStatus>(`/exports/${jobId}/status`);
|
|
104
|
-
onProgress(data.processed);
|
|
105
|
-
|
|
106
|
-
if (data.state === "completed" || data.state === "failed") {
|
|
107
|
-
return data;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
111
|
-
}
|
|
112
|
-
};
|
|
99
|
+
const messageOf = (error: unknown, fallback: string): string =>
|
|
100
|
+
error instanceof Error && error.message ? error.message : fallback;
|
|
113
101
|
|
|
114
102
|
/**
|
|
115
103
|
* Baixa o arquivo.
|
|
@@ -119,10 +107,7 @@ const waitUntilDone = async (
|
|
|
119
107
|
* `revokeObjectURL` devolve a memória do blob, que senão fica presa até a aba
|
|
120
108
|
* fechar.
|
|
121
109
|
*/
|
|
122
|
-
const download = async (
|
|
123
|
-
jobId: string,
|
|
124
|
-
fileName: string | null,
|
|
125
|
-
): Promise<void> => {
|
|
110
|
+
const download = async (jobId: string, fileName: string): Promise<void> => {
|
|
126
111
|
const { data } = await api.get<Blob>(`/exports/${jobId}`, {
|
|
127
112
|
responseType: "blob",
|
|
128
113
|
});
|
|
@@ -131,7 +116,7 @@ const download = async (
|
|
|
131
116
|
const link = document.createElement("a");
|
|
132
117
|
|
|
133
118
|
link.href = url;
|
|
134
|
-
link.download = fileName
|
|
119
|
+
link.download = fileName;
|
|
135
120
|
document.body.appendChild(link);
|
|
136
121
|
link.click();
|
|
137
122
|
link.remove();
|
package/src/i18n/messages/en.ts
CHANGED
|
@@ -170,7 +170,7 @@ export const en: Messages = {
|
|
|
170
170
|
export: {
|
|
171
171
|
label: "Export",
|
|
172
172
|
starting: "Generating...",
|
|
173
|
-
|
|
173
|
+
job: "Report",
|
|
174
174
|
failed: "Could not generate the report",
|
|
175
175
|
},
|
|
176
176
|
table: {
|
|
@@ -294,6 +294,7 @@ export const en: Messages = {
|
|
|
294
294
|
deleteWarning:
|
|
295
295
|
"This action cannot be undone. The record leaves the lists and stops appearing in the system.",
|
|
296
296
|
create: "Create",
|
|
297
|
+
createNamed: 'Create "{text}"',
|
|
297
298
|
confirm: "Confirm",
|
|
298
299
|
send: "Send",
|
|
299
300
|
yes: "Yes",
|
package/src/i18n/messages/pt.ts
CHANGED
|
@@ -172,7 +172,7 @@ export const pt = {
|
|
|
172
172
|
export: {
|
|
173
173
|
label: "Exportar",
|
|
174
174
|
starting: "Gerando...",
|
|
175
|
-
|
|
175
|
+
job: "Relatório",
|
|
176
176
|
failed: "Não foi possível gerar o relatório",
|
|
177
177
|
},
|
|
178
178
|
table: {
|
|
@@ -297,6 +297,7 @@ export const pt = {
|
|
|
297
297
|
deleteWarning:
|
|
298
298
|
"Esta ação não pode ser desfeita. O registro sai das listas e deixa de aparecer no sistema.",
|
|
299
299
|
create: "Criar",
|
|
300
|
+
createNamed: 'Criar "{text}"',
|
|
300
301
|
confirm: "Confirmar",
|
|
301
302
|
send: "Enviar",
|
|
302
303
|
yes: "Sim",
|
package/src/index.ts
CHANGED
|
@@ -82,7 +82,12 @@ export type {
|
|
|
82
82
|
UseConfirmResult,
|
|
83
83
|
} from "#core/hooks/use-confirm";
|
|
84
84
|
export { useConfirm } from "#core/hooks/use-confirm";
|
|
85
|
-
export
|
|
85
|
+
export { useDocumentTitle } from "#core/hooks/use-document-title";
|
|
86
|
+
export type {
|
|
87
|
+
ExportOptions,
|
|
88
|
+
ExportQuery,
|
|
89
|
+
UseExportResult,
|
|
90
|
+
} from "#core/hooks/use-export";
|
|
86
91
|
export { useExport } from "#core/hooks/use-export";
|
|
87
92
|
export type { UseFilterSchemaResult } from "#core/hooks/use-filter-schema";
|
|
88
93
|
export { useFilterSchema } from "#core/hooks/use-filter-schema";
|