From 7c161a138b0a4c750d10f7004c9ff7df0c52664d Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:32:29 +0500 Subject: [PATCH] =?UTF-8?q?feat(super-admin):=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=87=D0=B8=D0=B9=20quick-switch=20+=20UI-=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BC=D1=83=D1=82?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B9=20=D0=B2=20read-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit БАГ: dropdown «Открыть организацию» в topbar системной консоли вёл сначала на reload текущего /super-admin, а оттуда внутренний редирект конкурировал с TenantRouteGuard'ом, и юзер видел alert «Откройте через "Открыть как…"». Фикс — setOrgOverride получил опциональный { redirectTo }, делает window.location.assign(target) вместо reload. Точки вызова обновлены: - dropdown в SuperAdminLayout topbar → redirectTo='/dashboard' - кнопка «Открыть как» в строке таблицы орг → redirectTo='/dashboard' - кнопка «Выйти из режима» в баннере → redirectTo='/super-admin/organizations' Хук useReadOnly() централизует «override активен И edit-mode не включён» в один { readOnly, reason }. Button по умолчанию считает variant='primary' и variant='danger' мутирующими (опт-аут через mutating={false} для редких primary-без-мутаций) — в read-only они автоматически disabled с tooltip «Только просмотр. Включите редактирование в баннере…». Variant='secondary' и 'ghost' не блокируются — Cancel/Назад/Закрыть остаются кликабельны. Серверный 403 (ReadonlyOverrideMiddleware) остаётся как safety-net. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/food-market.web/src/components/Button.tsx | 17 ++++++++++++++++- .../src/components/SuperAdminAsOrgBanner.tsx | 2 +- src/food-market.web/src/lib/api.ts | 13 +++++++++---- src/food-market.web/src/lib/useReadOnly.ts | 15 +++++++++++++++ .../src/pages/SuperAdminOrganizationsPage.tsx | 2 +- 5 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 src/food-market.web/src/lib/useReadOnly.ts diff --git a/src/food-market.web/src/components/Button.tsx b/src/food-market.web/src/components/Button.tsx index 47b8a4f..704a935 100644 --- a/src/food-market.web/src/components/Button.tsx +++ b/src/food-market.web/src/components/Button.tsx @@ -1,5 +1,6 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react' import { cn } from '@/lib/utils' +import { useReadOnly } from '@/lib/useReadOnly' type Variant = 'primary' | 'secondary' | 'ghost' | 'danger' type Size = 'sm' | 'md' @@ -7,6 +8,10 @@ type Size = 'sm' | 'md' interface ButtonProps extends ButtonHTMLAttributes { variant?: Variant size?: Size + /** Кнопка инициирует мутацию данных. В read-only режиме SuperAdmin'а + * (override без edit-mode) такие кнопки автоматически disabled с tooltip. + * Нав-кнопки/закрытие модалок/Cancel — оставлять без этого пропа. */ + mutating?: boolean children: ReactNode } @@ -22,10 +27,20 @@ const sizes: Record = { md: 'px-3.5 py-1.5 text-sm', } -export function Button({ variant = 'primary', size = 'md', className, children, ...rest }: ButtonProps) { +export function Button({ variant = 'primary', size = 'md', mutating, className, children, disabled, title, ...rest }: ButtonProps) { + const ro = useReadOnly() + // Variant primary/danger по умолчанию считаем мутирующими (Добавить/ + // Сохранить/Удалить/Создать почти всегда primary либо danger). Secondary/ + // ghost — нав-кнопки и Cancel — не блокируются. Явный mutating={false} + // нужен только в редких случаях когда primary не делает мутации (например + // «Применить фильтр», «Войти в режим…»). + const isMutating = mutating ?? (variant === 'primary' || variant === 'danger') + const blocked = isMutating && ro.readOnly return ( )} - diff --git a/src/food-market.web/src/lib/api.ts b/src/food-market.web/src/lib/api.ts index 5117022..c8692e7 100644 --- a/src/food-market.web/src/lib/api.ts +++ b/src/food-market.web/src/lib/api.ts @@ -32,12 +32,17 @@ export function getOrgOverride(): OrgOverride | null { return raw ? JSON.parse(raw) as OrgOverride : null } catch { return null } } -export function setOrgOverride(value: OrgOverride | null) { +export function setOrgOverride(value: OrgOverride | null, opts?: { redirectTo?: string }) { if (value) localStorage.setItem(ORG_OVERRIDE_KEY, JSON.stringify(value)) else { localStorage.removeItem(ORG_OVERRIDE_KEY); localStorage.removeItem(EDIT_MODE_KEY) } - // Силой обновляем все вкладки/страницы — кэш TanStack Query построен по - // tenant'у, нужен hard reload чтобы снять старые данные. - if (typeof window !== 'undefined') window.location.reload() + // Hard navigation чтобы (а) снести TanStack Query кэш, (б) гарантированно + // выйти из текущего layout'а в нужный (override → tenant /dashboard, + // выход → /super-admin/organizations). Без redirectTo — просто reload + // на ту же страницу (старое поведение, для совместимости). + if (typeof window !== 'undefined') { + if (opts?.redirectTo) window.location.assign(opts.redirectTo) + else window.location.reload() + } } const EDIT_MODE_KEY = 'superAdminEditMode' diff --git a/src/food-market.web/src/lib/useReadOnly.ts b/src/food-market.web/src/lib/useReadOnly.ts new file mode 100644 index 0000000..12d0621 --- /dev/null +++ b/src/food-market.web/src/lib/useReadOnly.ts @@ -0,0 +1,15 @@ +import { getOrgOverride, getEditMode } from '@/lib/api' + +/** SuperAdmin зашёл в режим «открыто как…» и edit-mode НЕ включён — + * любые мутации в UI должны быть отключены. Возвращает { readOnly, reason } + * чтобы кнопки могли показать tooltip и причину. */ +export function useReadOnly(): { readOnly: boolean; reason: string } { + const ov = getOrgOverride() + if (!ov) return { readOnly: false, reason: '' } + const edit = getEditMode() + if (edit && edit.expiresAt > Date.now()) return { readOnly: false, reason: '' } + return { + readOnly: true, + reason: 'Только просмотр. Включите редактирование в баннере сверху, чтобы менять данные.', + } +} diff --git a/src/food-market.web/src/pages/SuperAdminOrganizationsPage.tsx b/src/food-market.web/src/pages/SuperAdminOrganizationsPage.tsx index a9d5ee5..d060b5b 100644 --- a/src/food-market.web/src/pages/SuperAdminOrganizationsPage.tsx +++ b/src/food-market.web/src/pages/SuperAdminOrganizationsPage.tsx @@ -100,7 +100,7 @@ export function SuperAdminOrganizationsPage() {
e.stopPropagation()}> {!r.isArchived && (