feat(nav): visibility scope for custom sidebar items (#3578)

Custom nav items rendered for every visitor including share-page and
logged-out contexts. Items gain a scope field: public (default,
unchanged behavior) or user (logged-in accounts only). The sidebar
filters on render; the admin editor exposes a per-item selector.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent 2b812d8447
commit 655bec9805

@ -170,6 +170,9 @@
"customNavItems": "Custom sidebar items",
"customNavItemsDes": "You can add custom items to the sidebar, and users will be redirected to the corresponding link when clicked.",
"navItemUrl": "Link",
"navItemVisibility": "Visibility",
"navScopePublic": "Everyone",
"navScopeUser": "Logged-in users",
"iconifyNamePlaceholder": "Iconify icon identifier, e.g. fluent:home-24-regular",
"imageUrl": "Image URL",
"iconifyName": "Iconify icon name",

@ -170,6 +170,9 @@
"customNavItems": "自定义侧边导航栏",
"customNavItemsDes": "你可以在左侧导航栏中添加自定义的条目,用户点击后会跳转到对应的链接。",
"navItemUrl": "链接",
"navItemVisibility": "可见范围",
"navScopePublic": "所有人",
"navScopeUser": "登录用户",
"iconifyNamePlaceholder": "Iconify 图标标识,比如:fluent:home-24-regular",
"imageUrl": "图像 URL",
"iconifyName": "Iconify 图标名",

@ -65,6 +65,7 @@ export interface CustomNavItem {
name: string;
url: string;
icon: string;
scope?: "public" | "user";
}
export interface CustomHTML {

@ -2,6 +2,7 @@ import { Icon } from "@iconify/react/dist/iconify.js";
import {
Box,
IconButton,
MenuItem,
Table,
TableBody,
TableCell,
@ -18,6 +19,7 @@ import { useTranslation } from "react-i18next";
import { CustomNavItem } from "../../../../api/site";
import {
DenseFilledTextField,
DenseSelect,
NoWrapCell,
NoWrapTableCell,
SecondaryButton,
@ -49,6 +51,7 @@ interface DraggableNavItemRowProps {
inputCache: { [key: number]: { [field: string]: string | undefined } };
onInputChange: (index: number, field: string, value: string) => void;
onInputBlur: (index: number, field: keyof CustomNavItem) => void;
onFieldChange: (index: number, field: keyof CustomNavItem, value: string) => void;
IconPreview: React.ComponentType<{ iconName: string }>;
t: any;
style?: React.CSSProperties;
@ -69,6 +72,7 @@ const DraggableNavItemRow = React.memo(
inputCache,
onInputChange,
onInputBlur,
onFieldChange,
IconPreview,
t,
style,
@ -144,6 +148,17 @@ const DraggableNavItemRow = React.memo(
placeholder="https://example.com"
/>
</TableCell>
<TableCell>
<DenseSelect
fullWidth
size="small"
value={item.scope === "user" ? "user" : "public"}
onChange={(e) => onFieldChange(index, "scope", e.target.value as string)}
>
<MenuItem value="public">{t("settings.navScopePublic")}</MenuItem>
<MenuItem value="user">{t("settings.navScopeUser")}</MenuItem>
</DenseSelect>
</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={() => onDelete(index)}>
<Delete fontSize="small" />
@ -327,6 +342,7 @@ const CustomNavItems = ({ value, onChange }: CustomNavItemsProps) => {
<NoWrapTableCell width={200}>{t("settings.iconifyName")}</NoWrapTableCell>
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
<NoWrapTableCell width={250}>{t("settings.navItemUrl")}</NoWrapTableCell>
<NoWrapTableCell width={130}>{t("settings.navItemVisibility")}</NoWrapTableCell>
<NoWrapTableCell width={80} align="right"></NoWrapTableCell>
<NoWrapTableCell width={80}></NoWrapTableCell>
</TableRow>
@ -349,6 +365,7 @@ const CustomNavItems = ({ value, onChange }: CustomNavItemsProps) => {
inputCache={inputCache}
onInputChange={handleInputChange}
onInputBlur={handleInputBlur}
onFieldChange={handleFieldChange}
IconPreview={IconPreview}
t={t}
/>
@ -356,7 +373,7 @@ const CustomNavItems = ({ value, onChange }: CustomNavItemsProps) => {
})}
{items.length === 0 && (
<TableRow>
<NoWrapCell colSpan={6} align="center">
<NoWrapCell colSpan={7} align="center">
<Typography variant="caption" color="text.secondary">
{t("application:setting.listEmpty")}
</Typography>

@ -256,7 +256,9 @@ const PageNavigation = () => {
)}
{customNavItems && customNavItems.length > 0 && (
<Box>
{customNavItems.map((item) => (
{customNavItems
.filter((item) => isLogin || item.scope !== "user")
.map((item) => (
<SideNavItemComponent
key={item.name}
item={{

@ -244,6 +244,9 @@ type CustomNavItem struct {
Icon string `json:"icon"`
Name string `json:"name"`
URL string `json:"url"`
// Scope limits display: "" or "public" shows to everyone, "user" only
// to logged-in accounts (#3578).
Scope string `json:"scope,omitempty"`
}
type CustomHTML struct {

Loading…
Cancel
Save