rl-core-front 0.3.0 → 0.4.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/_services/api/schema.d.ts +43 -132
- package/src/_utils/filter.ts +15 -34
- package/src/components/app-shell.tsx +51 -1
- package/src/components/brand-header.tsx +23 -8
- package/src/components/brand-panel.tsx +41 -0
- package/src/components/index.ts +1 -0
- package/src/components/ui/button.tsx +5 -0
- package/src/components/ui/confirm-dialog.tsx +3 -1
- package/src/components/ui/data-table.tsx +13 -4
- package/src/components/ui/dialog.tsx +120 -21
- package/src/components/ui/filter-sheet.tsx +47 -25
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/segmented-control.tsx +74 -0
- package/src/contexts/brand-context.tsx +15 -0
- package/src/contexts/i18n-context.tsx +69 -4
- package/src/features/audit/audit-screen.tsx +13 -140
- package/src/features/audit/components/audit-diff.tsx +156 -0
- package/src/features/audit/components/index.ts +3 -0
- package/src/features/audit/components/record-audit-button.tsx +176 -0
- package/src/features/audit/enums/audit-data-action.enum.ts +20 -0
- package/src/features/audit/hooks/use-audit-trail.ts +9 -2
- package/src/features/audit/services/audit.service.ts +43 -1
- package/src/features/login/login-screen.tsx +86 -40
- package/src/features/logs/enums/error-type.enum.ts +21 -0
- package/src/features/logs/enums/request-outcome.enum.ts +14 -0
- package/src/features/logs/hooks/use-request-logs.ts +26 -3
- package/src/features/logs/logs-screen.tsx +2 -14
- package/src/features/logs/panels/index.ts +0 -1
- package/src/features/logs/panels/request-logs-panel.tsx +177 -25
- package/src/features/logs/services/logs.service.ts +13 -51
- package/src/features/notifications/components/notification-item.tsx +10 -8
- package/src/features/notifications/enums/notification-type.enum.ts +8 -0
- package/src/features/profile/profile-screen.tsx +2 -1
- package/src/features/rbac/panels/groups-panel.tsx +20 -7
- package/src/features/users/components/user-form-dialog.tsx +16 -7
- package/src/features/users/hooks/use-users.ts +9 -2
- package/src/hooks/use-filters.ts +25 -2
- package/src/hooks/use-list-query.ts +14 -1
- package/src/i18n/messages/en.ts +17 -5
- package/src/i18n/messages/pt.ts +18 -5
- package/src/index.ts +12 -1
- package/src/components/ui/filter-chips.tsx +0 -88
- package/src/features/logs/hooks/use-error-logs.ts +0 -65
- package/src/features/logs/panels/error-logs-panel.tsx +0 -183
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-front",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Telas e componentes Next.js do core: login com 2FA, usu\u00e1rios, RBAC, auditoria, logs e listagens com filtro din\u00e2mico",
|
|
5
5
|
"author": "Rodrigo Liberti",
|
|
6
6
|
"license": "MIT",
|
|
@@ -608,14 +608,14 @@ export interface paths {
|
|
|
608
608
|
patch?: never;
|
|
609
609
|
trace?: never;
|
|
610
610
|
};
|
|
611
|
-
"/audit/
|
|
611
|
+
"/audit/requests": {
|
|
612
612
|
parameters: {
|
|
613
613
|
query?: never;
|
|
614
614
|
header?: never;
|
|
615
615
|
path?: never;
|
|
616
616
|
cookie?: never;
|
|
617
617
|
};
|
|
618
|
-
get: operations["
|
|
618
|
+
get: operations["AuditController_requests"];
|
|
619
619
|
put?: never;
|
|
620
620
|
post?: never;
|
|
621
621
|
delete?: never;
|
|
@@ -624,46 +624,14 @@ export interface paths {
|
|
|
624
624
|
patch?: never;
|
|
625
625
|
trace?: never;
|
|
626
626
|
};
|
|
627
|
-
"/audit/
|
|
627
|
+
"/audit/requests/filter-schema": {
|
|
628
628
|
parameters: {
|
|
629
629
|
query?: never;
|
|
630
630
|
header?: never;
|
|
631
631
|
path?: never;
|
|
632
632
|
cookie?: never;
|
|
633
633
|
};
|
|
634
|
-
get: operations["
|
|
635
|
-
put?: never;
|
|
636
|
-
post?: never;
|
|
637
|
-
delete?: never;
|
|
638
|
-
options?: never;
|
|
639
|
-
head?: never;
|
|
640
|
-
patch?: never;
|
|
641
|
-
trace?: never;
|
|
642
|
-
};
|
|
643
|
-
"/audit/errors": {
|
|
644
|
-
parameters: {
|
|
645
|
-
query?: never;
|
|
646
|
-
header?: never;
|
|
647
|
-
path?: never;
|
|
648
|
-
cookie?: never;
|
|
649
|
-
};
|
|
650
|
-
get: operations["AuditController_errors"];
|
|
651
|
-
put?: never;
|
|
652
|
-
post?: never;
|
|
653
|
-
delete?: never;
|
|
654
|
-
options?: never;
|
|
655
|
-
head?: never;
|
|
656
|
-
patch?: never;
|
|
657
|
-
trace?: never;
|
|
658
|
-
};
|
|
659
|
-
"/audit/errors/filter-schema": {
|
|
660
|
-
parameters: {
|
|
661
|
-
query?: never;
|
|
662
|
-
header?: never;
|
|
663
|
-
path?: never;
|
|
664
|
-
cookie?: never;
|
|
665
|
-
};
|
|
666
|
-
get: operations["AuditController_errorsFilterSchema"];
|
|
634
|
+
get: operations["AuditController_requestsFilterSchema"];
|
|
667
635
|
put?: never;
|
|
668
636
|
post?: never;
|
|
669
637
|
delete?: never;
|
|
@@ -1122,6 +1090,11 @@ export interface components {
|
|
|
1122
1090
|
permissionIds?: string[];
|
|
1123
1091
|
isActive?: boolean;
|
|
1124
1092
|
};
|
|
1093
|
+
/**
|
|
1094
|
+
* @description 0 = sucesso, 1 = erro, 2 = abortada
|
|
1095
|
+
* @enum {number}
|
|
1096
|
+
*/
|
|
1097
|
+
RequestOutcome: 0 | 1 | 2;
|
|
1125
1098
|
AuditUserRefResponse: {
|
|
1126
1099
|
/** Format: uuid */
|
|
1127
1100
|
id: string;
|
|
@@ -1129,58 +1102,43 @@ export interface components {
|
|
|
1129
1102
|
/** @example admin@admin.com */
|
|
1130
1103
|
email: string;
|
|
1131
1104
|
};
|
|
1105
|
+
/**
|
|
1106
|
+
* @description 0 = validação, 1 = autenticação, 2 = não encontrado, 3 = limite, 4 = banco, 5 = integração, 6 = interno
|
|
1107
|
+
* @enum {number}
|
|
1108
|
+
*/
|
|
1109
|
+
ErrorType: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
1132
1110
|
RequestLogResponse: {
|
|
1133
1111
|
/** Format: uuid */
|
|
1134
1112
|
id: string;
|
|
1135
|
-
/**
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
/** @example
|
|
1113
|
+
/**
|
|
1114
|
+
* @description 0 = sucesso, 1 = erro, 2 = abortada
|
|
1115
|
+
* @example 0
|
|
1116
|
+
*/
|
|
1117
|
+
outcome: components["schemas"]["RequestOutcome"];
|
|
1118
|
+
/** @example Login */
|
|
1119
|
+
action?: string | null;
|
|
1120
|
+
/** @example POST */
|
|
1141
1121
|
method?: string | null;
|
|
1122
|
+
/** @example /api/v1/users/9f3c… */
|
|
1142
1123
|
path?: string | null;
|
|
1124
|
+
/** @example /users/:id */
|
|
1125
|
+
route?: string | null;
|
|
1143
1126
|
/** @example 200 */
|
|
1144
1127
|
statusCode?: number | null;
|
|
1145
1128
|
/** @example 42 */
|
|
1146
1129
|
durationMs?: number | null;
|
|
1147
|
-
/** @example 10.0.0.1 */
|
|
1148
|
-
ipAddress?: string | null;
|
|
1149
|
-
/** Format: uuid */
|
|
1150
|
-
requestId?: string | null;
|
|
1151
|
-
metadata?: Record<string, never> | null;
|
|
1152
1130
|
userId?: string | null;
|
|
1153
1131
|
user?: components["schemas"]["AuditUserRefResponse"] | null;
|
|
1154
|
-
/**
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
updatedAt: string;
|
|
1158
|
-
/** Format: date-time */
|
|
1159
|
-
deletedAt?: string | null;
|
|
1160
|
-
};
|
|
1161
|
-
PaginatedRequestLogsResponse: {
|
|
1162
|
-
items: components["schemas"]["RequestLogResponse"][];
|
|
1163
|
-
meta: components["schemas"]["PaginationMetaResponse"];
|
|
1164
|
-
};
|
|
1165
|
-
/** @enum {string} */
|
|
1166
|
-
ErrorType: "VALIDATION" | "AUTH" | "NOT_FOUND" | "RATE_LIMIT" | "DATABASE" | "INTEGRATION" | "INTERNAL";
|
|
1167
|
-
ErrorLogResponse: {
|
|
1168
|
-
/** Format: uuid */
|
|
1169
|
-
id: string;
|
|
1170
|
-
type: components["schemas"]["ErrorType"];
|
|
1171
|
-
/** @example 500 */
|
|
1172
|
-
statusCode?: number | null;
|
|
1173
|
-
message: string;
|
|
1174
|
-
stack?: string | null;
|
|
1175
|
-
/** @example POST */
|
|
1176
|
-
method?: string | null;
|
|
1177
|
-
path?: string | null;
|
|
1178
|
-
/** @example 10.0.0.1 */
|
|
1179
|
-
ipAddress?: string | null;
|
|
1132
|
+
/** @description HMAC do IP — não é reversível ao endereço original */
|
|
1133
|
+
ipHash?: string | null;
|
|
1134
|
+
userAgent?: string | null;
|
|
1180
1135
|
/** Format: uuid */
|
|
1181
1136
|
requestId?: string | null;
|
|
1182
|
-
|
|
1183
|
-
|
|
1137
|
+
/** @description 0 = validação, 1 = autenticação, 2 = não encontrado, 3 = limite, 4 = banco, 5 = integração, 6 = interno */
|
|
1138
|
+
errorType?: components["schemas"]["ErrorType"] | null;
|
|
1139
|
+
errorCode?: string | null;
|
|
1140
|
+
errorMessage?: string | null;
|
|
1141
|
+
errorStack?: string | null;
|
|
1184
1142
|
/** Format: date-time */
|
|
1185
1143
|
createdAt: string;
|
|
1186
1144
|
/** Format: date-time */
|
|
@@ -1188,12 +1146,12 @@ export interface components {
|
|
|
1188
1146
|
/** Format: date-time */
|
|
1189
1147
|
deletedAt?: string | null;
|
|
1190
1148
|
};
|
|
1191
|
-
|
|
1192
|
-
items: components["schemas"]["
|
|
1149
|
+
PaginatedRequestLogsResponse: {
|
|
1150
|
+
items: components["schemas"]["RequestLogResponse"][];
|
|
1193
1151
|
meta: components["schemas"]["PaginationMetaResponse"];
|
|
1194
1152
|
};
|
|
1195
|
-
/** @enum {
|
|
1196
|
-
AuditDataAction:
|
|
1153
|
+
/** @enum {number} */
|
|
1154
|
+
AuditDataAction: 0 | 1 | 2;
|
|
1197
1155
|
AuditDataChangeResponse: {
|
|
1198
1156
|
/** Format: uuid */
|
|
1199
1157
|
id: string;
|
|
@@ -1218,8 +1176,8 @@ export interface components {
|
|
|
1218
1176
|
items: components["schemas"]["AuditDataChangeResponse"][];
|
|
1219
1177
|
meta: components["schemas"]["PaginationMetaResponse"];
|
|
1220
1178
|
};
|
|
1221
|
-
/** @enum {
|
|
1222
|
-
NotificationType:
|
|
1179
|
+
/** @enum {number} */
|
|
1180
|
+
NotificationType: 0 | 1 | 2 | 3;
|
|
1223
1181
|
UserNotificationResponse: {
|
|
1224
1182
|
/** Format: uuid */
|
|
1225
1183
|
id: string;
|
|
@@ -1245,8 +1203,8 @@ export interface components {
|
|
|
1245
1203
|
/** @example 3 */
|
|
1246
1204
|
unread: number;
|
|
1247
1205
|
};
|
|
1248
|
-
/** @enum {
|
|
1249
|
-
ScopeType:
|
|
1206
|
+
/** @enum {number} */
|
|
1207
|
+
ScopeType: 0 | 1 | 2;
|
|
1250
1208
|
NotificationTargetCommand: {
|
|
1251
1209
|
type: components["schemas"]["ScopeType"];
|
|
1252
1210
|
/** @description Id do usuário quando type=USER, nome do papel quando type=ROLE. Ignorado em ALL. */
|
|
@@ -2487,7 +2445,7 @@ export interface operations {
|
|
|
2487
2445
|
};
|
|
2488
2446
|
};
|
|
2489
2447
|
};
|
|
2490
|
-
|
|
2448
|
+
AuditController_requests: {
|
|
2491
2449
|
parameters: {
|
|
2492
2450
|
query?: {
|
|
2493
2451
|
page?: number;
|
|
@@ -2515,54 +2473,7 @@ export interface operations {
|
|
|
2515
2473
|
};
|
|
2516
2474
|
};
|
|
2517
2475
|
};
|
|
2518
|
-
|
|
2519
|
-
parameters: {
|
|
2520
|
-
query?: never;
|
|
2521
|
-
header?: never;
|
|
2522
|
-
path?: never;
|
|
2523
|
-
cookie?: never;
|
|
2524
|
-
};
|
|
2525
|
-
requestBody?: never;
|
|
2526
|
-
responses: {
|
|
2527
|
-
200: {
|
|
2528
|
-
headers: {
|
|
2529
|
-
[name: string]: unknown;
|
|
2530
|
-
};
|
|
2531
|
-
content: {
|
|
2532
|
-
"application/json": components["schemas"]["FilterFieldResponse"][];
|
|
2533
|
-
};
|
|
2534
|
-
};
|
|
2535
|
-
};
|
|
2536
|
-
};
|
|
2537
|
-
AuditController_errors: {
|
|
2538
|
-
parameters: {
|
|
2539
|
-
query?: {
|
|
2540
|
-
page?: number;
|
|
2541
|
-
limit?: number;
|
|
2542
|
-
search?: string;
|
|
2543
|
-
/** @description Coluna para ordenar (whitelist por endpoint) */
|
|
2544
|
-
sortBy?: string;
|
|
2545
|
-
sortDir?: "ASC" | "DESC";
|
|
2546
|
-
/** @description JSON, ex.: {"name":{"$SUBSTRING":"ana"}} */
|
|
2547
|
-
filter?: string;
|
|
2548
|
-
};
|
|
2549
|
-
header?: never;
|
|
2550
|
-
path?: never;
|
|
2551
|
-
cookie?: never;
|
|
2552
|
-
};
|
|
2553
|
-
requestBody?: never;
|
|
2554
|
-
responses: {
|
|
2555
|
-
200: {
|
|
2556
|
-
headers: {
|
|
2557
|
-
[name: string]: unknown;
|
|
2558
|
-
};
|
|
2559
|
-
content: {
|
|
2560
|
-
"application/json": components["schemas"]["PaginatedErrorLogsResponse"];
|
|
2561
|
-
};
|
|
2562
|
-
};
|
|
2563
|
-
};
|
|
2564
|
-
};
|
|
2565
|
-
AuditController_errorsFilterSchema: {
|
|
2476
|
+
AuditController_requestsFilterSchema: {
|
|
2566
2477
|
parameters: {
|
|
2567
2478
|
query?: never;
|
|
2568
2479
|
header?: never;
|
package/src/_utils/filter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { components } from "#core/_services/api/schema";
|
|
2
|
-
import {
|
|
2
|
+
import { fromIsoDay, toIsoDay } from "#core/_utils/calendar";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Tipos derivados do `openapi.json` (§5): o catálogo é servido pelo backend,
|
|
@@ -63,8 +63,20 @@ export const hasValue = (draft: FilterDraftValue | undefined): boolean => {
|
|
|
63
63
|
);
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Quantos filtros o botão "Filtros" anuncia.
|
|
68
|
+
*
|
|
69
|
+
* `ignore` são os campos que a barra da listagem controla: eles filtram de
|
|
70
|
+
* verdade, mas o contador não pode cobrá-los, senão a tela abre marcando 1 num
|
|
71
|
+
* painel onde não há nada marcado.
|
|
72
|
+
*/
|
|
73
|
+
export const activeFilterCount = (
|
|
74
|
+
values: FilterValues,
|
|
75
|
+
ignore: string[] = [],
|
|
76
|
+
): number =>
|
|
77
|
+
Object.entries(values).filter(
|
|
78
|
+
([field, draft]) => hasValue(draft) && !ignore.includes(field),
|
|
79
|
+
).length;
|
|
68
80
|
|
|
69
81
|
/**
|
|
70
82
|
* Ponta de um intervalo de datas em ISO **com o fuso de quem filtrou**.
|
|
@@ -280,34 +292,3 @@ const draftFrom = (
|
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
};
|
|
283
|
-
|
|
284
|
-
/** Resumo de um filtro ativo, para o chip. */
|
|
285
|
-
export const describeValue = (
|
|
286
|
-
field: FilterField,
|
|
287
|
-
draft: FilterDraftValue,
|
|
288
|
-
labels: { from: string; to: string; yes: string; no: string; locale: string },
|
|
289
|
-
): string => {
|
|
290
|
-
const show = (value: string): string =>
|
|
291
|
-
field.type === "DATE" || field.type === "DATETIME"
|
|
292
|
-
? formatDay(value, labels.locale)
|
|
293
|
-
: value;
|
|
294
|
-
|
|
295
|
-
if (field.type === "BOOLEAN") {
|
|
296
|
-
return draft.value === "true" ? labels.yes : labels.no;
|
|
297
|
-
}
|
|
298
|
-
if (draft.values?.length) {
|
|
299
|
-
const optionLabel = (value: string): string =>
|
|
300
|
-
field.options?.find((option) => option.value === value)?.label ?? value;
|
|
301
|
-
return draft.values.map(optionLabel).join(", ");
|
|
302
|
-
}
|
|
303
|
-
if (draft.from && draft.to) {
|
|
304
|
-
return `${show(draft.from)} – ${show(draft.to)}`;
|
|
305
|
-
}
|
|
306
|
-
if (draft.from) {
|
|
307
|
-
return `${labels.from} ${show(draft.from)}`;
|
|
308
|
-
}
|
|
309
|
-
if (draft.to) {
|
|
310
|
-
return `${labels.to} ${show(draft.to)}`;
|
|
311
|
-
}
|
|
312
|
-
return draft.value ?? "";
|
|
313
|
-
};
|
|
@@ -46,6 +46,30 @@ interface NavItem {
|
|
|
46
46
|
perm: string | null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Item de menu declarado pelo projeto.
|
|
51
|
+
*
|
|
52
|
+
* O core não conhece as telas de quem o instala (§14), então elas entram por
|
|
53
|
+
* aqui em vez de o projeto reescrever o shell.
|
|
54
|
+
*
|
|
55
|
+
* ```tsx
|
|
56
|
+
* <AppShell navItems={[{ label: "nav.tasks", href: "/tarefas", icon: ListTodo }]}>
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export interface AppNavItem {
|
|
60
|
+
/**
|
|
61
|
+
* Chave de i18n (`nav.tasks`) ou o texto pronto (`"Tarefas"`).
|
|
62
|
+
*
|
|
63
|
+
* Chave sem tradução cadastrada volta como veio, então as duas formas
|
|
64
|
+
* funcionam — o projeto só precisa do dicionário quando for bilíngue.
|
|
65
|
+
*/
|
|
66
|
+
label: string;
|
|
67
|
+
href: string;
|
|
68
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
69
|
+
/** Permissão exigida para o item aparecer. Ausente, aparece para todos. */
|
|
70
|
+
permission?: string | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
const DASHBOARD: NavItem = {
|
|
50
74
|
key: "nav.dashboard",
|
|
51
75
|
href: "/dashboard",
|
|
@@ -71,7 +95,13 @@ const ADMIN_CHILDREN: NavItem[] = [
|
|
|
71
95
|
},
|
|
72
96
|
];
|
|
73
97
|
|
|
74
|
-
export
|
|
98
|
+
export interface AppShellProps {
|
|
99
|
+
children: React.ReactNode;
|
|
100
|
+
/** Telas do projeto no menu, entre o dashboard e o grupo de administração. */
|
|
101
|
+
navItems?: AppNavItem[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function AppShell({ children, navItems }: AppShellProps): JSX.Element {
|
|
75
105
|
const { user, loading, logout, hasPermission } = useAuth();
|
|
76
106
|
const { t } = useI18n();
|
|
77
107
|
const brand = useBrand();
|
|
@@ -89,6 +119,22 @@ export function AppShell({ children }: { children: React.ReactNode }): JSX.Eleme
|
|
|
89
119
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
120
|
[user],
|
|
91
121
|
);
|
|
122
|
+
|
|
123
|
+
// Os itens do projeto passam pelo mesmo filtro de permissão dos do core: um
|
|
124
|
+
// menu que mostra o que o usuário não pode abrir só entrega 403.
|
|
125
|
+
const projectItems = useMemo<NavItem[]>(
|
|
126
|
+
() =>
|
|
127
|
+
(navItems ?? [])
|
|
128
|
+
.filter((item) => !item.permission || hasPermission(item.permission))
|
|
129
|
+
.map((item) => ({
|
|
130
|
+
key: item.label,
|
|
131
|
+
href: item.href,
|
|
132
|
+
icon: item.icon,
|
|
133
|
+
perm: item.permission ?? null,
|
|
134
|
+
})),
|
|
135
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
|
+
[user, navItems],
|
|
137
|
+
);
|
|
92
138
|
const adminActive = adminChildren.some((c) => pathname === c.href);
|
|
93
139
|
const [adminOpen, setAdminOpen] = useState(adminActive);
|
|
94
140
|
|
|
@@ -158,6 +204,10 @@ export function AppShell({ children }: { children: React.ReactNode }): JSX.Eleme
|
|
|
158
204
|
<div className="flex flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden p-3">
|
|
159
205
|
<NavLink item={DASHBOARD} mini={mini} />
|
|
160
206
|
|
|
207
|
+
{projectItems.map((item) => (
|
|
208
|
+
<NavLink key={item.href} item={item} mini={mini} />
|
|
209
|
+
))}
|
|
210
|
+
|
|
161
211
|
{adminChildren.length > 0 &&
|
|
162
212
|
(mini ? (
|
|
163
213
|
<>
|
|
@@ -7,6 +7,12 @@ import { useBrand } from "#core/contexts";
|
|
|
7
7
|
export interface BrandHeaderProps {
|
|
8
8
|
/** Título do passo ou da tela, exibido sob a marca. */
|
|
9
9
|
title: string;
|
|
10
|
+
/**
|
|
11
|
+
* Mostra logo e nome acima do título. Desligue quando a marca já estiver na
|
|
12
|
+
* tela por outro caminho — é o caso do `BrandPanel` ao lado do formulário,
|
|
13
|
+
* onde repeti-la no cartão seria dizer a mesma coisa duas vezes.
|
|
14
|
+
*/
|
|
15
|
+
showBrand?: boolean;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
/**
|
|
@@ -17,19 +23,28 @@ export interface BrandHeaderProps {
|
|
|
17
23
|
* ela está. O conteúdo vem do `BrandProvider` do projeto; sem ele, do padrão do
|
|
18
24
|
* core.
|
|
19
25
|
*/
|
|
20
|
-
export function BrandHeader({
|
|
26
|
+
export function BrandHeader({
|
|
27
|
+
title,
|
|
28
|
+
showBrand = true,
|
|
29
|
+
}: BrandHeaderProps): JSX.Element {
|
|
21
30
|
const brand = useBrand();
|
|
22
31
|
|
|
23
32
|
return (
|
|
24
33
|
<div className="mb-6 flex flex-col items-center gap-1">
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
{showBrand && (
|
|
35
|
+
<>
|
|
36
|
+
<div className="mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
|
37
|
+
{brand.logo}
|
|
38
|
+
</div>
|
|
39
|
+
<span className="text-lg font-extrabold">{brand.name}</span>
|
|
40
|
+
{brand.tagline && (
|
|
41
|
+
<p className="text-sm text-muted-foreground">{brand.tagline}</p>
|
|
42
|
+
)}
|
|
43
|
+
</>
|
|
31
44
|
)}
|
|
32
|
-
<h1 className="mt-2 text-xl font-semibold"
|
|
45
|
+
<h1 className={showBrand ? "mt-2 text-xl font-semibold" : "text-xl font-semibold"}>
|
|
46
|
+
{title}
|
|
47
|
+
</h1>
|
|
33
48
|
</div>
|
|
34
49
|
);
|
|
35
50
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
|
|
5
|
+
import { useBrand } from "#core/contexts";
|
|
6
|
+
import { cn } from "#core/lib/utils";
|
|
7
|
+
|
|
8
|
+
export interface BrandPanelProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A marca ocupando metade da tela de login, ao lado do formulário.
|
|
14
|
+
*
|
|
15
|
+
* É o irmão grande do `BrandHeader`: mesma fonte (o `BrandProvider` do
|
|
16
|
+
* projeto), espaço diferente. Serve a quem tem logo grande — no cabeçalho do
|
|
17
|
+
* cartão ela sairia espremida.
|
|
18
|
+
*
|
|
19
|
+
* O core não dimensiona a logo: ela é um `ReactNode` do projeto, que sabe se é
|
|
20
|
+
* um SVG, um `next/image` ou um ícone. Aqui ela só ganha o espaço.
|
|
21
|
+
*/
|
|
22
|
+
export function BrandPanel({ className }: BrandPanelProps): JSX.Element {
|
|
23
|
+
const brand = useBrand();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={cn(
|
|
28
|
+
"flex flex-1 flex-col items-center justify-center gap-4 bg-muted/30 p-8 text-center",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{brand.logo}
|
|
33
|
+
<span className="text-3xl font-extrabold">{brand.name}</span>
|
|
34
|
+
{brand.tagline && (
|
|
35
|
+
<p className="max-w-sm text-sm text-muted-foreground">
|
|
36
|
+
{brand.tagline}
|
|
37
|
+
</p>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Barrel da pasta — importe daqui de fora; dentro da pasta use o caminho direto.
|
|
2
2
|
export * from "./app-shell";
|
|
3
3
|
export * from "./brand-header";
|
|
4
|
+
export * from "./brand-panel";
|
|
4
5
|
export * from "./breadcrumbs";
|
|
5
6
|
export * from "./language-selector";
|
|
6
7
|
export * from "./password-field";
|
|
@@ -52,6 +52,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
52
52
|
<Comp
|
|
53
53
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
54
54
|
ref={ref}
|
|
55
|
+
// `<button>` sem `type` nasce `submit`: dentro de um form, um botão que
|
|
56
|
+
// só abre um modal acabava salvando o registro junto. Quem submete
|
|
57
|
+
// declara `type="submit"` — o spread abaixo deixa isso sobrescrever.
|
|
58
|
+
// Com `asChild` o filho pode não ser um `<button>`, então não força.
|
|
59
|
+
{...(asChild ? {} : { type: "button" as const })}
|
|
55
60
|
{...props}
|
|
56
61
|
/>
|
|
57
62
|
);
|
|
@@ -43,7 +43,9 @@ export function ConfirmDialog({
|
|
|
43
43
|
}: ConfirmDialogProps): React.JSX.Element {
|
|
44
44
|
return (
|
|
45
45
|
<Dialog open={open} onOpenChange={(o) => !o && onCancel()}>
|
|
46
|
-
|
|
46
|
+
{/* Sem ampliar: são duas linhas e dois botões, e o tamanho pequeno é
|
|
47
|
+
parte do recado — esticado só ganharia vazio. */}
|
|
48
|
+
<DialogContent className="max-w-sm" expandable={false}>
|
|
47
49
|
<DialogHeader>
|
|
48
50
|
<DialogTitle>{title}</DialogTitle>
|
|
49
51
|
{description && <DialogDescription>{description}</DialogDescription>}
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
ColumnVisibilityModal,
|
|
40
40
|
ColumnVisibilityOption,
|
|
41
41
|
} from "#core/components/ui/column-visibility-modal";
|
|
42
|
-
import { FilterChips } from "#core/components/ui/filter-chips";
|
|
43
42
|
import { FilterSheet } from "#core/components/ui/filter-sheet";
|
|
44
43
|
import { Spinner } from "#core/components/ui/spinner";
|
|
45
44
|
import {
|
|
@@ -94,6 +93,16 @@ interface DataTableProps<T extends RowData> {
|
|
|
94
93
|
* não aparece — listagem sem catálogo continua funcionando igual.
|
|
95
94
|
*/
|
|
96
95
|
filters?: FiltersState;
|
|
96
|
+
/**
|
|
97
|
+
* Campos que a `toolbar` já controla: somem do painel de filtros e não são
|
|
98
|
+
* contados no botão "Filtros", para a mesma escolha não existir em dois
|
|
99
|
+
* lugares.
|
|
100
|
+
*
|
|
101
|
+
* Some da interface, não do catálogo — é o catálogo que serializa o filtro,
|
|
102
|
+
* então o atalho da barra continua funcionando normalmente. O painel também
|
|
103
|
+
* não os desmarca ao aplicar nem ao limpar: não são dele.
|
|
104
|
+
*/
|
|
105
|
+
hiddenFilterFields?: string[];
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/** Extras que uma coluna pode declarar em `meta`. */
|
|
@@ -144,6 +153,7 @@ export function DataTable<T extends RowData>({
|
|
|
144
153
|
onSortingChange,
|
|
145
154
|
enableSorting = true,
|
|
146
155
|
filters,
|
|
156
|
+
hiddenFilterFields,
|
|
147
157
|
}: DataTableProps<T>): JSX.Element {
|
|
148
158
|
const { t } = useI18n();
|
|
149
159
|
const [colsOpen, setColsOpen] = useState(false);
|
|
@@ -220,7 +230,7 @@ export function DataTable<T extends RowData>({
|
|
|
220
230
|
? filters.tree && filters.filter
|
|
221
231
|
? countConditions(filters.tree)
|
|
222
232
|
: 0
|
|
223
|
-
: activeFilterCount(filters.values);
|
|
233
|
+
: activeFilterCount(filters.values, hiddenFilterFields);
|
|
224
234
|
|
|
225
235
|
return (
|
|
226
236
|
<div>
|
|
@@ -257,8 +267,6 @@ export function DataTable<T extends RowData>({
|
|
|
257
267
|
</div>
|
|
258
268
|
</div>
|
|
259
269
|
|
|
260
|
-
{filters && <FilterChips filters={filters} />}
|
|
261
|
-
|
|
262
270
|
<Card>
|
|
263
271
|
<Table>
|
|
264
272
|
<TableHeader>
|
|
@@ -394,6 +402,7 @@ export function DataTable<T extends RowData>({
|
|
|
394
402
|
open={filtersOpen}
|
|
395
403
|
onOpenChange={setFiltersOpen}
|
|
396
404
|
filters={filters}
|
|
405
|
+
hiddenFields={hiddenFilterFields}
|
|
397
406
|
/>
|
|
398
407
|
)}
|
|
399
408
|
</div>
|