fix(web): keep menu labels on one line and fold the admin section by default
ci / ts / ts (packages + web) (push) Successful in 1m27s
ci / ts / ts (packages + web) (push) Successful in 1m27s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
dfc72b1d69
commit
2b76060ea3
4 files changed
+34
-9
No files matched your search
@@ -117,7 +117,8 @@ describe('navigation by role', () => {
|
|||||||
|
|
||||||
it('shows users but not the audit log to moderators', async () => {
|
it('shows users but not the audit log to moderators', async () => {
|
||||||
const mod = makeUser('mod', 'moderator');
|
const mod = makeUser('mod', 'moderator');
|
||||||
await renderApp([mod], '/', { signedInAs: mod });
|
const { user } = await renderApp([mod], '/', { signedInAs: mod });
|
||||||
|
await user.click(await screen.findByRole('button', { name: 'Админка' }));
|
||||||
expect(await screen.findByRole('link', { name: 'Пользователи' })).toBeInTheDocument();
|
expect(await screen.findByRole('link', { name: 'Пользователи' })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole('link', { name: 'Журнал' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('link', { name: 'Журнал' })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -268,7 +268,9 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
top: calc(100% + 6px);
|
top: calc(100% + 6px);
|
||||||
z-index: 40;
|
z-index: 40;
|
||||||
|
width: max-content;
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
|
max-width: min(360px, calc(100vw - 24px));
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--elev);
|
background: var(--elev);
|
||||||
@@ -292,6 +294,7 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
|
|
||||||
.profileHead {
|
.profileHead {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto max-content;
|
||||||
gap: 4px 12px;
|
gap: 4px 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 10px 12px;
|
padding: 10px 10px 12px;
|
||||||
@@ -100,11 +100,13 @@
|
|||||||
|
|
||||||
.profileName {
|
.profileName {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileMeta {
|
.profileMeta {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text3);
|
color: var(--text3);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side {
|
.side {
|
||||||
|
|||||||
@@ -79,25 +79,44 @@ function NavLink({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FOLDED_KEY = 'pipu.nav.folded';
|
const FOLDED_KEY = 'pipu.nav.folded';
|
||||||
|
const TOUCHED_KEY = 'pipu.nav.touched';
|
||||||
|
|
||||||
function readFolded(): string[] {
|
function readIds(key: string): string[] {
|
||||||
try {
|
try {
|
||||||
const v: unknown = JSON.parse(localStorage.getItem(FOLDED_KEY) ?? '[]');
|
const v: unknown = JSON.parse(localStorage.getItem(key) ?? '[]');
|
||||||
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [];
|
return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Раздел меню: заголовок сворачивает пункты; состояние помнит браузер. Свёрнутый раздел показывает сумму бейджей. */
|
/** Раздел меню: заголовок сворачивает пункты; явный выбор помнит браузер. Свёрнутый раздел показывает сумму бейджей. */
|
||||||
function NavSection({ id, title, badge, children }: { id: string; title: string; badge?: number; children: ReactNode }) {
|
function NavSection({
|
||||||
const [folded, setFolded] = useState(() => readFolded().includes(id));
|
id,
|
||||||
|
title,
|
||||||
|
badge,
|
||||||
|
children,
|
||||||
|
defaultFolded = false,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
badge?: number;
|
||||||
|
children: ReactNode;
|
||||||
|
defaultFolded?: boolean;
|
||||||
|
}) {
|
||||||
|
const [folded, setFolded] = useState(() => {
|
||||||
|
if (readIds(FOLDED_KEY).includes(id)) return true;
|
||||||
|
if (readIds(TOUCHED_KEY).includes(id)) return false;
|
||||||
|
return defaultFolded;
|
||||||
|
});
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
const next = !folded;
|
const next = !folded;
|
||||||
setFolded(next);
|
setFolded(next);
|
||||||
try {
|
try {
|
||||||
const rest = readFolded().filter((x) => x !== id);
|
const rest = readIds(FOLDED_KEY).filter((x) => x !== id);
|
||||||
localStorage.setItem(FOLDED_KEY, JSON.stringify(next ? [...rest, id] : rest));
|
localStorage.setItem(FOLDED_KEY, JSON.stringify(next ? [...rest, id] : rest));
|
||||||
|
const touched = readIds(TOUCHED_KEY);
|
||||||
|
if (!touched.includes(id)) localStorage.setItem(TOUCHED_KEY, JSON.stringify([...touched, id]));
|
||||||
} catch {
|
} catch {
|
||||||
// Без хранилища раздел просто не запомнит состояние.
|
// Без хранилища раздел просто не запомнит состояние.
|
||||||
}
|
}
|
||||||
@@ -188,7 +207,7 @@ function Shell() {
|
|||||||
/>
|
/>
|
||||||
</NavSection>
|
</NavSection>
|
||||||
{staff && (
|
{staff && (
|
||||||
<NavSection id="admin" title={t('nav.admin')} badge={pending}>
|
<NavSection id="admin" title={t('nav.admin')} badge={pending} defaultFolded>
|
||||||
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
<NavLink to="/admin/users" label={t('nav.users')} icon={groupIcon} activeIcon={groupFillIcon} />
|
||||||
{user.role === 'admin' && (
|
{user.role === 'admin' && (
|
||||||
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
<NavLink to="/admin/sources" label={t('nav.sources')} icon={storageIcon} activeIcon={storageFillIcon} />
|
||||||
|
|||||||
Reference in new issue
Block a user