Repository navigation
feat(frontend): add custom navigation items support with drag-and-drop reordering #4082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| /* | ||
| Copyright (C) 2025 QuantumNous | ||
|
|
||
| This program is free software: you can redistribute it and/or modify | ||
| it under the terms of the GNU Affero General Public License as | ||
| published by the Free Software Foundation, either version 3 of the | ||
| License, or (at your option) any later version. | ||
|
|
||
| This program is distributed in the hope that it will be useful, | ||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| GNU Affero General Public License for more details. | ||
|
|
||
| You should have received a copy of the GNU Affero General Public License | ||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
|
|
||
| For commercial licensing, please contact support@quantumnous.com | ||
| */ | ||
|
|
||
| const BUILT_IN_ORDER = ['home', 'console', 'pricing', 'docs', 'about']; | ||
|
|
||
| export function migrateOldFormatToItems(modules) { | ||
| const items = BUILT_IN_ORDER.map((key) => { | ||
| if (key === 'pricing') { | ||
| const val = modules[key]; | ||
| if (typeof val === 'object') { | ||
| return { | ||
| key, | ||
| enabled: val.enabled !== false, | ||
| requireAuth: val.requireAuth || false, | ||
| }; | ||
| } | ||
| return { key, enabled: val !== false, requireAuth: false }; | ||
| } | ||
| return { key, enabled: modules[key] !== false }; | ||
| }); | ||
|
|
||
| const customItems = Array.isArray(modules.customItems) | ||
| ? modules.customItems | ||
| : []; | ||
| const sorted = [...customItems].sort( | ||
| (a, b) => (a.position ?? 99) - (b.position ?? 99), | ||
| ); | ||
|
|
||
| const cursors = new Map(); | ||
| for (const ci of sorted) { | ||
| const pos = ci.position ?? 99; | ||
| const { position: _, ...rest } = ci; | ||
| const customItem = { ...rest }; | ||
|
|
||
| if (pos === 0) { | ||
| const cursor = cursors.get('start') ?? 0; | ||
| items.splice(cursor, 0, customItem); | ||
| cursors.set('start', cursor + 1); | ||
| } else if (pos === 99) { | ||
| items.push(customItem); | ||
| } else { | ||
| const anchorKey = BUILT_IN_ORDER[pos - 1]; | ||
| const anchorIdx = items.findIndex((it) => it.key === anchorKey); | ||
| if (anchorIdx >= 0) { | ||
| const cursor = cursors.get(anchorKey) ?? anchorIdx + 1; | ||
| items.splice(cursor, 0, customItem); | ||
| cursors.set(anchorKey, cursor + 1); | ||
| } else { | ||
| items.push(customItem); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return items; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3313,6 +3313,16 @@ | |
| "从剪贴板粘贴配置": "クリップボードから貼り付け", | ||
| "剪贴板中未检测到连接信息": "クリップボードに接続情報が見つかりません", | ||
| "连接信息已填入": "接続情報を入力しました", | ||
| "无法读取剪贴板": "クリップボードを読み取れません" | ||
| "无法读取剪贴板": "クリップボードを読み取れません", | ||
| "在新标签页打开": "新しいタブで開く", | ||
| "添加导航项": "ナビゲーション項目を追加", | ||
| "编辑导航项": "ナビゲーション項目を編集", | ||
| "链接地址": "リンクアドレス", | ||
| "内部路径": "内部パス", | ||
| "外部链接": "外部リンク", | ||
| "最多添加 {{max}} 个自定义导航项": "カスタムナビゲーション項目は最大{{max}}個まで", | ||
| "请填写完整信息": "情報を完全に入力してください", | ||
| "链接示例: https://example.com 或 /path": "例: https://example.com または /path", | ||
| "拖拽可调整顺序": "ドラッグして並べ替え" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add the new settings helper-text key to this locale.
As per coding guidelines 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2980,6 +2980,17 @@ | |
| "从剪贴板粘贴配置": "从剪贴板粘贴配置", | ||
| "剪贴板中未检测到连接信息": "剪贴板中未检测到连接信息", | ||
| "连接信息已填入": "连接信息已填入", | ||
| "无法读取剪贴板": "无法读取剪贴板" | ||
| "无法读取剪贴板": "无法读取剪贴板", | ||
| "在新标签页打开": "在新标签页打开", | ||
| "添加导航项": "添加导航项", | ||
| "编辑导航项": "编辑导航项", | ||
| "链接地址": "链接地址", | ||
| "内部路径": "内部路径", | ||
| "外部链接": "外部链接", | ||
| "最多添加 {{max}} 个自定义导航项": "最多添加 {{max}} 个自定义导航项", | ||
| "请填写完整信息": "请填写完整信息", | ||
| "链接示例: https://example.com 或 /path": "链接示例: https://example.com 或 /path", | ||
| "拖拽可调整顺序": "拖拽可调整顺序", | ||
| "内置": "内置" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add the new section help text to the locale bundle.
➕ Suggested locale addition "拖拽可调整顺序": "拖拽可调整顺序",
- "内置": "内置"
+ "内置": "内置",
+ "控制顶栏导航项的显示和排序,全局生效": "控制顶栏导航项的显示和排序,全局生效"🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden migration against malformed persisted config.
At Line 26 and Line 48, null/non-object values can throw (
pricing: null,customItems: [null]) and break header rendering. Please guard object assumptions before property access/destructuring.🔧 Proposed defensive fix
export function migrateOldFormatToItems(modules) { + const safeModules = + modules && typeof modules === 'object' && !Array.isArray(modules) + ? modules + : {}; + const items = BUILT_IN_ORDER.map((key) => { if (key === 'pricing') { - const val = modules[key]; - if (typeof val === 'object') { + const val = safeModules[key]; + if (val && typeof val === 'object' && !Array.isArray(val)) { return { key, enabled: val.enabled !== false, - requireAuth: val.requireAuth || false, + requireAuth: val.requireAuth === true, }; } return { key, enabled: val !== false, requireAuth: false }; } - return { key, enabled: modules[key] !== false }; + return { key, enabled: safeModules[key] !== false }; }); - const customItems = Array.isArray(modules.customItems) - ? modules.customItems + const customItems = Array.isArray(safeModules.customItems) + ? safeModules.customItems.filter( + (ci) => ci && typeof ci === 'object' && !Array.isArray(ci), + ) : [];🤖 Prompt for AI Agents