Skip to content

feat: integrate Page/Dashboard editors into Console with design routes#788

Merged
hotlong merged 7 commits intomainfrom
copilot/integrate-page-dashboard-editors
Feb 23, 2026
Merged

feat: integrate Page/Dashboard editors into Console with design routes#788
hotlong merged 7 commits intomainfrom
copilot/integrate-page-dashboard-editors

Conversation

Copy link
Contributor

Copilot AI commented Feb 23, 2026

Wire PageCanvasEditor and DashboardEditor from plugin-designer into Console as full editing routes with auto-save, export/import, and edit button entry points from read-only views.

Routes & Lazy Imports

  • /design/page/:pageNamePageDesignPage wrapping PageCanvasEditor
  • /design/dashboard/:dashboardNameDashboardDesignPage wrapping DashboardEditor
  • Both lazy-loaded; MAX_CHUNK_COUNT bumped 20→22
  • design excluded from objectNameFromPath detection to prevent false object lookups

Edit Buttons on Views

  • PageView and DashboardView gain a ✏️ Edit button navigating to the corresponding design route via relative path (../design/page/:pageName)

Design Pages

  • PageDesignPage / DashboardDesignPage: load schema from useMetadata(), init editor state, auto-save onChange via dataSource.update('sys_page'|'sys_dashboard', ...), JSON export downloads file, back button navigates -1
  • Ctrl+S / Cmd+S keyboard shortcut to explicitly save with toast confirmation
  • Undo/redo and preview mode provided by the built-in editor hooks (useUndoRedo, Ctrl+Z/Ctrl+Y)

Storybook

  • Designers/PageCanvasEditor — Default, WithComponents, ReadOnly
  • Designers/DashboardEditor — Default, WithWidgets, ReadOnly

Tests

  • 12 new tests across PageDesignPage.test.tsx and DashboardDesignPage.test.tsx (render, 404, back nav, schema passthrough, onChange propagation, Ctrl+S keyboard save)
  • All 607 existing console tests pass, 235 plugin-designer tests unaffected

ROADMAP

  • Console Integration section updated with design page entries, Ctrl+S shortcut, and updated test counts
Original prompt

This section details on the original issue you should resolve

<issue_title>将 Page/Canvas Editor 和 Dashboard Editor 集成到 Console,支持完整编辑体验</issue_title>
<issue_description>为实现 #744 的交付要求,需要将已开发的 PageCanvasEditor 和 DashboardEditor 集成到 Console 应用,打通所见即所得页面/仪表盘的设计与编辑体验。

目标

  1. 页面和仪表盘可视化编辑器可直接在 Console(管理后台)访问与修改。
  2. 支持页面/仪表盘的创建、编辑、保存,同步至元数据。
  3. 编辑操作后自动运行相关测试并更新 Roadmap。

需要开发的细节

1. Console 路由与入口

  • 新增 /design/page/:pageName 路由,集成 PageCanvasEditor
  • 新增 /design/dashboard/:dashboardName 路由,集成 DashboardEditor
  • 在页面、仪表盘的只读视图(PageView/DashboardView)增加"编辑"按钮,跳转到对应设计器
  • Console 左侧导航自动增补"页面设计"与"仪表盘设计"入口(可选)

2. 编辑与数据持久化

  • 编辑器 onChange 时自动通过 dataSource/API 保存 schema(PageSchema/DashboardSchema)
  • 从后端拉取 schema 时可回显到编辑器
  • 支持 JSON Schema 导入导出,兼容与 @objectstack/spec 协议
  • 支持编辑历史(undo/redo)、预览、以及键盘操作(保存、撤销)

3. Navigation 结构联动

  • 新增/编辑 Page 或 Dashboard 后,可选同步到导航结构(navigation designer)
  • 删除页面/仪表盘时同步移除导航项

4. Storybook & 测试

  • 为 PageCanvasEditor、DashboardEditor 新增 Storybook 用例
  • 补充/完善 E2E 测试场景:从 Console 编辑到保存,确保测试全部通过

5. ROADMAP 文档同步

  • 集成、回归发布时在 ROADMAP.md 对应部分对进度和特性进行更新

负责人:@hotlong

如遇实现细节方案冲突,可先发起讨论或新开 issue 进行技术选型分析。</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link

vercel bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 23, 2026 0:29am
objectui-demo Error Error Feb 23, 2026 0:29am
objectui-storybook Error Error Feb 23, 2026 0:29am

Request Review

…h design routes, edit buttons, storybook stories, and tests

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…design page lazy imports

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Integrate Page/Canvas Editor and Dashboard Editor into Console feat: integrate Page/Dashboard editors into Console with design routes Feb 23, 2026
Copilot AI requested a review from hotlong February 23, 2026 11:32
@hotlong
Copy link
Contributor

hotlong commented Feb 23, 2026

@copilot 仔细评估是不是完成了任务的所有要求?

…esign routes from object path detection

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot 仔细评估是不是完成了任务的所有要求?

经过仔细评估,以下是对 issue 所有要求的完成情况:

✅ 1. Console 路由与入口

  • /design/page/:pageName 路由 ✅
  • /design/dashboard/:dashboardName 路由 ✅
  • PageView/DashboardView 编辑按钮 ✅
  • 左侧导航入口(标记为可选)— 未实现

✅ 2. 编辑与数据持久化

  • onChange 自动保存 ✅
  • 从后端拉取 schema 回显到编辑器 ✅
  • JSON Schema 导入导出 ✅
  • Undo/Redo(编辑器内置 Ctrl+Z/Ctrl+Y)✅
  • 预览模式 ✅
  • Ctrl+S/Cmd+S 保存快捷键 ✅ (commit 5a7bafa 新增)

⏳ 3. Navigation 结构联动

  • 新增/编辑后同步到导航结构 — 未实现(需要 NavigationDesigner API 集成,建议开新 issue 进行技术选型)
  • 删除时同步移除导航项 — 未实现(同上,需要与 NavigationDesigner 深度集成)

✅ 4. Storybook & 测试

  • Storybook stories(PageCanvasEditor + DashboardEditor)✅
  • 12 个单元测试(含 Ctrl+S 保存测试)✅
  • E2E 测试 — 未新增 Playwright 场景(现有 console-rendering E2E 框架可扩展)

✅ 5. ROADMAP 同步

  • ROADMAP.md Console Integration 部分已更新 ✅

此外还修复了 design 路由未被排除在 objectNameFromPath 检测之外的问题(5a7bafa)。

总结: 核心功能(路由、编辑、持久化、快捷键、测试、Storybook)已全部完成。Navigation 结构联动(Section 3)由于需要与 NavigationDesigner 深度集成,建议作为独立 issue 跟进。

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@hotlong hotlong marked this pull request as ready for review February 23, 2026 12:11
Copilot AI review requested due to automatic review settings February 23, 2026 12:11
@github-actions github-actions bot added documentation Improvements or additions to documentation plugin apps tests labels Feb 23, 2026
@hotlong hotlong merged commit f0b5a1f into main Feb 23, 2026
4 of 11 checks passed
@github-actions
Copy link
Contributor

❌ Console Performance Budget

Metric Value Budget
Main entry (gzip) ** KB** KB
Entry file ``
Status FAIL

📦 Bundle Size Report

Package Size Gzipped
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 7.36KB 1.78KB
auth (ForgotPasswordForm.js) 4.00KB 1.44KB
auth (LoginForm.js) 4.20KB 1.45KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 5.66KB 1.59KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (createAuthClient.js) 2.78KB 0.98KB
auth (createAuthenticatedFetch.js) 1.24KB 0.60KB
auth (index.js) 1.18KB 0.51KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 1.57KB 0.57KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (index.js) 1.16KB 0.50KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 1854.03KB 434.20KB
core (index.js) 0.86KB 0.34KB
create-plugin (index.js) 10.13KB 3.17KB
data-objectstack (index.js) 38.87KB 9.57KB
fields (index.js) 92.37KB 18.57KB
i18n (i18n.js) 2.03KB 0.77KB
i18n (index.js) 1.79KB 0.72KB
i18n (provider.js) 3.21KB 1.09KB
layout (index.js) 97.78KB 25.90KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (index.js) 1.19KB 0.53KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.11KB 0.87KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.85KB 0.40KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 0.99KB 0.49KB
plugin-aggrid (AddressField-C9Ij3zMt.js) 3.05KB 0.76KB
plugin-aggrid (AgGridImpl-Dpo8MmN2.js) 7.04KB 2.33KB
plugin-aggrid (AutoNumberField-C0FkjOVx.js) 0.28KB 0.27KB
plugin-aggrid (FileField-DtRpVqal.js) 5.90KB 2.10KB
plugin-aggrid (FormulaField--J9ATNxo.js) 0.52KB 0.38KB
plugin-aggrid (GeolocationField-C71taYLr.js) 4.46KB 1.50KB
plugin-aggrid (GridField-BqYbyBXM.js) 1.71KB 0.68KB
plugin-aggrid (LocationField-FQnepdz1.js) 0.93KB 0.54KB
plugin-aggrid (MasterDetailField-CgOz_IlL.js) 3.86KB 1.18KB
plugin-aggrid (ObjectAgGridImpl-DtzS-_Cz.js) 920.44KB 203.73KB
plugin-aggrid (ObjectField-DT9YUBF2.js) 1.61KB 0.77KB
plugin-aggrid (QRCodeField-QB634UcF.js) 3.38KB 1.23KB
plugin-aggrid (RichTextField-VxIHKJkl.js) 1.16KB 0.59KB
plugin-aggrid (SignatureField-Dw-QbW9T.js) 3.33KB 1.29KB
plugin-aggrid (SummaryField-DrGrsHtS.js) 0.48KB 0.37KB
plugin-aggrid (UserField-B6r7XwoX.js) 2.44KB 0.94KB
plugin-aggrid (VectorField-BOaVtgLd.js) 0.79KB 0.45KB
plugin-aggrid (index-KiWwqVL5.js) 19.95KB 5.03KB
plugin-aggrid (index.js) 0.22KB 0.16KB
plugin-ai (index.js) 25.35KB 6.39KB
plugin-calendar (index.js) 49.03KB 13.35KB
plugin-charts (AdvancedChartImpl-GErbMD6M.js) 126.98KB 26.25KB
plugin-charts (BarChart-C_I0OFbj.js) 542.77KB 135.30KB
plugin-charts (ChartImpl-CnD20l5F.js) 3.19KB 1.10KB
plugin-charts (index-Byep0tj1.js) 15.23KB 4.49KB
plugin-charts (index.js) 0.19KB 0.16KB
plugin-chatbot (index.js) 1140.69KB 333.05KB
plugin-dashboard (index.js) 156.26KB 40.27KB
plugin-designer (index.js) 366.26KB 71.00KB
plugin-detail (AddressField-OdUOI_M-.js) 2.98KB 0.75KB
plugin-detail (AutoNumberField-BxnFqllo.js) 0.28KB 0.26KB
plugin-detail (AvatarField-GYEITc5e.js) 2.62KB 1.07KB
plugin-detail (BooleanField-D6a3Az7d.js) 1.13KB 0.51KB
plugin-detail (CodeField-CSQwuVHZ.js) 0.74KB 0.47KB
plugin-detail (ColorField-B_fX6R-v.js) 1.23KB 0.54KB
plugin-detail (CurrencyField-DOil_fAf.js) 1.48KB 0.73KB
plugin-detail (DateField-CIRY8vl3.js) 0.54KB 0.37KB
plugin-detail (DateTimeField-ft4k-k1s.js) 0.71KB 0.42KB
plugin-detail (EmailField-BIC9E_YO.js) 0.81KB 0.48KB
plugin-detail (FileField-DzPMbrR1.js) 5.50KB 1.94KB
plugin-detail (FormulaField-CJkkwIK8.js) 0.51KB 0.38KB
plugin-detail (GeolocationField-C2sbCul5.js) 3.71KB 1.21KB
plugin-detail (GridField-B3yJymJF.js) 1.68KB 0.68KB
plugin-detail (ImageField-C1hU5fbo.js) 2.83KB 1.14KB
plugin-detail (LocationField-CYpuZrM0.js) 0.95KB 0.53KB
plugin-detail (LookupField-DDekVxJ9.js) 4.00KB 1.38KB
plugin-detail (MasterDetailField-CBHUmErz.js) 3.39KB 1.00KB
plugin-detail (NumberField-Dom6iSWP.js) 0.68KB 0.44KB
plugin-detail (ObjectField-Dj-UUch1.js) 1.64KB 0.76KB
plugin-detail (PasswordField-QyK-VJTy.js) 1.21KB 0.62KB
plugin-detail (PercentField-ARTev1An.js) 1.89KB 0.84KB
plugin-detail (PhoneField-Dk-LpR_j.js) 0.80KB 0.48KB
plugin-detail (QRCodeField-BHfcV2Ll.js) 2.35KB 0.91KB
plugin-detail (RatingField-BFpbdPhx.js) 1.62KB 0.66KB
plugin-detail (RichTextField-C71jmrpX.js) 1.15KB 0.59KB
plugin-detail (SelectField-B2yQsJdl.js) 0.90KB 0.49KB
plugin-detail (SignatureField-gulCJiBb.js) 2.97KB 1.10KB
plugin-detail (SliderField-DAzRylmz.js) 1.00KB 0.49KB
plugin-detail (SummaryField-ugYPYxjP.js) 0.47KB 0.36KB
plugin-detail (TextAreaField-DTLGDKpM.js) 1.09KB 0.59KB
plugin-detail (TextField-BASDk9EJ.js) 0.82KB 0.43KB
plugin-detail (TimeField-CAzBittR.js) 0.50KB 0.34KB
plugin-detail (UrlField-Cq5HOCxZ.js) 0.98KB 0.52KB
plugin-detail (UserField-CoyZW5bV.js) 2.33KB 0.90KB
plugin-detail (VectorField-CKg9jdGa.js) 0.78KB 0.44KB
plugin-detail (index-CYoSbrlZ.js) 1852.89KB 433.40KB
plugin-detail (index.js) 0.90KB 0.51KB
plugin-editor (MonacoImpl-hfdmoz6k.js) 18.15KB 5.59KB
plugin-editor (index-CuYbY6xb.js) 10.10KB 3.32KB
plugin-editor (index.js) 0.19KB 0.15KB
plugin-form (index.js) 66.73KB 13.98KB
plugin-gantt (index.js) 244.31KB 58.97KB
plugin-grid (index.js) 100.75KB 26.59KB
plugin-kanban (KanbanEnhanced-BzMzUMfy.js) 32.31KB 9.09KB
plugin-kanban (KanbanImpl-BrDbqzQA.js) 14.19KB 4.18KB
plugin-kanban (index-CotqYB1p.js) 31.26KB 9.21KB
plugin-kanban (index.js) 0.42KB 0.26KB
plugin-kanban (sortable.esm-CNNHgHk5.js) 71.43KB 18.99KB
plugin-list (index.js) 1819.90KB 428.20KB
plugin-map (index.js) 130.47KB 31.67KB
plugin-map (maplibre-gl-DSpYxujd.js) 1416.27KB 302.72KB
plugin-markdown (MarkdownImpl-E6vCIsNj.js) 256.68KB 64.45KB
plugin-markdown (index-Dr10kVgr.js) 9.63KB 3.17KB
plugin-markdown (index.js) 0.19KB 0.15KB
plugin-report (index.js) 65.85KB 13.42KB
plugin-timeline (index.js) 108.21KB 25.15KB
plugin-view (index.js) 135.39KB 34.38KB
plugin-workflow (index.js) 82.94KB 17.11KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 8.21KB 2.56KB
react (index.js) 0.66KB 0.38KB
tenant (TenantContext.js) 0.31KB 0.25KB
tenant (TenantGuard.js) 1.04KB 0.43KB
tenant (TenantProvider.js) 2.76KB 0.98KB
tenant (TenantScopedQuery.js) 0.77KB 0.44KB
tenant (index.js) 0.75KB 0.38KB
tenant (resolver.js) 2.64KB 0.76KB
tenant (useTenant.js) 0.50KB 0.32KB
tenant (useTenantBranding.js) 0.62KB 0.39KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.73KB 0.39KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.25KB 0.58KB
types (layout.js) 0.20KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (tenant.js) 0.20KB 0.18KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.20KB 0.18KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB
types (workflow.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR integrates the PageCanvasEditor and DashboardEditor designer components from @object-ui/plugin-designer into the Console application, enabling full WYSIWYG editing of pages and dashboards. The integration follows the established lazy-loading pattern for route-based code splitting and provides a complete editing experience with auto-save, keyboard shortcuts, JSON export/import, and edit button entry points from read-only views.

Changes:

  • Added two new design routes (/design/page/:pageName and /design/dashboard/:dashboardName) that wrap the editor components with save logic, keyboard shortcuts (Ctrl+S), and export/import handlers
  • Added Edit buttons to PageView and DashboardView that navigate to the corresponding design routes using relative paths
  • Created Storybook stories for both editor components with Default, WithComponents/WithWidgets, and ReadOnly variants

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
packages/plugin-designer/src/PageCanvasEditor.stories.tsx New Storybook stories for PageCanvasEditor with 3 variants (Default, WithComponents, ReadOnly)
packages/plugin-designer/src/DashboardEditor.stories.tsx New Storybook stories for DashboardEditor with 3 variants (Default, WithWidgets, ReadOnly)
apps/console/src/pages/PageDesignPage.tsx New page component wrapping PageCanvasEditor with auto-save, Ctrl+S shortcut, export/import handlers, and 404 handling
apps/console/src/pages/DashboardDesignPage.tsx New page component wrapping DashboardEditor with auto-save, Ctrl+S shortcut, export/import handlers, and 404 handling
apps/console/src/components/PageView.tsx Added Edit button in hover-revealed toolbar that navigates to design route
apps/console/src/components/DashboardView.tsx Added Edit button in header that navigates to design route
apps/console/src/__tests__/console-load-performance.test.tsx Updated MAX_CHUNK_COUNT from 20 to 22 to account for two new lazy-loaded design pages
apps/console/src/__tests__/PageDesignPage.test.tsx New test file with 6 tests covering rendering, 404, back navigation, schema passthrough, onChange, and Ctrl+S save
apps/console/src/__tests__/DashboardDesignPage.test.tsx New test file with 6 tests covering rendering, 404, back navigation, schema passthrough, onChange, and Ctrl+S save
apps/console/src/App.tsx Added lazy imports for design pages, registered routes, and excluded 'design' from objectNameFromPath detection
ROADMAP.md Updated console integration section with design page features, Ctrl+S shortcut, Storybook story count (78→80), and test counts

Comment on lines +56 to +67
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveSchema(schemaRef.current).then((ok) => {
if (ok) toast.success('Page saved');
});
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [saveSchema]);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyboard event listener for Ctrl+S is attached to the global window object without checking if the user is focused on an input element or contenteditable area. This means pressing Ctrl+S while typing in a text field within the PageCanvasEditor will trigger a save, which could be disruptive to the user's workflow. Consider adding a check to prevent the shortcut from firing when the user is typing, similar to the pattern used in KeyboardShortcutsDialog.tsx where it checks if the target is an input, textarea, or contenteditable element.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +74
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveSchema(schemaRef.current).then((ok) => {
if (ok) toast.success('Dashboard saved');
});
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [saveSchema]);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keyboard event listener for Ctrl+S is attached to the global window object without checking if the user is focused on an input element or contenteditable area. This means pressing Ctrl+S while typing in a text field within the DashboardEditor will trigger a save, which could be disruptive to the user's workflow. Consider adding a check to prevent the shortcut from firing when the user is typing, similar to the pattern used in KeyboardShortcutsDialog.tsx where it checks if the target is an input, textarea, or contenteditable element.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +55
<button
type="button"
onClick={() => navigate(`../design/page/${pageName}`)}
className="inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground"
data-testid="page-edit-button"
>
<Pencil className="h-3.5 w-3.5" />
Edit
</button>
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Edit button lacks an aria-label attribute. While the button has visible text ("Edit") and an icon, adding an explicit aria-label would improve accessibility for screen reader users by providing clearer context, such as "Edit page" or "Edit page {pageName}". This is especially important since the button appears in a hover-revealed toolbar where context might not be immediately clear.

Copilot uses AI. Check for mistakes.
a.href = url;
a.download = `${pageName || 'page'}.json`;
a.click();
URL.revokeObjectURL(url);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL.revokeObjectURL(url) call happens immediately after the download is triggered with a.click(), which may cause the download to fail in some browsers if the browser hasn't started the download yet. Consider wrapping the revoke call in a setTimeout with a small delay (e.g., 100ms) to ensure the browser has time to begin the download before the URL is revoked. This is a common best practice for programmatic downloads to avoid race conditions.

Suggested change
URL.revokeObjectURL(url);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);

Copilot uses AI. Check for mistakes.
a.href = url;
a.download = `${dashboardName || 'dashboard'}.json`;
a.click();
URL.revokeObjectURL(url);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL.revokeObjectURL(url) call happens immediately after the download is triggered with a.click(), which may cause the download to fail in some browsers if the browser hasn't started the download yet. Consider wrapping the revoke call in a setTimeout with a small delay (e.g., 100ms) to ensure the browser has time to begin the download before the URL is revoked. This is a common best practice for programmatic downloads to avoid race conditions.

Suggested change
URL.revokeObjectURL(url);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +97
const page = pages?.find((p: any) => p.name === pageName);

const [schema, setSchema] = useState<PageSchema>(
() => (page as PageSchema) || { type: 'page', name: pageName ?? '', title: pageName ?? '', children: [] },
);
const schemaRef = useRef(schema);
schemaRef.current = schema;

const saveSchema = useCallback(
async (toSave: PageSchema) => {
try {
if (dataSource) {
await dataSource.update('sys_page', pageName!, toSave);
return true;
}
} catch {
// Save errors are non-blocking; user can retry via export
}
return false;
},
[dataSource, pageName],
);

const handleChange = useCallback(
async (updated: PageSchema) => {
setSchema(updated);
await saveSchema(updated);
},
[saveSchema],
);

// Ctrl+S / Cmd+S keyboard shortcut to explicitly save
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveSchema(schemaRef.current).then((ok) => {
if (ok) toast.success('Page saved');
});
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [saveSchema]);

const handleExport = useCallback(
(exported: PageSchema) => {
const blob = new Blob([JSON.stringify(exported, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${pageName || 'page'}.json`;
a.click();
URL.revokeObjectURL(url);
toast.success('Page schema exported');
},
[pageName],
);

const handleImport = useCallback(
(imported: PageSchema) => {
toast.success('Page schema imported');
handleChange(imported);
},
[handleChange],
);

if (!page) {
return (
<div className="h-full flex items-center justify-center text-muted-foreground">
Page &quot;{pageName}&quot; not found
</div>
);
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component initializes schema state with a fallback value even when the page is not found (line 27), but then returns early with a 404 message (lines 91-97). This means the state initialization, useCallback hooks, and useEffect hook all execute even when the page doesn't exist. While functionally harmless since the 404 UI is returned before rendering the editor, this pattern wastes computation. Consider moving the early return check before state initialization and hook declarations, or use a loading state pattern to defer hook execution until page existence is confirmed.

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +104
const dashboard = dashboards?.find((d: any) => d.name === dashboardName);

const [schema, setSchema] = useState<DashboardSchema>(
() =>
(dashboard as DashboardSchema) || {
type: 'dashboard',
name: dashboardName ?? '',
title: dashboardName ?? '',
columns: 2,
widgets: [],
},
);
const schemaRef = useRef(schema);
schemaRef.current = schema;

const saveSchema = useCallback(
async (toSave: DashboardSchema) => {
try {
if (dataSource) {
await dataSource.update('sys_dashboard', dashboardName!, toSave);
return true;
}
} catch {
// Save errors are non-blocking; user can retry via export
}
return false;
},
[dataSource, dashboardName],
);

const handleChange = useCallback(
async (updated: DashboardSchema) => {
setSchema(updated);
await saveSchema(updated);
},
[saveSchema],
);

// Ctrl+S / Cmd+S keyboard shortcut to explicitly save
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveSchema(schemaRef.current).then((ok) => {
if (ok) toast.success('Dashboard saved');
});
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [saveSchema]);

const handleExport = useCallback(
(exported: DashboardSchema) => {
const blob = new Blob([JSON.stringify(exported, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${dashboardName || 'dashboard'}.json`;
a.click();
URL.revokeObjectURL(url);
toast.success('Dashboard schema exported');
},
[dashboardName],
);

const handleImport = useCallback(
(imported: DashboardSchema) => {
toast.success('Dashboard schema imported');
handleChange(imported);
},
[handleChange],
);

if (!dashboard) {
return (
<div className="h-full flex items-center justify-center text-muted-foreground">
Dashboard &quot;{dashboardName}&quot; not found
</div>
);
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component initializes schema state with a fallback value even when the dashboard is not found (lines 26-35), but then returns early with a 404 message (lines 98-104). This means the state initialization, useCallback hooks, and useEffect hook all execute even when the dashboard doesn't exist. While functionally harmless since the 404 UI is returned before rendering the editor, this pattern wastes computation. Consider moving the early return check before state initialization and hook declarations, or use a loading state pattern to defer hook execution until dashboard existence is confirmed.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +45
const saveSchema = useCallback(
async (toSave: PageSchema) => {
try {
if (dataSource) {
await dataSource.update('sys_page', pageName!, toSave);
return true;
}
} catch {
// Save errors are non-blocking; user can retry via export
}
return false;
},
[dataSource, pageName],
);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The saveSchema function swallows all errors silently without logging or providing feedback to the user. When auto-save fails, users have no indication that their changes weren't persisted. Consider adding at minimum a console.error for debugging and optionally a toast.error for user notification, while still allowing retry via export as noted in the comment.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +52
const saveSchema = useCallback(
async (toSave: DashboardSchema) => {
try {
if (dataSource) {
await dataSource.update('sys_dashboard', dashboardName!, toSave);
return true;
}
} catch {
// Save errors are non-blocking; user can retry via export
}
return false;
},
[dataSource, dashboardName],
);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The saveSchema function swallows all errors silently without logging or providing feedback to the user. When auto-save fails, users have no indication that their changes weren't persisted. Consider adding at minimum a console.error for debugging and optionally a toast.error for user notification, while still allowing retry via export as noted in the comment.

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +125
export function PageDesignPage() {
const navigate = useNavigate();
const { pageName } = useParams<{ pageName: string }>();
const dataSource = useAdapter();
const { pages } = useMetadata();

const page = pages?.find((p: any) => p.name === pageName);

const [schema, setSchema] = useState<PageSchema>(
() => (page as PageSchema) || { type: 'page', name: pageName ?? '', title: pageName ?? '', children: [] },
);
const schemaRef = useRef(schema);
schemaRef.current = schema;

const saveSchema = useCallback(
async (toSave: PageSchema) => {
try {
if (dataSource) {
await dataSource.update('sys_page', pageName!, toSave);
return true;
}
} catch {
// Save errors are non-blocking; user can retry via export
}
return false;
},
[dataSource, pageName],
);

const handleChange = useCallback(
async (updated: PageSchema) => {
setSchema(updated);
await saveSchema(updated);
},
[saveSchema],
);

// Ctrl+S / Cmd+S keyboard shortcut to explicitly save
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveSchema(schemaRef.current).then((ok) => {
if (ok) toast.success('Page saved');
});
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [saveSchema]);

const handleExport = useCallback(
(exported: PageSchema) => {
const blob = new Blob([JSON.stringify(exported, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${pageName || 'page'}.json`;
a.click();
URL.revokeObjectURL(url);
toast.success('Page schema exported');
},
[pageName],
);

const handleImport = useCallback(
(imported: PageSchema) => {
toast.success('Page schema imported');
handleChange(imported);
},
[handleChange],
);

if (!page) {
return (
<div className="h-full flex items-center justify-center text-muted-foreground">
Page &quot;{pageName}&quot; not found
</div>
);
}

return (
<div className="h-full flex flex-col" data-testid="page-design-page">
<div className="flex items-center gap-3 border-b px-4 py-3 sm:px-6">
<button
type="button"
onClick={() => navigate(-1)}
className="rounded p-1.5 text-muted-foreground hover:text-foreground"
aria-label="Go back"
data-testid="page-design-back"
>
<ArrowLeft className="h-4 w-4" />
</button>
<h1 className="text-lg font-semibold tracking-tight truncate">
Edit Page: {(page as any).label || (page as any).title || pageName}
</h1>
</div>
<div className="flex-1 overflow-auto p-4 sm:p-6">
<PageCanvasEditor
schema={schema}
onChange={handleChange}
onExport={handleExport}
onImport={handleImport}
/>
</div>
</div>
);
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PageDesignPage and DashboardDesignPage components contain significant code duplication. They share identical patterns for: state management (schema + schemaRef), save logic, keyboard shortcuts (Ctrl+S), export/import handlers, 404 rendering, header layout with back button, and editor wrapping structure. Consider extracting a generic DesignPageWrapper component or a custom hook (useDesignPageLogic) to reduce duplication and improve maintainability. The wrapper could accept the schema type, editor component, entity name, and metadata accessor as parameters.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apps documentation Improvements or additions to documentation plugin tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

将 Page/Canvas Editor 和 Dashboard Editor 集成到 Console,支持完整编辑体验

3 participants