rl-core-front 0.18.7 → 0.18.9

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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/_services/api/axios.factory.ts +3 -0
  3. package/src/_utils/storage.ts +31 -0
  4. package/src/components/app-shell.tsx +173 -114
  5. package/src/components/ui/column-visibility-modal.tsx +11 -4
  6. package/src/components/ui/combobox.tsx +46 -0
  7. package/src/components/ui/filter-sheet.tsx +9 -3
  8. package/src/contexts/auth-context.tsx +6 -1
  9. package/src/contexts/color-mode-context.tsx +29 -22
  10. package/src/contexts/i18n-context.tsx +5 -15
  11. package/src/contexts/socket-context.tsx +10 -1
  12. package/src/features/audit/components/record-audit-button.tsx +3 -1
  13. package/src/features/audit/hooks/use-audit-trail.ts +3 -1
  14. package/src/features/logs/hooks/use-request-logs.ts +3 -1
  15. package/src/features/notifications/hooks/use-notifications.ts +3 -1
  16. package/src/features/profile/profile-screen.tsx +3 -1
  17. package/src/features/queues/hooks/use-queue-jobs.ts +3 -1
  18. package/src/features/rbac/panels/catalog-panel.tsx +3 -1
  19. package/src/features/rbac/panels/groups-panel.tsx +3 -1
  20. package/src/features/rbac/panels/permissions-panel.tsx +3 -1
  21. package/src/features/rbac/panels/roles-panel.tsx +8 -4
  22. package/src/features/recovery/reset-password-screen.tsx +17 -5
  23. package/src/features/users/hooks/use-users.ts +3 -1
  24. package/src/hooks/use-column-visibility.ts +30 -18
  25. package/src/hooks/use-document-title.ts +49 -18
  26. package/src/hooks/use-filter-schema.ts +3 -1
  27. package/src/hooks/use-filters.ts +36 -10
  28. package/src/hooks/use-is-hydrated.ts +21 -0
  29. package/src/hooks/use-job-progress.ts +11 -4
  30. package/src/hooks/use-list-query.ts +9 -4
  31. package/src/hooks/use-stored-value.ts +35 -0
  32. package/src/hooks/use-tab-state.ts +62 -28
  33. package/src/hooks/use-table-view.ts +15 -17
@@ -1,8 +1,16 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useLayoutEffect, useRef, useState } from "react";
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useSyncExternalStore,
9
+ } from "react";
4
10
 
5
11
  import { safeStorage } from "#core/_utils/storage";
12
+ import { useIsHydrated } from "#core/hooks/use-is-hydrated";
13
+ import { useStoredValue } from "#core/hooks/use-stored-value";
6
14
 
7
15
  const STORAGE_PREFIX = "tab:";
8
16
  const DEFAULT_PARAM = "tab";
@@ -28,11 +36,30 @@ export interface UseTabStateResult<T extends string> {
28
36
  const readFromUrl = (param: string): string | null =>
29
37
  new URLSearchParams(window.location.search).get(param);
30
38
 
39
+ /** Quem lê a querystring, para saber que ela mudou por aqui. */
40
+ const urlListeners = new Set<() => void>();
41
+
42
+ const subscribeUrl = (onChange: () => void): (() => void) => {
43
+ urlListeners.add(onChange);
44
+ // O "voltar" do navegador também troca a aba, e esse não passa por aqui.
45
+ window.addEventListener("popstate", onChange);
46
+ return () => {
47
+ urlListeners.delete(onChange);
48
+ window.removeEventListener("popstate", onChange);
49
+ };
50
+ };
51
+
31
52
  /** Troca só o próprio parâmetro, preservando o resto — o `?filter=` da listagem mora ao lado. */
32
53
  const writeToUrl = (param: string, value: string): void => {
33
54
  const params = new URLSearchParams(window.location.search);
34
55
  params.set(param, value);
35
56
  window.history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
57
+
58
+ // `replaceState` não dispara evento nenhum: sem este aviso, a tela ficaria
59
+ // na aba antiga até o próximo render por outro motivo.
60
+ for (const listener of urlListeners) {
61
+ listener();
62
+ }
36
63
  };
37
64
 
38
65
  /**
@@ -61,10 +88,25 @@ export function useTabState<T extends string>(
61
88
  defaultTab: T = tabs[0],
62
89
  { param = DEFAULT_PARAM }: UseTabStateOptions = {},
63
90
  ): UseTabStateResult<T> {
64
- const [tab, setStoredTab] = useState<T>(defaultTab);
91
+ // As duas memórias são de fora do React — a querystring e o armazenamento —,
92
+ // e por isso são lidas no próprio render, e não copiadas para um estado
93
+ // dentro de um efeito: no primeiro render do Next não há `window`, e a
94
+ // leitura do cliente entra assim que ele hidrata, sem um quadro na aba
95
+ // errada.
96
+ const urlTab = useSyncExternalStore(
97
+ subscribeUrl,
98
+ () => readFromUrl(param),
99
+ () => null,
100
+ );
101
+ const storedTab = useStoredValue(`${STORAGE_PREFIX}${storageKey}`);
102
+
103
+ const isTab = (value: string | null): value is T =>
104
+ value !== null && (tabs as readonly string[]).includes(value);
105
+
106
+ const tab: T = isTab(urlTab) ? urlTab : isTab(storedTab) ? storedTab : defaultTab;
65
107
 
66
108
  // Por `ref` porque a lista costuma nascer dentro do render (filtrada por
67
- // permissão, por exemplo): como dependência, ela refaria a hidratação a cada
109
+ // permissão, por exemplo): como dependência, ela refaria o `setTab` a cada
68
110
  // render.
69
111
  const known = useRef(tabs);
70
112
 
@@ -72,43 +114,35 @@ export function useTabState<T extends string>(
72
114
  known.current = tabs;
73
115
  });
74
116
 
75
- const isTab = useCallback((value: string | null): value is T => {
76
- return value !== null && (known.current as readonly string[]).includes(value);
77
- }, []);
117
+ const hydrated = useIsHydrated();
78
118
 
79
- // Hidrata uma vez, no cliente: no primeiro render do Next não `window`,
80
- // e ler ali faria o servidor e o navegador desenharem abas diferentes.
119
+ // A URL passa a dizer onde a tela está, venha a aba de onde vier — sem isso,
120
+ // copiar o link depois de cair na aba do storage levaria à padrão. Lista
121
+ // vazia (nenhuma aba permitida) não tem o que dizer.
81
122
  //
82
- // `useLayoutEffect`, e não `useEffect`: o passivo roda depois da pintura, e
83
- // o F5 mostrava a aba padrão por um quadro antes de pular para a certa.
84
- // O de layout corre antes de o navegador desenhar — a primeira imagem já é
85
- // a aba salva. No servidor o React o ignora, que é o que se quer.
86
- useLayoutEffect(() => {
87
- const fromUrl = readFromUrl(param);
88
- const fromStorage = safeStorage.get(`${STORAGE_PREFIX}${storageKey}`);
89
- const resolved = isTab(fromUrl) ? fromUrl : isTab(fromStorage) ? fromStorage : defaultTab;
90
-
91
- setStoredTab(resolved);
92
-
93
- // A URL passa a dizer onde a tela está, venha a aba de onde vier — sem
94
- // isso, copiar o link depois de cair na aba do storage levaria à padrão.
95
- // Lista vazia (nenhuma aba permitida) não tem o que dizer.
96
- if (resolved !== undefined && fromUrl !== resolved) {
97
- writeToUrl(param, resolved);
123
+ // depois de hidratar: antes disso o armazenamento ainda não foi lido, e
124
+ // escrever aqui gravaria a aba padrão na URL que manda sobre o storage e
125
+ // apagaria justamente a aba que se quer restaurar.
126
+ useEffect(() => {
127
+ if (!hydrated) {
128
+ return;
129
+ }
130
+ if (tab !== undefined && readFromUrl(param) !== tab) {
131
+ writeToUrl(param, tab);
98
132
  }
99
- }, [storageKey, defaultTab, isTab, param]);
133
+ }, [hydrated, tab, param]);
100
134
 
101
135
  const setTab = useCallback(
102
136
  (next: string): void => {
103
- if (!isTab(next)) {
137
+ if (!(known.current as readonly string[]).includes(next)) {
104
138
  return;
105
139
  }
106
140
 
107
- setStoredTab(next);
141
+ // Escrever nos dois já avisa quem lê: a aba vem da URL e do storage.
108
142
  writeToUrl(param, next);
109
143
  safeStorage.set(`${STORAGE_PREFIX}${storageKey}`, next);
110
144
  },
111
- [storageKey, isTab, param],
145
+ [storageKey, param],
112
146
  );
113
147
 
114
148
  return { tab, setTab };
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useState } from "react";
3
+ import { useCallback, useState } from "react";
4
4
 
5
5
  import { safeStorage } from "#core/_utils/storage";
6
+ import { useStoredValue } from "#core/hooks/use-stored-value";
6
7
 
7
8
  const STORAGE_PREFIX = "view:";
8
9
 
@@ -14,6 +15,9 @@ export interface UseTableViewResult {
14
15
  setView: (next: TableView) => void;
15
16
  }
16
17
 
18
+ const isTableView = (value: string | null): value is TableView =>
19
+ value === "list" || value === "grid";
20
+
17
21
  /**
18
22
  * Guarda em qual formato a pessoa deixou a listagem, por `storageKey`.
19
23
  *
@@ -26,28 +30,22 @@ export function useTableView(
26
30
  storageKey?: string,
27
31
  defaultView: TableView = "list",
28
32
  ): UseTableViewResult {
29
- const [view, setStoredView] = useState<TableView>(defaultView);
30
-
31
- useEffect(() => {
32
- if (!storageKey) {
33
- return;
34
- }
35
- const raw = safeStorage.get(`${STORAGE_PREFIX}${storageKey}`);
33
+ const key = storageKey ? `${STORAGE_PREFIX}${storageKey}` : undefined;
34
+ const stored = useStoredValue(key);
35
+ const [sessionView, setSessionView] = useState<TableView>(defaultView);
36
36
 
37
- if (raw === "list" || raw === "grid") {
38
- setStoredView(raw);
39
- }
40
- }, [storageKey]);
37
+ const view = key ? (isTableView(stored) ? stored : defaultView) : sessionView;
41
38
 
42
39
  const setView = useCallback(
43
40
  (next: TableView) => {
44
- setStoredView(next);
45
-
46
- if (storageKey) {
47
- safeStorage.set(`${STORAGE_PREFIX}${storageKey}`, next);
41
+ if (!key) {
42
+ setSessionView(next);
43
+ return;
48
44
  }
45
+ // Escrever já avisa quem lê a chave — o estado vem do armazenamento.
46
+ safeStorage.set(key, next);
49
47
  },
50
- [storageKey],
48
+ [key],
51
49
  );
52
50
 
53
51
  return { view, setView };