rl-core-front 0.16.4 → 0.16.5
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/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/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
|
);
|
|
@@ -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
|
),
|
|
@@ -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
|
);
|