feat(design-preview): пометки строк «Не сохранено» и «Будет удалено», массовое удаление с подтверждением
Строки с накопленными правками подсвечиваются и получают плашку (getRowStatus из DataGrid v9), удаление применяется при «Сохранить», в каталоге пользователей добавлено «Удалить…» в панели массовых действий. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
41f5669812
commit
e02cfad0b1
7 files changed
+102
-16
No files matched your search
@@ -398,3 +398,12 @@ export function Segmented({
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Плашка строки с накопленной правкой: «Не сохранено» или «Будет удалено». */
|
||||||
|
export function PendingChip({ mark }: { mark: 'new' | 'dirty' | 'deleted' }) {
|
||||||
|
return (
|
||||||
|
<Badge variant={mark === 'deleted' ? 'destructive-light' : 'warning-light'} size="sm" className="whitespace-nowrap">
|
||||||
|
{mark === 'deleted' ? 'Будет удалено' : 'Не сохранено'}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -92,7 +92,8 @@ function SettingRow({ title, description, children }: { title: string; descripti
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AppLinksPage() {
|
export function AppLinksPage() {
|
||||||
const { addChange } = usePreviewStore()
|
const { addChange, marksFor } = usePreviewStore()
|
||||||
|
const marks = marksFor('app-links')
|
||||||
const [rows, setRows] = useState<LinkRow[]>(INITIAL)
|
const [rows, setRows] = useState<LinkRow[]>(INITIAL)
|
||||||
const rowsRef = useRef(rows)
|
const rowsRef = useRef(rows)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -113,6 +114,8 @@ export function AppLinksPage() {
|
|||||||
kind: 'chg',
|
kind: 'chg',
|
||||||
title: enabled ? 'Показать в меню' : 'Скрыть из меню',
|
title: enabled ? 'Показать в меню' : 'Скрыть из меню',
|
||||||
lines: names.map((n) => `${enabled ? '+' : '−'} ${n}`),
|
lines: names.map((n) => `${enabled ? '+' : '−'} ${n}`),
|
||||||
|
resource: 'app-links',
|
||||||
|
marks: Object.fromEntries(ids.map((i) => [i, 'dirty' as const])),
|
||||||
revert: () => setRows(before),
|
revert: () => setRows(before),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -134,6 +137,8 @@ export function AppLinksPage() {
|
|||||||
kind: 'chg',
|
kind: 'chg',
|
||||||
title: 'Порядок изменён',
|
title: 'Порядок изменён',
|
||||||
lines: [` ${item!.name}`, `− позиция ${from + 1}`, `+ позиция ${to + 1}`],
|
lines: [` ${item!.name}`, `− позиция ${from + 1}`, `+ позиция ${to + 1}`],
|
||||||
|
resource: 'app-links',
|
||||||
|
marks: { [item!.id]: 'dirty' },
|
||||||
revert: () => setRows(before),
|
revert: () => setRows(before),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -157,6 +162,8 @@ export function AppLinksPage() {
|
|||||||
kind: 'chg',
|
kind: 'chg',
|
||||||
title: 'Порядок сброшен',
|
title: 'Порядок сброшен',
|
||||||
lines: ['− пользовательский порядок', '+ порядок каталога'],
|
lines: ['− пользовательский порядок', '+ порядок каталога'],
|
||||||
|
resource: 'app-links',
|
||||||
|
marks: Object.fromEntries(before.map((r) => [r.id, 'dirty' as const])),
|
||||||
revert: () => setRows(before),
|
revert: () => setRows(before),
|
||||||
})
|
})
|
||||||
}, [addChange])
|
}, [addChange])
|
||||||
@@ -304,6 +311,7 @@ export function AppLinksPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
onReorder={dropOn}
|
onReorder={dropOn}
|
||||||
|
rowStatus={(r) => marks[r.id]}
|
||||||
onRowClick={setEditing}
|
onRowClick={setEditing}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import { UsersGrid, type UsersGridHandlers } from '../users-grid'
|
|||||||
import { usePreviewStore } from '../preview-store'
|
import { usePreviewStore } from '../preview-store'
|
||||||
|
|
||||||
export function UsersPage() {
|
export function UsersPage() {
|
||||||
const { addChange } = usePreviewStore()
|
const { addChange, marksFor } = usePreviewStore()
|
||||||
|
const marks = marksFor('users')
|
||||||
const [users, setUsers] = useState<AdminUser[]>(MOCK_USERS)
|
const [users, setUsers] = useState<AdminUser[]>(MOCK_USERS)
|
||||||
const usersRef = useRef(users)
|
const usersRef = useRef(users)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,6 +31,7 @@ export function UsersPage() {
|
|||||||
const [accessFor, setAccessFor] = useState<AdminUser | null>(null)
|
const [accessFor, setAccessFor] = useState<AdminUser | null>(null)
|
||||||
const [deleteFor, setDeleteFor] = useState<AdminUser | null>(null)
|
const [deleteFor, setDeleteFor] = useState<AdminUser | null>(null)
|
||||||
const [resetFor, setResetFor] = useState<AdminUser | null>(null)
|
const [resetFor, setResetFor] = useState<AdminUser | null>(null)
|
||||||
|
const [bulkDeleteIds, setBulkDeleteIds] = useState<string[] | null>(null)
|
||||||
|
|
||||||
/** Меняет поля у выбранных пользователей и кладёт изменение в «Не сохранено». */
|
/** Меняет поля у выбранных пользователей и кладёт изменение в «Не сохранено». */
|
||||||
const patch = useCallback(
|
const patch = useCallback(
|
||||||
@@ -42,6 +44,8 @@ export function UsersPage() {
|
|||||||
kind: 'chg',
|
kind: 'chg',
|
||||||
title,
|
title,
|
||||||
lines,
|
lines,
|
||||||
|
resource: 'users',
|
||||||
|
marks: Object.fromEntries(before.map((b) => [b.id, 'dirty' as const])),
|
||||||
revert: () => setUsers((prev) => prev.map((u) => before.find((b) => b.id === u.id) ?? u)),
|
revert: () => setUsers((prev) => prev.map((u) => before.find((b) => b.id === u.id) ?? u)),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -78,6 +82,8 @@ export function UsersPage() {
|
|||||||
kind: 'add',
|
kind: 'add',
|
||||||
title: `Создан ${created.email}`,
|
title: `Создан ${created.email}`,
|
||||||
lines: [`+ email: ${created.email}`, `+ name: ${created.name}`, `+ is_admin: ${String(created.is_admin)}`],
|
lines: [`+ email: ${created.email}`, `+ name: ${created.name}`, `+ is_admin: ${String(created.is_admin)}`],
|
||||||
|
resource: 'users',
|
||||||
|
marks: { [created.id]: 'new' },
|
||||||
revert: () => setUsers((prev) => prev.filter((u) => u.id !== created.id)),
|
revert: () => setUsers((prev) => prev.filter((u) => u.id !== created.id)),
|
||||||
})
|
})
|
||||||
setCreateOpen(false)
|
setCreateOpen(false)
|
||||||
@@ -97,6 +103,7 @@ export function UsersPage() {
|
|||||||
onToggleDisabled: onDeactivate,
|
onToggleDisabled: onDeactivate,
|
||||||
onResetPasskeys,
|
onResetPasskeys,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onBulkDelete: (ids) => setBulkDeleteIds(ids),
|
||||||
onBulkDisabled: (ids, disabled) =>
|
onBulkDisabled: (ids, disabled) =>
|
||||||
patch(ids, (u) => ({ ...u, disabled }), `${disabled ? 'Отключено' : 'Включено'} пользователей: ${ids.length}`, () => [
|
patch(ids, (u) => ({ ...u, disabled }), `${disabled ? 'Отключено' : 'Включено'} пользователей: ${ids.length}`, () => [
|
||||||
`− disabled: ${String(!disabled)}`,
|
`− disabled: ${String(!disabled)}`,
|
||||||
@@ -142,7 +149,7 @@ export function UsersPage() {
|
|||||||
<TabsTrigger value="matrix">Доступ по приложениям · {ALL_APP_IDS.length}</TabsTrigger>
|
<TabsTrigger value="matrix">Доступ по приложениям · {ALL_APP_IDS.length}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="users" className="min-w-0">
|
<TabsContent value="users" className="min-w-0">
|
||||||
<UsersGrid users={users} handlers={handlers} />
|
<UsersGrid users={users} handlers={handlers} marks={marks} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="matrix" className="min-w-0">
|
<TabsContent value="matrix" className="min-w-0">
|
||||||
<PreviewSectionGrid
|
<PreviewSectionGrid
|
||||||
@@ -201,23 +208,40 @@ export function UsersPage() {
|
|||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
const target = deleteFor
|
const target = deleteFor
|
||||||
if (!target) return
|
if (!target) return
|
||||||
const index = usersRef.current.findIndex((u) => u.id === target.id)
|
|
||||||
setUsers((prev) => prev.filter((u) => u.id !== target.id))
|
|
||||||
addChange({
|
addChange({
|
||||||
kind: 'del',
|
kind: 'del',
|
||||||
title: `Удалён ${target.email}`,
|
title: `Удалён ${target.email}`,
|
||||||
lines: [`− email: ${target.email}`, `− apps: ${target.apps.length}`],
|
lines: [`− email: ${target.email}`, `− apps: ${target.apps.length}`],
|
||||||
revert: () =>
|
resource: 'users',
|
||||||
setUsers((prev) => {
|
marks: { [target.id]: 'deleted' },
|
||||||
const copy = [...prev]
|
commit: () => setUsers((prev) => prev.filter((u) => u.id !== target.id)),
|
||||||
copy.splice(Math.min(index, copy.length), 0, target)
|
|
||||||
return copy
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
setDeleteFor(null)
|
setDeleteFor(null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TypedConfirm
|
||||||
|
open={bulkDeleteIds !== null}
|
||||||
|
onOpenChange={(o) => !o && setBulkDeleteIds(null)}
|
||||||
|
title="Удалить выбранных пользователей?"
|
||||||
|
description={`Будут удалены учётные записи: ${bulkDeleteIds?.length ?? 0}. Действие попадёт в панель «Не сохранено» и в журнал.`}
|
||||||
|
expected="удалить"
|
||||||
|
confirmLabel="Удалить"
|
||||||
|
onConfirm={() => {
|
||||||
|
const ids = bulkDeleteIds ?? []
|
||||||
|
const targets = usersRef.current.filter((u) => ids.includes(u.id))
|
||||||
|
addChange({
|
||||||
|
kind: 'del',
|
||||||
|
title: `Удалено пользователей: ${targets.length}`,
|
||||||
|
lines: targets.map((t) => `− ${t.email}`),
|
||||||
|
resource: 'users',
|
||||||
|
marks: Object.fromEntries(targets.map((t) => [t.id, 'deleted' as const])),
|
||||||
|
commit: () => setUsers((prev) => prev.filter((u) => !ids.includes(u.id))),
|
||||||
|
})
|
||||||
|
setBulkDeleteIds(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<TypedConfirm
|
<TypedConfirm
|
||||||
open={resetFor !== null}
|
open={resetFor !== null}
|
||||||
onOpenChange={(o) => !o && setResetFor(null)}
|
onOpenChange={(o) => !o && setResetFor(null)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
type Contrast,
|
type Contrast,
|
||||||
type Density,
|
type Density,
|
||||||
type PendingChange,
|
type PendingChange,
|
||||||
|
type RowMark,
|
||||||
type PreviewStore,
|
type PreviewStore,
|
||||||
} from './preview-store'
|
} from './preview-store'
|
||||||
|
|
||||||
@@ -77,15 +78,32 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const applyAll = useCallback(() => {
|
const applyAll = useCallback(() => {
|
||||||
const count = pendingRef.current.length
|
const count = pendingRef.current.length
|
||||||
|
for (const c of pendingRef.current) c.commit?.()
|
||||||
setPending([])
|
setPending([])
|
||||||
toast.success(`Сохранено изменений: ${count}`, {
|
toast.success(`Сохранено изменений: ${count}`, {
|
||||||
description: 'Записано в портал и журнал «Изменения» (макет: ничего не отправляется).',
|
description: 'Записано в портал и журнал «Изменения» (макет: ничего не отправляется).',
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const marksFor = useCallback(
|
||||||
|
(resource: string) => {
|
||||||
|
const out: Record<string, RowMark> = {}
|
||||||
|
for (const c of pending) {
|
||||||
|
if (c.resource !== resource || !c.marks) continue
|
||||||
|
for (const [id, mark] of Object.entries(c.marks)) {
|
||||||
|
// «Будет удалено» сильнее остальных пометок.
|
||||||
|
if (out[id] !== 'deleted') out[id] = mark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
|
[pending],
|
||||||
|
)
|
||||||
|
|
||||||
const value = useMemo<PreviewStore>(
|
const value = useMemo<PreviewStore>(
|
||||||
() => ({
|
() => ({
|
||||||
pending,
|
pending,
|
||||||
|
marksFor,
|
||||||
addChange,
|
addChange,
|
||||||
discardAll,
|
discardAll,
|
||||||
applyAll,
|
applyAll,
|
||||||
@@ -98,7 +116,7 @@ export function PreviewProvider({ children }: { children: ReactNode }) {
|
|||||||
contrast,
|
contrast,
|
||||||
setContrast,
|
setContrast,
|
||||||
}),
|
}),
|
||||||
[pending, addChange, discardAll, applyAll, paletteOpen, pins, togglePin, density, contrast],
|
[pending, marksFor, addChange, discardAll, applyAll, paletteOpen, pins, togglePin, density, contrast],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import { createContext, useContext } from 'react'
|
|||||||
|
|
||||||
export type ChangeKind = 'add' | 'chg' | 'del'
|
export type ChangeKind = 'add' | 'chg' | 'del'
|
||||||
|
|
||||||
|
export type RowMark = 'new' | 'dirty' | 'deleted'
|
||||||
|
|
||||||
export type PendingChange = {
|
export type PendingChange = {
|
||||||
id: string
|
id: string
|
||||||
kind: ChangeKind
|
kind: ChangeKind
|
||||||
title: string
|
title: string
|
||||||
|
/** Раздел, к которому относятся пометки строк («users», «app-links»). */
|
||||||
|
resource?: string
|
||||||
|
/** Пометки строк: «Не сохранено» (dirty/new) и «Будет удалено» (deleted). */
|
||||||
|
marks?: Record<string, RowMark>
|
||||||
|
/** Выполняется при «Сохранить»: окончательно применяет правку (например, убирает удалённую строку). */
|
||||||
|
commit?: () => void
|
||||||
/** Строки diff: «+ …», «− …», « … ». */
|
/** Строки diff: «+ …», «− …», « … ». */
|
||||||
lines: string[]
|
lines: string[]
|
||||||
/** Откатывает локальное состояние страницы при «Отменить». */
|
/** Откатывает локальное состояние страницы при «Отменить». */
|
||||||
@@ -17,6 +25,8 @@ export type Contrast = 'standard' | 'high'
|
|||||||
|
|
||||||
export type PreviewStore = {
|
export type PreviewStore = {
|
||||||
pending: PendingChange[]
|
pending: PendingChange[]
|
||||||
|
/** Пометки строк раздела из накопленных правок. */
|
||||||
|
marksFor: (resource: string) => Record<string, RowMark>
|
||||||
addChange: (change: Omit<PendingChange, 'id'>) => void
|
addChange: (change: Omit<PendingChange, 'id'>) => void
|
||||||
discardAll: () => void
|
discardAll: () => void
|
||||||
applyAll: () => void
|
applyAll: () => void
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { InputGroup, InputGroupAddon, InputGroupInput } from '@authportal/ui/com
|
|||||||
import { cn } from '@authportal/ui/lib/utils'
|
import { cn } from '@authportal/ui/lib/utils'
|
||||||
import { Segmented } from './bits'
|
import { Segmented } from './bits'
|
||||||
import type { Tone } from './mock'
|
import type { Tone } from './mock'
|
||||||
|
import type { RowMark } from './preview-store'
|
||||||
import { TONE_DOT } from './tone'
|
import { TONE_DOT } from './tone'
|
||||||
|
|
||||||
export type GridColumn<T> = {
|
export type GridColumn<T> = {
|
||||||
@@ -66,6 +67,7 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
bulkActions,
|
bulkActions,
|
||||||
toolbar,
|
toolbar,
|
||||||
onReorder,
|
onReorder,
|
||||||
|
rowStatus,
|
||||||
pageSize = 8,
|
pageSize = 8,
|
||||||
emptyMessage = 'Нет записей по текущим фильтрам.',
|
emptyMessage = 'Нет записей по текущим фильтрам.',
|
||||||
className,
|
className,
|
||||||
@@ -84,6 +86,8 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
toolbar?: ReactNode
|
toolbar?: ReactNode
|
||||||
/** Включает перетаскивание строк (dnd-kit). Передаёт id строки и id строки, на место которой её бросили. */
|
/** Включает перетаскивание строк (dnd-kit). Передаёт id строки и id строки, на место которой её бросили. */
|
||||||
onReorder?: (activeId: string, overId: string) => void
|
onReorder?: (activeId: string, overId: string) => void
|
||||||
|
/** Пометка строки: «Не сохранено» (dirty/new) или «Будет удалено» (deleted). */
|
||||||
|
rowStatus?: (row: T) => RowMark | undefined
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
emptyMessage?: string
|
emptyMessage?: string
|
||||||
className?: string
|
className?: string
|
||||||
@@ -193,6 +197,7 @@ export function PreviewSectionGrid<T extends object>({
|
|||||||
i18n={DATA_GRID_RU}
|
i18n={DATA_GRID_RU}
|
||||||
recordCount={data.length}
|
recordCount={data.length}
|
||||||
onRowClick={onRowClick}
|
onRowClick={onRowClick}
|
||||||
|
getRowStatus={rowStatus}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
tableLayout={{ headerSticky: false, dense: true, width: 'auto', rowsDraggable: Boolean(onReorder) }}
|
tableLayout={{ headerSticky: false, dense: true, width: 'auto', rowsDraggable: Boolean(onReorder) }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@authportal/ui/components/dropdown-menu'
|
} from '@authportal/ui/components/dropdown-menu'
|
||||||
import { StatusBadge } from './bits'
|
import { PendingChip, StatusBadge } from './bits'
|
||||||
|
import type { RowMark } from './preview-store'
|
||||||
import { ALL_APP_IDS, APP_SHORT, USER_META, type Tone } from './mock'
|
import { ALL_APP_IDS, APP_SHORT, USER_META, type Tone } from './mock'
|
||||||
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid'
|
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid'
|
||||||
|
|
||||||
@@ -46,10 +47,11 @@ export type UsersGridHandlers = {
|
|||||||
onResetPasskeys: (u: AdminUser) => void
|
onResetPasskeys: (u: AdminUser) => void
|
||||||
onDelete: (u: AdminUser) => void
|
onDelete: (u: AdminUser) => void
|
||||||
onBulkDisabled: (ids: string[], disabled: boolean) => void
|
onBulkDisabled: (ids: string[], disabled: boolean) => void
|
||||||
|
onBulkDelete: (ids: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Каталог пользователей по канвасу: участник, статус, роль, приложения, passkey, активность. */
|
/** Каталог пользователей по канвасу: участник, статус, роль, приложения, passkey, активность. */
|
||||||
export function UsersGrid({ users, handlers }: { users: AdminUser[]; handlers: UsersGridHandlers }) {
|
export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; handlers: UsersGridHandlers; marks: Record<string, RowMark> }) {
|
||||||
const columns = useMemo<GridColumn<AdminUser>[]>(
|
const columns = useMemo<GridColumn<AdminUser>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -59,7 +61,13 @@ export function UsersGrid({ users, handlers }: { users: AdminUser[]; handlers: U
|
|||||||
sortValue: (u) => u.email,
|
sortValue: (u) => u.email,
|
||||||
cell: (u) => <TwoLineCell dot={userStatus(u).tone} title={u.email} sub={USER_META[u.id]?.sub ?? u.name} />,
|
cell: (u) => <TwoLineCell dot={userStatus(u).tone} title={u.email} sub={USER_META[u.id]?.sub ?? u.name} />,
|
||||||
},
|
},
|
||||||
{ id: 'status', title: 'Статус', size: 140, cell: (u) => <StatusBadge tone={userStatus(u).tone}>{userStatus(u).label}</StatusBadge> },
|
{ id: 'status', title: 'Статус', size: 140, cell: (u) => (
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<StatusBadge tone={userStatus(u).tone}>{userStatus(u).label}</StatusBadge>
|
||||||
|
{marks[u.id] ? <PendingChip mark={marks[u.id]!} /> : null}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
{ id: 'role', title: 'Роль', size: 130, sortValue: (u) => (u.is_admin ? 'a' : 'u'), cell: (u) => <span className="text-sm">{u.is_admin ? 'Админ' : 'Пользователь'}</span> },
|
{ id: 'role', title: 'Роль', size: 130, sortValue: (u) => (u.is_admin ? 'a' : 'u'), cell: (u) => <span className="text-sm">{u.is_admin ? 'Админ' : 'Пользователь'}</span> },
|
||||||
{ id: 'apps', title: 'Приложения', size: 220, cell: (u) => <span className="block truncate text-sm">{appsSummary(u)}</span> },
|
{ id: 'apps', title: 'Приложения', size: 220, cell: (u) => <span className="block truncate text-sm">{appsSummary(u)}</span> },
|
||||||
{ id: 'passkey', title: 'Passkey', size: 100, sortValue: (u) => u.passkey_count, cell: (u) => <span className="text-sm">{passkeyLabel(u.passkey_count)}</span> },
|
{ id: 'passkey', title: 'Passkey', size: 100, sortValue: (u) => u.passkey_count, cell: (u) => <span className="text-sm">{passkeyLabel(u.passkey_count)}</span> },
|
||||||
@@ -104,7 +112,7 @@ export function UsersGrid({ users, handlers }: { users: AdminUser[]; handlers: U
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[handlers],
|
[handlers, marks],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -124,6 +132,7 @@ export function UsersGrid({ users, handlers }: { users: AdminUser[]; handlers: U
|
|||||||
{ label: 'Отключённые', match: (u) => u.disabled },
|
{ label: 'Отключённые', match: (u) => u.disabled },
|
||||||
]}
|
]}
|
||||||
onRowClick={handlers.onOpenAccess}
|
onRowClick={handlers.onOpenAccess}
|
||||||
|
rowStatus={(u) => marks[u.id]}
|
||||||
bulkActions={(ids, clear) => (
|
bulkActions={(ids, clear) => (
|
||||||
<>
|
<>
|
||||||
<Button type="button" size="sm" variant="outline" onClick={() => { handlers.onBulkDisabled(ids, false); clear() }}>
|
<Button type="button" size="sm" variant="outline" onClick={() => { handlers.onBulkDisabled(ids, false); clear() }}>
|
||||||
@@ -132,6 +141,9 @@ export function UsersGrid({ users, handlers }: { users: AdminUser[]; handlers: U
|
|||||||
<Button type="button" size="sm" variant="outline" onClick={() => { handlers.onBulkDisabled(ids, true); clear() }}>
|
<Button type="button" size="sm" variant="outline" onClick={() => { handlers.onBulkDisabled(ids, true); clear() }}>
|
||||||
Отключить
|
Отключить
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button type="button" size="sm" variant="outline" onClick={() => handlers.onBulkDelete(ids)}>
|
||||||
|
Удалить…
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user