refactor(admin): remove sidebar favorites
This commit is contained in:
1 parent
a070004e7b
commit
96234075f8
7 files changed
+1
-152
No files matched your search
@@ -95,7 +95,6 @@ async function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AdminSidebarNav
|
<AdminSidebarNav
|
||||||
userId={staff.id}
|
|
||||||
allowedPermissions={allowedPermissions}
|
allowedPermissions={allowedPermissions}
|
||||||
isSuperAdmin={isSuperAdmin}
|
isSuperAdmin={isSuperAdmin}
|
||||||
navConfig={navConfig}
|
navConfig={navConfig}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { Star, X } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import type { AdminNavItem } from "@/lib/admin-nav";
|
|
||||||
import { parseFavoritePages } from "@/lib/admin-navigation";
|
|
||||||
import { AdminNavLink } from "./admin-nav-link";
|
|
||||||
|
|
||||||
export function AdminFavorites({
|
|
||||||
userId,
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
userId: number;
|
|
||||||
items: AdminNavItem[];
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("pages.admin.nav");
|
|
||||||
const key = `admin-favorites:${userId}`;
|
|
||||||
const [saved, setSaved] = useState<{ key: string; hrefs: string[] }>({
|
|
||||||
key: "",
|
|
||||||
hrefs: [],
|
|
||||||
});
|
|
||||||
const [storageError, setStorageError] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
setSaved({ key, hrefs: parseFavoritePages(localStorage.getItem(key)) });
|
|
||||||
} catch {
|
|
||||||
setSaved({ key, hrefs: [] });
|
|
||||||
}
|
|
||||||
}, [key]);
|
|
||||||
const favorites =
|
|
||||||
saved.key === key
|
|
||||||
? saved.hrefs.flatMap((href) => items.find((i) => i.href === href) ?? [])
|
|
||||||
: [];
|
|
||||||
function save(hrefs: string[]) {
|
|
||||||
setSaved({ key, hrefs });
|
|
||||||
try {
|
|
||||||
localStorage.setItem(key, JSON.stringify(hrefs));
|
|
||||||
setStorageError(false);
|
|
||||||
} catch {
|
|
||||||
setStorageError(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className="mb-3 border-b border-[var(--admin-border)] pb-3"
|
|
||||||
aria-label={t("favorites")}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
style={{ fontSize: ".75rem", margin: 0 }}
|
|
||||||
className="flex items-center gap-2 px-2 py-2 text-xs font-semibold text-[var(--admin-sidebar-muted-readable)]"
|
|
||||||
>
|
|
||||||
<Star size={14} aria-hidden />
|
|
||||||
{t("favorites")}
|
|
||||||
</h2>
|
|
||||||
{favorites.map((item) => (
|
|
||||||
<div key={item.href} className="flex items-center">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<AdminNavLink {...item} icon={<item.icon size={16} />}>
|
|
||||||
{t(item.labelKey)}
|
|
||||||
</AdminNavLink>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
save(
|
|
||||||
favorites
|
|
||||||
.filter((i) => i.href !== item.href)
|
|
||||||
.map((i) => i.href),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
aria-label={t("removeFavorite", { name: t(item.labelKey) })}
|
|
||||||
className="grid h-9 w-9 shrink-0 place-items-center rounded focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]"
|
|
||||||
>
|
|
||||||
<X size={14} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{favorites.length < 6 && (
|
|
||||||
<select
|
|
||||||
aria-label={t("addFavorite")}
|
|
||||||
value=""
|
|
||||||
disabled={saved.key !== key}
|
|
||||||
onChange={(event) => {
|
|
||||||
if (event.target.value)
|
|
||||||
save([...favorites.map((i) => i.href), event.target.value]);
|
|
||||||
}}
|
|
||||||
className="mt-1 h-9 w-full min-w-0 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-sidebar-background)] px-2 text-xs text-[var(--admin-sidebar-text-readable)]"
|
|
||||||
>
|
|
||||||
<option value="">{t("addFavorite")}</option>
|
|
||||||
{items
|
|
||||||
.filter((item) => !favorites.some((f) => f.href === item.href))
|
|
||||||
.map((item) => (
|
|
||||||
<option key={item.href} value={item.href}>
|
|
||||||
{t(item.labelKey)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
{storageError && (
|
|
||||||
<p role="status" className="px-2 text-xs">
|
|
||||||
{t("favoriteStorageError")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ import type { AdminNavItem } from "@/lib/admin-nav";
|
|||||||
import type { AdminNavConfig } from "@/lib/admin-nav-config";
|
import type { AdminNavConfig } from "@/lib/admin-nav-config";
|
||||||
import { visibleAdminNavigation } from "@/lib/admin-navigation";
|
import { visibleAdminNavigation } from "@/lib/admin-navigation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AdminFavorites } from "./admin-favorites";
|
|
||||||
|
|
||||||
const STORAGE_KEY = "admin-nav-collapsed";
|
const STORAGE_KEY = "admin-nav-collapsed";
|
||||||
|
|
||||||
@@ -62,12 +61,10 @@ function persistCollapsed(next: Record<string, boolean>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AdminSidebarNav({
|
export function AdminSidebarNav({
|
||||||
userId = 0,
|
|
||||||
allowedPermissions = [],
|
allowedPermissions = [],
|
||||||
isSuperAdmin = false,
|
isSuperAdmin = false,
|
||||||
navConfig = {},
|
navConfig = {},
|
||||||
}: {
|
}: {
|
||||||
userId?: number;
|
|
||||||
/** ACL slugs the staff member holds (ignored when isSuperAdmin). */
|
/** ACL slugs the staff member holds (ignored when isSuperAdmin). */
|
||||||
allowedPermissions?: string[];
|
allowedPermissions?: string[];
|
||||||
isSuperAdmin?: boolean;
|
isSuperAdmin?: boolean;
|
||||||
@@ -263,12 +260,6 @@ export function AdminSidebarNav({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 py-3">
|
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 py-3">
|
||||||
{!search && (
|
|
||||||
<AdminFavorites
|
|
||||||
userId={userId}
|
|
||||||
items={groups.flatMap((group) => group.items)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{visibleGroups.length === 0 && (
|
{visibleGroups.length === 0 && (
|
||||||
<p
|
<p
|
||||||
role="status"
|
role="status"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, it } from "vitest";
|
import { expect, it } from "vitest";
|
||||||
import { ADMIN_NAV_GROUPS } from "@/lib/admin-nav";
|
import { ADMIN_NAV_GROUPS } from "@/lib/admin-nav";
|
||||||
import { parseFavoritePages, visibleAdminNavigation } from "./admin-navigation";
|
import { visibleAdminNavigation } from "./admin-navigation";
|
||||||
|
|
||||||
const access = { isSuperAdmin: true, allowedPermissions: [], navConfig: {} };
|
const access = { isSuperAdmin: true, allowedPermissions: [], navConfig: {} };
|
||||||
it("retains each route and its permission definition once after regrouping", () => {
|
it("retains each route and its permission definition once after regrouping", () => {
|
||||||
@@ -36,11 +36,3 @@ it("respects hidden groups, owner order and ACL in every consumer", () => {
|
|||||||
}).flatMap((g) => g.items);
|
}).flatMap((g) => g.items);
|
||||||
expect(limited.every((i) => !i.permission)).toBe(true);
|
expect(limited.every((i) => !i.permission)).toBe(true);
|
||||||
});
|
});
|
||||||
it("rejects malformed favorites, external links and duplicates", () => {
|
|
||||||
expect(parseFavoritePages("{")).toEqual([]);
|
|
||||||
expect(
|
|
||||||
parseFavoritePages(
|
|
||||||
'["/admin/users","https://evil.test","/admin/users",null,"/admin?token=x"]',
|
|
||||||
),
|
|
||||||
).toEqual(["/admin/users"]);
|
|
||||||
});
|
|
||||||
@@ -159,22 +159,3 @@ export function visibleAdminNavigation(
|
|||||||
});
|
});
|
||||||
return organized;
|
return organized;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseFavoritePages(raw: string | null): string[] {
|
|
||||||
try {
|
|
||||||
const value: unknown = JSON.parse(raw ?? "[]");
|
|
||||||
return Array.isArray(value)
|
|
||||||
? [
|
|
||||||
...new Set(
|
|
||||||
value.filter(
|
|
||||||
(entry): entry is string =>
|
|
||||||
typeof entry === "string" &&
|
|
||||||
/^\/admin(?:\/[^?#]*)?$/.test(entry),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
].slice(0, 6)
|
|
||||||
: [];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1099,10 +1099,6 @@
|
|||||||
"catalogFurniture": "Catalog and furniture",
|
"catalogFurniture": "Catalog and furniture",
|
||||||
"communityContent": "Community and content",
|
"communityContent": "Community and content",
|
||||||
"staffPermissions": "Staff and access",
|
"staffPermissions": "Staff and access",
|
||||||
"favorites": "Favorites",
|
|
||||||
"addFavorite": "Add favorite",
|
|
||||||
"removeFavorite": "Remove {name} from favorites",
|
|
||||||
"favoriteStorageError": "Favorites are available for this session; browser storage is unavailable.",
|
|
||||||
"cmsErrors": "CMS errors",
|
"cmsErrors": "CMS errors",
|
||||||
"themeBuilder": "Theme builder",
|
"themeBuilder": "Theme builder",
|
||||||
"openSidebar": "Open menu",
|
"openSidebar": "Open menu",
|
||||||
|
|||||||
@@ -1057,10 +1057,6 @@
|
|||||||
"catalogFurniture": "Catalogo e furni",
|
"catalogFurniture": "Catalogo e furni",
|
||||||
"communityContent": "Community e contenuti",
|
"communityContent": "Community e contenuti",
|
||||||
"staffPermissions": "Staff e accessi",
|
"staffPermissions": "Staff e accessi",
|
||||||
"favorites": "Preferiti",
|
|
||||||
"addFavorite": "Aggiungi preferito",
|
|
||||||
"removeFavorite": "Rimuovi {name} dai preferiti",
|
|
||||||
"favoriteStorageError": "Preferiti disponibili per questa sessione: memoria del browser non disponibile.",
|
|
||||||
"cmsErrors": "Errori CMS",
|
"cmsErrors": "Errori CMS",
|
||||||
"themeBuilder": "Editor del tema",
|
"themeBuilder": "Editor del tema",
|
||||||
"catalogMaintenance": "Manutenzione catalogo",
|
"catalogMaintenance": "Manutenzione catalogo",
|
||||||
|
|||||||
Reference in new issue
Block a user