rl-core-front 0.18.0 → 0.18.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 +1 -1
- package/src/features/rbac/rbac-screen.tsx +7 -1
- package/src/hooks/use-tab-state.ts +105 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
GroupsPanel,
|
|
10
10
|
RolesPanel,
|
|
11
11
|
} from "#core/features/rbac/panels";
|
|
12
|
+
import { useTabState } from "#core/hooks/use-tab-state";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Controle de acesso, organizado pelo que se vem fazer aqui.
|
|
@@ -54,11 +55,16 @@ export function RbacScreen(): JSX.Element {
|
|
|
54
55
|
},
|
|
55
56
|
].filter((tab) => tab.perm.some((code) => hasPermission(code)));
|
|
56
57
|
|
|
58
|
+
const { tab, setTab } = useTabState(
|
|
59
|
+
"rbac",
|
|
60
|
+
tabs.map((item) => item.value),
|
|
61
|
+
);
|
|
62
|
+
|
|
57
63
|
return (
|
|
58
64
|
<div>
|
|
59
65
|
<h1 className="mb-4 text-3xl font-bold">{t("rbac.title")}</h1>
|
|
60
66
|
{tabs.length > 0 && (
|
|
61
|
-
<Tabs
|
|
67
|
+
<Tabs value={tab} onValueChange={setTab}>
|
|
62
68
|
<TabsList>
|
|
63
69
|
{tabs.map((tab) => (
|
|
64
70
|
<TabsTrigger key={tab.value} value={tab.value}>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { safeStorage } from "#core/_utils/storage";
|
|
6
|
+
|
|
7
|
+
const STORAGE_PREFIX = "tab:";
|
|
8
|
+
const QUERY_PARAM = "tab";
|
|
9
|
+
|
|
10
|
+
export interface UseTabStateResult<T extends string> {
|
|
11
|
+
tab: T;
|
|
12
|
+
/**
|
|
13
|
+
* Recebe `string` porque é o que o `Tabs` e o `TabShortcuts` entregam; valor
|
|
14
|
+
* fora da lista é ignorado, para um link editado à mão não abrir aba nenhuma.
|
|
15
|
+
*/
|
|
16
|
+
setTab: (next: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const readFromUrl = (): string | null =>
|
|
20
|
+
new URLSearchParams(window.location.search).get(QUERY_PARAM);
|
|
21
|
+
|
|
22
|
+
/** Troca só o `?tab=`, preservando o resto — o `?filter=` da listagem mora ao lado. */
|
|
23
|
+
const writeToUrl = (value: string): void => {
|
|
24
|
+
const params = new URLSearchParams(window.location.search);
|
|
25
|
+
params.set(QUERY_PARAM, value);
|
|
26
|
+
window.history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Em qual aba a pessoa deixou a tela, por `storageKey`.
|
|
31
|
+
*
|
|
32
|
+
* Duas memórias, porque cada uma responde a uma pergunta diferente. A
|
|
33
|
+
* querystring (`?tab=`) faz o F5 voltar onde estava e transforma a aba em
|
|
34
|
+
* link compartilhável — o mesmo motivo do `?filter=` do `useFilters`. O
|
|
35
|
+
* `localStorage` cobre o que a URL não alcança: abrir a tela pelo menu amanhã
|
|
36
|
+
* e cair na última aba usada, e não na primeira.
|
|
37
|
+
*
|
|
38
|
+
* A URL manda quando as duas discordam: quem recebeu um link quer ver o que o
|
|
39
|
+
* link aponta, não a última aba de quem abriu.
|
|
40
|
+
*
|
|
41
|
+
* O espelho é `replaceState`, como no filtro: trocar de aba não é navegação, e
|
|
42
|
+
* empilhar histórico faria o "voltar" percorrer aba por aba antes de sair da
|
|
43
|
+
* tela.
|
|
44
|
+
*
|
|
45
|
+
* `tabs` é a lista do que existe: o que se lê da URL ou do storage e não está
|
|
46
|
+
* nela cai no padrão — uma aba que foi removida numa versão nova, ou um link
|
|
47
|
+
* digitado errado, abre a tela em vez de deixá-la vazia.
|
|
48
|
+
*/
|
|
49
|
+
export function useTabState<T extends string>(
|
|
50
|
+
storageKey: string,
|
|
51
|
+
tabs: readonly T[],
|
|
52
|
+
defaultTab: T = tabs[0],
|
|
53
|
+
): UseTabStateResult<T> {
|
|
54
|
+
const [tab, setStoredTab] = useState<T>(defaultTab);
|
|
55
|
+
|
|
56
|
+
// Por `ref` porque a lista costuma nascer dentro do render (filtrada por
|
|
57
|
+
// permissão, por exemplo): como dependência, ela refaria a hidratação a cada
|
|
58
|
+
// render.
|
|
59
|
+
const known = useRef(tabs);
|
|
60
|
+
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
known.current = tabs;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const isTab = useCallback((value: string | null): value is T => {
|
|
66
|
+
return value !== null && (known.current as readonly string[]).includes(value);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Hidrata uma vez, no cliente: no primeiro render do Next não há `window`,
|
|
70
|
+
// e ler ali faria o servidor e o navegador desenharem abas diferentes.
|
|
71
|
+
//
|
|
72
|
+
// `useLayoutEffect`, e não `useEffect`: o passivo roda depois da pintura, e
|
|
73
|
+
// o F5 mostrava a aba padrão por um quadro antes de pular para a certa.
|
|
74
|
+
// O de layout corre antes de o navegador desenhar — a primeira imagem já é
|
|
75
|
+
// a aba salva. No servidor o React o ignora, que é o que se quer.
|
|
76
|
+
useLayoutEffect(() => {
|
|
77
|
+
const fromUrl = readFromUrl();
|
|
78
|
+
const fromStorage = safeStorage.get(`${STORAGE_PREFIX}${storageKey}`);
|
|
79
|
+
const resolved = isTab(fromUrl) ? fromUrl : isTab(fromStorage) ? fromStorage : defaultTab;
|
|
80
|
+
|
|
81
|
+
setStoredTab(resolved);
|
|
82
|
+
|
|
83
|
+
// A URL passa a dizer onde a tela está, venha a aba de onde vier — sem
|
|
84
|
+
// isso, copiar o link depois de cair na aba do storage levaria à padrão.
|
|
85
|
+
// Lista vazia (nenhuma aba permitida) não tem o que dizer.
|
|
86
|
+
if (resolved !== undefined && fromUrl !== resolved) {
|
|
87
|
+
writeToUrl(resolved);
|
|
88
|
+
}
|
|
89
|
+
}, [storageKey, defaultTab, isTab]);
|
|
90
|
+
|
|
91
|
+
const setTab = useCallback(
|
|
92
|
+
(next: string): void => {
|
|
93
|
+
if (!isTab(next)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setStoredTab(next);
|
|
98
|
+
writeToUrl(next);
|
|
99
|
+
safeStorage.set(`${STORAGE_PREFIX}${storageKey}`, next);
|
|
100
|
+
},
|
|
101
|
+
[storageKey, isTab],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return { tab, setTab };
|
|
105
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -121,6 +121,8 @@ export {
|
|
|
121
121
|
TAB_SHORTCUT_DIGITS,
|
|
122
122
|
useShortcut,
|
|
123
123
|
} from "#core/hooks/use-shortcut";
|
|
124
|
+
export type { UseTabStateResult } from "#core/hooks/use-tab-state";
|
|
125
|
+
export { useTabState } from "#core/hooks/use-tab-state";
|
|
124
126
|
export type { TableView, UseTableViewResult } from "#core/hooks/use-table-view";
|
|
125
127
|
export { useTableView } from "#core/hooks/use-table-view";
|
|
126
128
|
// RequestOperation é enum (valor, não só tipo): sem ele o projeto não consegue
|