rl-core-front 0.17.1 → 0.18.1
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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { useAuth } from "#core/contexts/auth-context";
|
|
6
|
+
|
|
7
|
+
export interface CanProps {
|
|
8
|
+
/** Uma chave, ou todas as da lista. */
|
|
9
|
+
permission?: string | string[];
|
|
10
|
+
/** Pelo menos uma destas. Combinado com `permission`, as duas condições valem. */
|
|
11
|
+
any?: string[];
|
|
12
|
+
/** O que aparece no lugar quando não pode. Ausente, nada. */
|
|
13
|
+
fallback?: ReactNode;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Mostra os filhos só para quem tem a permissão.
|
|
19
|
+
*
|
|
20
|
+
* É o `hasPermission` do `useAuth` em forma de bloco: um interruptor que
|
|
21
|
+
* lança noutro módulo, um botão que só entrega 403 para quem não tem a chave.
|
|
22
|
+
* Esconder é cortesia — quem proíbe de verdade é o backend, e a chave aqui
|
|
23
|
+
* precisa ser a mesma que a rota exige, senão o bloco some para quem podia ou
|
|
24
|
+
* aparece para quem não pode.
|
|
25
|
+
*
|
|
26
|
+
* Sem chave nenhuma, mostra: é como a rota sem decorator.
|
|
27
|
+
*/
|
|
28
|
+
export function Can({ permission, any, fallback = null, children }: CanProps): JSX.Element {
|
|
29
|
+
const { hasAllPermissions, hasAnyPermission } = useAuth();
|
|
30
|
+
const all = permission === undefined ? [] : [permission].flat();
|
|
31
|
+
const allowed =
|
|
32
|
+
(all.length === 0 || hasAllPermissions(...all)) &&
|
|
33
|
+
(!any || any.length === 0 || hasAnyPermission(...any));
|
|
34
|
+
|
|
35
|
+
return <>{allowed ? children : fallback}</>;
|
|
36
|
+
}
|
|
@@ -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,100 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, 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
|
+
useEffect(() => {
|
|
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
|
+
useEffect(() => {
|
|
72
|
+
const fromUrl = readFromUrl();
|
|
73
|
+
const fromStorage = safeStorage.get(`${STORAGE_PREFIX}${storageKey}`);
|
|
74
|
+
const resolved = isTab(fromUrl) ? fromUrl : isTab(fromStorage) ? fromStorage : defaultTab;
|
|
75
|
+
|
|
76
|
+
setStoredTab(resolved);
|
|
77
|
+
|
|
78
|
+
// A URL passa a dizer onde a tela está, venha a aba de onde vier — sem
|
|
79
|
+
// isso, copiar o link depois de cair na aba do storage levaria à padrão.
|
|
80
|
+
// Lista vazia (nenhuma aba permitida) não tem o que dizer.
|
|
81
|
+
if (resolved !== undefined && fromUrl !== resolved) {
|
|
82
|
+
writeToUrl(resolved);
|
|
83
|
+
}
|
|
84
|
+
}, [storageKey, defaultTab, isTab]);
|
|
85
|
+
|
|
86
|
+
const setTab = useCallback(
|
|
87
|
+
(next: string): void => {
|
|
88
|
+
if (!isTab(next)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setStoredTab(next);
|
|
93
|
+
writeToUrl(next);
|
|
94
|
+
safeStorage.set(`${STORAGE_PREFIX}${storageKey}`, next);
|
|
95
|
+
},
|
|
96
|
+
[storageKey, isTab],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return { tab, setTab };
|
|
100
|
+
}
|
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
|