rl-core-front 0.16.4 → 0.16.6
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/ui/button.tsx +35 -2
- package/src/components/ui/date-picker.tsx +44 -3
- package/src/components/ui/hover-tip.tsx +79 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/money-input.tsx +25 -11
- package/src/features/rbac/panels/catalog-panel.tsx +22 -15
- package/src/features/rbac/panels/groups-panel.tsx +22 -15
- package/src/features/rbac/panels/permissions-panel.tsx +22 -15
- package/src/features/rbac/panels/roles-panel.tsx +22 -15
- package/src/features/users/users-screen.tsx +93 -82
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { Loader2 } from "lucide-react";
|
|
5
6
|
import * as React from "react";
|
|
6
7
|
|
|
7
8
|
import { cn } from "#core/lib/utils";
|
|
@@ -43,22 +44,54 @@ export interface ButtonProps
|
|
|
43
44
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
44
45
|
VariantProps<typeof buttonVariants> {
|
|
45
46
|
asChild?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* A ação está em curso: mostra o giro e recusa clique.
|
|
49
|
+
*
|
|
50
|
+
* As duas coisas juntas de propósito. Salvar sem sinal nenhum é o que faz
|
|
51
|
+
* clicar de novo, e clicar de novo é o que duplica o lançamento — desabilitar
|
|
52
|
+
* sem mostrar por quê parece que o botão quebrou. Quem passa `loading` não
|
|
53
|
+
* precisa lembrar de passar `disabled` também.
|
|
54
|
+
*
|
|
55
|
+
* Ignorado com `asChild`: ali o filho é que decide o que desenha.
|
|
56
|
+
*/
|
|
57
|
+
loading?: boolean;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
49
|
-
(
|
|
61
|
+
(
|
|
62
|
+
{ className, variant, size, asChild = false, loading = false, children, ...props },
|
|
63
|
+
ref,
|
|
64
|
+
) => {
|
|
50
65
|
const Comp = asChild ? Slot : "button";
|
|
66
|
+
|
|
67
|
+
// O giro entra ANTES do conteúdo, no lugar onde o ícone do botão já ficava:
|
|
68
|
+
// aparecendo depois, o texto pula para a esquerda quando ele some.
|
|
69
|
+
const conteudo =
|
|
70
|
+
loading && !asChild ? (
|
|
71
|
+
<>
|
|
72
|
+
<Loader2 className="animate-spin" aria-hidden />
|
|
73
|
+
{children}
|
|
74
|
+
</>
|
|
75
|
+
) : (
|
|
76
|
+
children
|
|
77
|
+
);
|
|
78
|
+
|
|
51
79
|
return (
|
|
52
80
|
<Comp
|
|
53
81
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
54
82
|
ref={ref}
|
|
83
|
+
{...(asChild ? {} : { disabled: loading || props.disabled })}
|
|
84
|
+
// Quem usa leitor de tela não vê o giro: é o `aria-busy` que conta.
|
|
85
|
+
{...(loading && !asChild ? { "aria-busy": true } : {})}
|
|
55
86
|
// `<button>` sem `type` nasce `submit`: dentro de um form, um botão que
|
|
56
87
|
// só abre um modal acabava salvando o registro junto. Quem submete
|
|
57
88
|
// declara `type="submit"` — o spread abaixo deixa isso sobrescrever.
|
|
58
89
|
// Com `asChild` o filho pode não ser um `<button>`, então não força.
|
|
59
90
|
{...(asChild ? {} : { type: "button" as const })}
|
|
60
91
|
{...props}
|
|
61
|
-
|
|
92
|
+
>
|
|
93
|
+
{conteudo}
|
|
94
|
+
</Comp>
|
|
62
95
|
);
|
|
63
96
|
},
|
|
64
97
|
);
|
|
@@ -38,6 +38,18 @@ export interface DatePickerProps {
|
|
|
38
38
|
/** Descreve o campo para quem usa leitor de tela. */
|
|
39
39
|
label?: string;
|
|
40
40
|
placeholder?: string;
|
|
41
|
+
/**
|
|
42
|
+
* O primeiro dia escolhível (`"yyyy-MM-dd"`) — os anteriores ficam apagados
|
|
43
|
+
* no calendário e o digitado é recusado.
|
|
44
|
+
*
|
|
45
|
+
* Existe porque avisar depois não serve para data: o vencimento anterior à
|
|
46
|
+
* compra e o pagamento anterior ao lançamento são erros que a pessoa comete
|
|
47
|
+
* sem perceber, e descobrir no botão Salvar é refazer o caminho. Aqui o dia
|
|
48
|
+
* simplesmente não se deixa escolher.
|
|
49
|
+
*/
|
|
50
|
+
minDay?: string;
|
|
51
|
+
/** O último dia escolhível, pela mesma razão. */
|
|
52
|
+
maxDay?: string;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
/**
|
|
@@ -69,6 +81,8 @@ export function DatePicker({
|
|
|
69
81
|
disabled,
|
|
70
82
|
label,
|
|
71
83
|
placeholder,
|
|
84
|
+
minDay,
|
|
85
|
+
maxDay,
|
|
72
86
|
}: DatePickerProps): JSX.Element {
|
|
73
87
|
const { t, locale } = useI18n();
|
|
74
88
|
const [open, setOpen] = useState(false);
|
|
@@ -104,8 +118,24 @@ export function DatePicker({
|
|
|
104
118
|
caret.current = null;
|
|
105
119
|
});
|
|
106
120
|
|
|
121
|
+
/**
|
|
122
|
+
* O dia está dentro da faixa permitida?
|
|
123
|
+
*
|
|
124
|
+
* Comparação de texto ISO, e não de `Date`: `2026-09-03 < 2026-09-08` é
|
|
125
|
+
* verdade em ordem alfabética porque o formato é fixo, e assim não há
|
|
126
|
+
* conversão de fuso no meio para transformar o dia 1º no dia 30.
|
|
127
|
+
*/
|
|
128
|
+
const dayAllowed = (iso: string): boolean =>
|
|
129
|
+
(!minDay || iso >= minDay) && (!maxDay || iso <= maxDay);
|
|
130
|
+
|
|
107
131
|
const pickDay = (day: Date): void => {
|
|
108
|
-
|
|
132
|
+
const iso = toIsoDay(day);
|
|
133
|
+
|
|
134
|
+
if (!dayAllowed(iso)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onChange(iso);
|
|
109
139
|
setTyped(null);
|
|
110
140
|
setOpen(false);
|
|
111
141
|
};
|
|
@@ -129,7 +159,9 @@ export function DatePicker({
|
|
|
129
159
|
(selected ?? today()).getFullYear(),
|
|
130
160
|
);
|
|
131
161
|
|
|
132
|
-
|
|
162
|
+
// Digitar é o outro caminho para o mesmo campo: sem esta guarda, o dia que
|
|
163
|
+
// o calendário recusa entraria pelo teclado.
|
|
164
|
+
if (iso && dayAllowed(iso)) {
|
|
133
165
|
onChange(iso);
|
|
134
166
|
setMonth(fromIsoDay(iso) ?? month);
|
|
135
167
|
}
|
|
@@ -180,6 +212,11 @@ export function DatePicker({
|
|
|
180
212
|
};
|
|
181
213
|
|
|
182
214
|
const dayClass = (day: Date): string => {
|
|
215
|
+
if (!dayAllowed(toIsoDay(day))) {
|
|
216
|
+
// Continua desenhado, e não escondido: o calendário sem os dias 1 a 7
|
|
217
|
+
// pareceria quebrado, e é o cinza que diz "existe, mas não aqui".
|
|
218
|
+
return "text-muted-foreground/30 line-through";
|
|
219
|
+
}
|
|
183
220
|
if (selected && isSameDay(day, selected)) {
|
|
184
221
|
return "bg-primary text-primary-foreground font-semibold";
|
|
185
222
|
}
|
|
@@ -285,9 +322,13 @@ export function DatePicker({
|
|
|
285
322
|
<button
|
|
286
323
|
key={day.toISOString()}
|
|
287
324
|
type="button"
|
|
325
|
+
disabled={!dayAllowed(toIsoDay(day))}
|
|
288
326
|
onClick={() => pickDay(day)}
|
|
289
327
|
className={cn(
|
|
290
|
-
"flex h-8 w-8 items-center justify-center rounded-full text-sm transition-colors
|
|
328
|
+
"flex h-8 w-8 items-center justify-center rounded-full text-sm transition-colors",
|
|
329
|
+
dayAllowed(toIsoDay(day))
|
|
330
|
+
? "hover:bg-accent"
|
|
331
|
+
: "cursor-not-allowed hover:bg-transparent",
|
|
291
332
|
dayClass(day),
|
|
292
333
|
)}
|
|
293
334
|
>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FocusEvent, JSX, MouseEvent, ReactNode } from "react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
|
|
7
|
+
export interface HoverTipProps {
|
|
8
|
+
/** O que o tooltip diz. Vazio não desenha nada. */
|
|
9
|
+
label: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Onde o rótulo aparece na tela, em coordenada de viewport. */
|
|
14
|
+
interface TipPosition {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A folga entre o gatilho e o rótulo. */
|
|
20
|
+
const GAP = 6;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* O rótulo que aparece **na hora** ao passar o mouse.
|
|
24
|
+
*
|
|
25
|
+
* Existe pelo mesmo motivo do `HoverLabel` dos gráficos: o `title` do HTML só
|
|
26
|
+
* aparece depois de cerca de um segundo parado, e um segundo é tempo suficiente
|
|
27
|
+
* para quem passou o mouse concluir que não há nada ali e seguir em frente.
|
|
28
|
+
* Numa coluna de cinco ícones, isso é a diferença entre descobrir o que cada um
|
|
29
|
+
* faz e clicar para descobrir.
|
|
30
|
+
*
|
|
31
|
+
* Abre **embaixo**, e sai do documento por um portal com posição fixa. As duas
|
|
32
|
+
* decisões são a mesma: o `Table` do core envolve a tabela num `overflow-x-auto`
|
|
33
|
+
* e, quando um eixo rola, o navegador recorta o outro também — dentro da célula,
|
|
34
|
+
* o rótulo da última linha era cortado pela borda da tabela. Abrir à esquerda,
|
|
35
|
+
* como já se tentou, cabia na caixa mas cobria o conteúdo da própria linha: na
|
|
36
|
+
* lista de receitas, o "Recebido" do botão tapava o "Pendente" da situação, que
|
|
37
|
+
* é justamente o dado que se confere antes de clicar.
|
|
38
|
+
*
|
|
39
|
+
* O estado mora aqui dentro e guarda só a coordenada. Ele re-renderiza este
|
|
40
|
+
* componente e o rótulo — não a linha da tabela, que entra por `children` e
|
|
41
|
+
* chega pronta de fora.
|
|
42
|
+
*/
|
|
43
|
+
export const HoverTip = ({ label, children }: HoverTipProps): JSX.Element => {
|
|
44
|
+
const [position, setPosition] = useState<TipPosition | null>(null);
|
|
45
|
+
|
|
46
|
+
const show = (event: MouseEvent<HTMLElement> | FocusEvent<HTMLElement>): void => {
|
|
47
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
48
|
+
|
|
49
|
+
setPosition({ x: box.left + box.width / 2, y: box.bottom + GAP });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const hide = (): void => setPosition(null);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
className="relative inline-flex"
|
|
57
|
+
onMouseEnter={show}
|
|
58
|
+
onMouseLeave={hide}
|
|
59
|
+
// Chegando por teclado o rótulo também aparece: quem navega com Tab
|
|
60
|
+
// precisa do mesmo texto que o mouse mostra.
|
|
61
|
+
onFocus={show}
|
|
62
|
+
onBlur={hide}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
{label &&
|
|
66
|
+
position &&
|
|
67
|
+
createPortal(
|
|
68
|
+
<span
|
|
69
|
+
role="tooltip"
|
|
70
|
+
style={{ left: position.x, top: position.y }}
|
|
71
|
+
className="pointer-events-none fixed z-100 max-w-[90vw] -translate-x-1/2 whitespace-nowrap rounded-md border border-border bg-popover px-2 py-0.5 text-[11px] font-medium text-popover-foreground shadow-lg"
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</span>,
|
|
75
|
+
document.body,
|
|
76
|
+
)}
|
|
77
|
+
</span>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
@@ -20,6 +20,7 @@ export * from "./filter-field";
|
|
|
20
20
|
export * from "./filter-group";
|
|
21
21
|
export * from "./filter-rule";
|
|
22
22
|
export * from "./filter-sheet";
|
|
23
|
+
export * from "./hover-tip";
|
|
23
24
|
export * from "./image-upload";
|
|
24
25
|
export * from "./input";
|
|
25
26
|
export * from "./job-progress";
|
|
@@ -37,6 +37,15 @@ export interface MoneyInputProps {
|
|
|
37
37
|
id?: string;
|
|
38
38
|
placeholder?: string;
|
|
39
39
|
disabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Foca o campo ao montar.
|
|
42
|
+
*
|
|
43
|
+
* Existe porque num modal cujo primeiro campo é o valor — a meta do mês, o
|
|
44
|
+
* ajuste de saldo — o `DialogContent` entrega o foco ao botão de fechar do
|
|
45
|
+
* cabeçalho, e quem abriu para digitar um número precisa de um Tab antes de
|
|
46
|
+
* começar.
|
|
47
|
+
*/
|
|
48
|
+
autoFocus?: boolean;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
/**
|
|
@@ -113,6 +122,7 @@ export const MoneyInput = ({
|
|
|
113
122
|
id,
|
|
114
123
|
placeholder,
|
|
115
124
|
disabled = false,
|
|
125
|
+
autoFocus = false,
|
|
116
126
|
}: MoneyInputProps): JSX.Element => {
|
|
117
127
|
const { t } = useI18n();
|
|
118
128
|
const [open, setOpen] = useState(false);
|
|
@@ -218,6 +228,7 @@ export const MoneyInput = ({
|
|
|
218
228
|
onBlur={onBlur}
|
|
219
229
|
placeholder={placeholder}
|
|
220
230
|
disabled={disabled}
|
|
231
|
+
autoFocus={autoFocus}
|
|
221
232
|
prefix="R$"
|
|
222
233
|
inputMode="decimal"
|
|
223
234
|
className="pr-11"
|
|
@@ -254,20 +265,20 @@ export const MoneyInput = ({
|
|
|
254
265
|
tabIndex={-1}
|
|
255
266
|
className="w-72"
|
|
256
267
|
>
|
|
257
|
-
<div className="flex min-h-
|
|
268
|
+
<div className="flex min-h-14 flex-col justify-between gap-1 rounded-md border border-border bg-muted/50 px-3 py-1.5 text-right">
|
|
258
269
|
<span className="min-h-5 break-all font-mono text-xs text-muted-foreground">
|
|
259
270
|
{toDisplayExpression(expression)}
|
|
260
271
|
</span>
|
|
261
272
|
{outcome.message ? (
|
|
262
273
|
<span className="break-words text-xs text-destructive">{outcome.message}</span>
|
|
263
274
|
) : (
|
|
264
|
-
<span className="break-all font-mono text-
|
|
275
|
+
<span className="break-all font-mono text-base font-medium tabular-nums">
|
|
265
276
|
{formatMoneyBR(outcome.value ?? 0)}
|
|
266
277
|
</span>
|
|
267
278
|
)}
|
|
268
279
|
</div>
|
|
269
280
|
|
|
270
|
-
<div className="mt-2 grid grid-cols-4 gap-1
|
|
281
|
+
<div className="mt-2 grid grid-cols-4 gap-1">
|
|
271
282
|
{KEYS.map((key, index) => (
|
|
272
283
|
<Button
|
|
273
284
|
key={key.label ?? key.action ?? index}
|
|
@@ -276,7 +287,7 @@ export const MoneyInput = ({
|
|
|
276
287
|
aria-label={key.labelKey ? t(key.labelKey) : undefined}
|
|
277
288
|
onClick={() => press(key)}
|
|
278
289
|
className={cn(
|
|
279
|
-
"h-
|
|
290
|
+
"h-8 px-0 font-mono text-sm",
|
|
280
291
|
key.role === "operator" && "text-primary",
|
|
281
292
|
key.role === "aux" && "text-muted-foreground",
|
|
282
293
|
)}
|
|
@@ -286,28 +297,31 @@ export const MoneyInput = ({
|
|
|
286
297
|
))}
|
|
287
298
|
</div>
|
|
288
299
|
|
|
289
|
-
|
|
300
|
+
{/*
|
|
301
|
+
A dica saiu daqui e virou o `title` do botão: num notebook de 768px o
|
|
302
|
+
painel inteiro não cabe na altura disponível, e o `PopoverContent`
|
|
303
|
+
passa a rolar — o teclado de uma calculadora com barra de rolagem é
|
|
304
|
+
pior que uma dica a menos.
|
|
305
|
+
*/}
|
|
306
|
+
<div className="mt-2 flex gap-1">
|
|
290
307
|
<Button
|
|
291
308
|
type="button"
|
|
292
309
|
variant="outline"
|
|
293
|
-
className="h-
|
|
310
|
+
className="h-8 flex-1"
|
|
294
311
|
onClick={() => setOpen(false)}
|
|
295
312
|
>
|
|
296
313
|
{t("calculator.cancel")}
|
|
297
314
|
</Button>
|
|
298
315
|
<Button
|
|
299
316
|
type="button"
|
|
300
|
-
className="h-
|
|
317
|
+
className="h-8 flex-1"
|
|
318
|
+
title={t("calculator.hint")}
|
|
301
319
|
disabled={outcome.value === null}
|
|
302
320
|
onClick={apply}
|
|
303
321
|
>
|
|
304
322
|
{t("calculator.apply")}
|
|
305
323
|
</Button>
|
|
306
324
|
</div>
|
|
307
|
-
|
|
308
|
-
<p className="mt-2 text-center text-[10px] text-muted-foreground">
|
|
309
|
-
{t("calculator.hint")}
|
|
310
|
-
</p>
|
|
311
325
|
</PopoverContent>
|
|
312
326
|
</Popover>
|
|
313
327
|
);
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
DialogHeader,
|
|
18
18
|
DialogTitle,
|
|
19
19
|
Field,
|
|
20
|
+
HoverTip,
|
|
20
21
|
Input,
|
|
21
22
|
RowActions,
|
|
22
23
|
} from "#core/components/ui";
|
|
@@ -157,23 +158,29 @@ export function CatalogPanel({
|
|
|
157
158
|
cell: ({ row }) => (
|
|
158
159
|
<RowActions>
|
|
159
160
|
{canUpdate && (
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
<HoverTip label={t("common.edit")}>
|
|
162
|
+
<Button
|
|
163
|
+
aria-label={t("common.edit")}
|
|
164
|
+
variant="ghost"
|
|
165
|
+
size="iconSm"
|
|
166
|
+
onClick={() => openEdit(row.original)}
|
|
167
|
+
>
|
|
168
|
+
<Pencil className="h-4 w-4" />
|
|
169
|
+
</Button>
|
|
170
|
+
</HoverTip>
|
|
167
171
|
)}
|
|
168
172
|
{canDelete && (
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
<HoverTip label={t("common.delete")}>
|
|
174
|
+
<Button
|
|
175
|
+
aria-label={t("common.delete")}
|
|
176
|
+
variant="ghost"
|
|
177
|
+
size="iconSm"
|
|
178
|
+
className="text-destructive"
|
|
179
|
+
onClick={() => removeItem(row.original)}
|
|
180
|
+
>
|
|
181
|
+
<Trash2 className="h-4 w-4" />
|
|
182
|
+
</Button>
|
|
183
|
+
</HoverTip>
|
|
177
184
|
)}
|
|
178
185
|
</RowActions>
|
|
179
186
|
),
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
DialogHeader,
|
|
19
19
|
DialogTitle,
|
|
20
20
|
Field,
|
|
21
|
+
HoverTip,
|
|
21
22
|
Input,
|
|
22
23
|
Label,
|
|
23
24
|
RowActions,
|
|
@@ -188,23 +189,29 @@ export function GroupsPanel(): JSX.Element {
|
|
|
188
189
|
cell: ({ row }) => (
|
|
189
190
|
<RowActions>
|
|
190
191
|
{hasPermission("groups:update:any") && (
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
<HoverTip label={t("common.edit")}>
|
|
193
|
+
<Button
|
|
194
|
+
aria-label={t("common.edit")}
|
|
195
|
+
variant="ghost"
|
|
196
|
+
size="iconSm"
|
|
197
|
+
onClick={() => openEdit(row.original)}
|
|
198
|
+
>
|
|
199
|
+
<Pencil className="h-4 w-4" />
|
|
200
|
+
</Button>
|
|
201
|
+
</HoverTip>
|
|
198
202
|
)}
|
|
199
203
|
{hasPermission("groups:delete:any") && (
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
<HoverTip label={t("common.delete")}>
|
|
205
|
+
<Button
|
|
206
|
+
aria-label={t("common.delete")}
|
|
207
|
+
variant="ghost"
|
|
208
|
+
size="iconSm"
|
|
209
|
+
className="text-destructive"
|
|
210
|
+
onClick={() => remove(row.original)}
|
|
211
|
+
>
|
|
212
|
+
<Trash2 className="h-4 w-4" />
|
|
213
|
+
</Button>
|
|
214
|
+
</HoverTip>
|
|
208
215
|
)}
|
|
209
216
|
</RowActions>
|
|
210
217
|
),
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
DialogHeader,
|
|
17
17
|
DialogTitle,
|
|
18
18
|
Field,
|
|
19
|
+
HoverTip,
|
|
19
20
|
Input,
|
|
20
21
|
RowActions,
|
|
21
22
|
} from "#core/components/ui";
|
|
@@ -118,23 +119,29 @@ export function PermissionsPanel(): JSX.Element {
|
|
|
118
119
|
cell: ({ row }) => (
|
|
119
120
|
<RowActions>
|
|
120
121
|
{hasPermission("permissions:update:any") && (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
<HoverTip label={t("common.edit")}>
|
|
123
|
+
<Button
|
|
124
|
+
aria-label={t("common.edit")}
|
|
125
|
+
variant="ghost"
|
|
126
|
+
size="iconSm"
|
|
127
|
+
onClick={() => openEdit(row.original)}
|
|
128
|
+
>
|
|
129
|
+
<Pencil className="h-4 w-4" />
|
|
130
|
+
</Button>
|
|
131
|
+
</HoverTip>
|
|
128
132
|
)}
|
|
129
133
|
{hasPermission("permissions:delete:any") && (
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
<HoverTip label={t("common.delete")}>
|
|
135
|
+
<Button
|
|
136
|
+
aria-label={t("common.delete")}
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="iconSm"
|
|
139
|
+
className="text-destructive"
|
|
140
|
+
onClick={() => remove(row.original)}
|
|
141
|
+
>
|
|
142
|
+
<Trash2 className="h-4 w-4" />
|
|
143
|
+
</Button>
|
|
144
|
+
</HoverTip>
|
|
138
145
|
)}
|
|
139
146
|
</RowActions>
|
|
140
147
|
),
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
DialogHeader,
|
|
20
20
|
DialogTitle,
|
|
21
21
|
Field,
|
|
22
|
+
HoverTip,
|
|
22
23
|
Input,
|
|
23
24
|
Label,
|
|
24
25
|
} from "#core/components/ui";
|
|
@@ -151,23 +152,29 @@ export function RolesPanel(): JSX.Element {
|
|
|
151
152
|
{canManage && (
|
|
152
153
|
<div className="flex">
|
|
153
154
|
{hasPermission("roles:update:any") && (
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
155
|
+
<HoverTip label={t("common.edit")}>
|
|
156
|
+
<Button
|
|
157
|
+
aria-label={t("common.edit")}
|
|
158
|
+
variant="ghost"
|
|
159
|
+
size="icon"
|
|
160
|
+
onClick={() => openEdit(role)}
|
|
161
|
+
>
|
|
162
|
+
<Pencil className="h-4 w-4" />
|
|
163
|
+
</Button>
|
|
164
|
+
</HoverTip>
|
|
161
165
|
)}
|
|
162
166
|
{hasPermission("roles:delete:any") && (
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
<HoverTip label={t("common.delete")}>
|
|
168
|
+
<Button
|
|
169
|
+
aria-label={t("common.delete")}
|
|
170
|
+
variant="ghost"
|
|
171
|
+
size="icon"
|
|
172
|
+
className="text-destructive"
|
|
173
|
+
onClick={() => remove(role)}
|
|
174
|
+
>
|
|
175
|
+
<Trash2 className="h-4 w-4" />
|
|
176
|
+
</Button>
|
|
177
|
+
</HoverTip>
|
|
171
178
|
)}
|
|
172
179
|
</div>
|
|
173
180
|
)}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
CreateButton,
|
|
27
27
|
DataTable,
|
|
28
28
|
DataTableFeatures,
|
|
29
|
+
HoverTip,
|
|
29
30
|
RowActions,
|
|
30
31
|
} from "#core/components/ui";
|
|
31
32
|
import { useAuth, useI18n } from "#core/contexts";
|
|
@@ -152,74 +153,82 @@ export function UsersScreen(): JSX.Element {
|
|
|
152
153
|
return (
|
|
153
154
|
<RowActions>
|
|
154
155
|
{canUpdate && (
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
156
|
+
<HoverTip label={t("common.edit")}>
|
|
157
|
+
<Button
|
|
158
|
+
variant="ghost"
|
|
159
|
+
size="iconSm"
|
|
160
|
+
aria-label={t("common.edit")}
|
|
161
|
+
onClick={() => openEdit(item)}
|
|
162
|
+
>
|
|
163
|
+
<Pencil className="h-4 w-4" />
|
|
164
|
+
</Button>
|
|
165
|
+
</HoverTip>
|
|
163
166
|
)}
|
|
164
167
|
{canInvite && (
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
168
|
+
<HoverTip label={t("users.sendFirstAccessLink")}>
|
|
169
|
+
<Button
|
|
170
|
+
variant="ghost"
|
|
171
|
+
size="iconSm"
|
|
172
|
+
aria-label={t("users.sendFirstAccessLink")}
|
|
173
|
+
onClick={() => {
|
|
174
|
+
void confirm({
|
|
175
|
+
title: t("users.sendFirstAccessLink"),
|
|
176
|
+
description: item.email,
|
|
177
|
+
confirmLabel: t("common.send"),
|
|
178
|
+
}).then((ok) => {
|
|
179
|
+
if (ok) {
|
|
180
|
+
void u.sendFirstAccessLink(item.id);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<MailPlus className="h-4 w-4" />
|
|
186
|
+
</Button>
|
|
187
|
+
</HoverTip>
|
|
183
188
|
)}
|
|
184
189
|
{canReset && (
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
190
|
+
<HoverTip label={t("users.sendPasswordResetLink")}>
|
|
191
|
+
<Button
|
|
192
|
+
variant="ghost"
|
|
193
|
+
size="iconSm"
|
|
194
|
+
aria-label={t("users.sendPasswordResetLink")}
|
|
195
|
+
onClick={() => {
|
|
196
|
+
void confirm({
|
|
197
|
+
title: t("users.sendPasswordResetLink"),
|
|
198
|
+
description: item.email,
|
|
199
|
+
confirmLabel: t("common.send"),
|
|
200
|
+
}).then((ok) => {
|
|
201
|
+
if (ok) {
|
|
202
|
+
void u.sendPasswordResetLink(item.id);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<KeyRound className="h-4 w-4" />
|
|
208
|
+
</Button>
|
|
209
|
+
</HoverTip>
|
|
203
210
|
)}
|
|
204
211
|
{canReset2fa && item.twoFactorEnabled && (
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
212
|
+
<HoverTip label={t("users.resetTwoFactor")}>
|
|
213
|
+
<Button
|
|
214
|
+
variant="ghost"
|
|
215
|
+
size="iconSm"
|
|
216
|
+
aria-label={t("users.resetTwoFactor")}
|
|
217
|
+
onClick={() => {
|
|
218
|
+
void confirm({
|
|
219
|
+
title: t("users.resetTwoFactor"),
|
|
220
|
+
description: item.name,
|
|
221
|
+
destructive: true,
|
|
222
|
+
}).then((ok) => {
|
|
223
|
+
if (ok) {
|
|
224
|
+
void u.resetTwoFactor(item.id);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<ShieldOff className="h-4 w-4" />
|
|
230
|
+
</Button>
|
|
231
|
+
</HoverTip>
|
|
223
232
|
)}
|
|
224
233
|
{canDeactivate && (
|
|
225
234
|
<Button
|
|
@@ -238,26 +247,28 @@ export function UsersScreen(): JSX.Element {
|
|
|
238
247
|
</Button>
|
|
239
248
|
)}
|
|
240
249
|
{canDelete && (
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
250
|
+
<HoverTip label={t("common.delete")}>
|
|
251
|
+
<Button
|
|
252
|
+
variant="ghost"
|
|
253
|
+
size="iconSm"
|
|
254
|
+
className="text-destructive"
|
|
255
|
+
aria-label={t("common.delete")}
|
|
256
|
+
onClick={() => {
|
|
257
|
+
void confirm({
|
|
258
|
+
title: t("common.delete"),
|
|
259
|
+
description: item.name,
|
|
260
|
+
confirmLabel: t("common.delete"),
|
|
261
|
+
destructive: true,
|
|
262
|
+
}).then((ok) => {
|
|
263
|
+
if (ok) {
|
|
264
|
+
void u.remove(item.id);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
<Trash2 className="h-4 w-4" />
|
|
270
|
+
</Button>
|
|
271
|
+
</HoverTip>
|
|
261
272
|
)}
|
|
262
273
|
</RowActions>
|
|
263
274
|
);
|