Skip to content

feat: multiplayer collaboration(Ospp 2025) - #1564

Merged
hexqi merged 36 commits into
opentiny:ospp-2025/multiplayer-collaborationfrom
ztygod:ospp-2025/multiplayer-collaboration
Oct 29, 2025
Merged

feat: multiplayer collaboration(Ospp 2025)#1564
hexqi merged 36 commits into
opentiny:ospp-2025/multiplayer-collaborationfrom
ztygod:ospp-2025/multiplayer-collaboration

Conversation

@ztygod

@ztygod ztygod commented Jul 28, 2025

Copy link
Copy Markdown

English | 简体中文

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • Built its own designer, fully self-validated

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe: ospp开源之夏多人协作项目

Background and solution

搭建版本管理界面
image

What is the current behavior?

  1. 可以根据作者或者commit hash值索引提交记录
  2. 查看commit的具体内容
  3. 与现在版本的内容进行对比
  4. 回滚版本
  5. 从当前commit创建分支
  6. 创建新的分支或者标签

Issue Number: N/A

What is the new behavior?

同上

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Full Version Control UI: branch & commit management, timeline, diffs, commit details, tag/branch/commit creation and revert.
    • Real-time collaboration toolkit: multi-user editing, shared cursors, presence avatars, collaborative Monaco editor, drag/selection sync.
  • Improvements

    • Canvas and toolbars surface collaboration indicators and quick Version Control access.
    • Live sync for styles, props, methods, selections, node operations, and schema changes.
  • Documentation

    • Added guides for collaborative composables and version-control usage.
  • Chores

    • Mock server, package manifests and build configs updated to support new features.

@coderabbitai

ghost commented Jul 28, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a Version Control plugin and a Yjs-based multi-person collaboration stack, two collab UI packages (cursor, avatar), many Vue components/composables, domain models/services/repositories for VCS, real-time managers/operation handlers, mock-server endpoints/data, TS path aliases, and integrations across canvas, settings, layout, and toolbars.

Changes

Cohort / File(s) Summary
Build & TS Paths
packages/build/vite-config/.../devAliasPlugin.js, tsconfig.app.json
Added dev aliases and TS path mappings for multi-person-collaboration, version-control, and collab-ui packages; enabled experimental decorators and metadata.
Design Core & Layout
packages/design-core/*, packages/layout/src/defaultLayout.js
Added workspace deps, re-exports for VersionControl/Cursor/Avatar, registered plugin and collabUI entries, and placed VersionControl & Avatar into layout toolbars.
Registry & Hooks
packages/register/src/constants.ts, packages/register/src/hooks.ts
Added META_APP keys (VersionControl, Avatar) and HOOK_NAME.useRealtimeCollab with exported useRealtimeCollab.
Version Control Package
packages/plugins/versioncontrol/**
New plugin package: package.json, Vite config, VersionManager, domain models (Branch/Commit/Schema), services (domain + app), repositories, strategies (merge/conflict/diff), types, utils (sha1, Memoize, formatDateTime), many Vue UI components, composables, tests, README, and base styles.
Multi‑Person Collaboration Package
packages/multi-person-collaboration/**
New collaboration package: package.json, Vite config, README, .gitignore, composables (useYjs, useAwareness, useCollabSchema, useCollabCursor, useCollabMonaco, useCollabTree), managers (Doc/Provider/Schema), OperationHandler, NodeSchemaModel, utilities (toYjs/fromYjs/sanitize), types, config constants, and tests.
Collab UI Packages
packages/collab-ui/cursor/*, packages/collab-ui/avatar/*
New cursor and avatar packages with metadata, entries, Vite configs, components, viewport composable, and builds.
Canvas & Component Integrations
packages/canvas/container/src/*, .../components/CanvasAction.vue, .../composables/useMultiSelect.ts, .../container.ts
Integrated realtime hooks: cursor rendering, remote selection syncing, broadcasting selection updates, and invoking shared-node insert/delete/move flows.
Settings, Props, Events, Toolbars
packages/settings/**, packages/plugins/script/*, packages/toolbars/collaboration/src/Main.vue, packages/plugins/script/src/js/method.ts
Added useRealtimeCollab calls to sync style/props/methods/attributes, UI CSS for remote selection, and toolbar action to open VersionControl.
Mock Server & APIs
mockServer/**
Added branch/commit mock services, seed data, /app-center/api/version routes, Yjs WebSocket server integration, and runtime deps for yjs/y-websocket.
Tests
packages/*/test/**
Large suite of unit/component tests for composables, managers, components, utils, and mock-server behaviors.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as VersionControl UI
  participant AppSvc as Commit/Branch AppService
  participant Repo as HTTP Repositories
  participant Domain as Domain Services
  participant Diff as SchemaDiffResolver

  User->>UI: Create commit (branchId, message, schema, type)
  UI->>AppSvc: createCommit(...)
  AppSvc->>Repo: load branch & commits
  AppSvc->>Domain: createCommit(...) -> Domain computes hash/stats
  Domain->>Diff: calculateDiff(oldSchema, newSchema)
  Diff-->>Domain: delta/stats
  Domain-->>AppSvc: Commit instance
  AppSvc->>Repo: save commit, update branch head
  AppSvc-->>UI: return created commit
Loading
sequenceDiagram
  autonumber
  participant Editor as Local Canvas/Editor
  participant Collab as useYjs/useAwareness
  participant Provider as WebSocket Provider
  participant SchemaMgr as SchemaManager / OperationHandler
  participant Peers as Remote Clients

  Editor->>Collab: init(roomId, wsUrl)
  Collab->>Provider: connect
  Provider-->>Collab: sync
  Collab->>SchemaMgr: createSchema(roomId, provider)
  Editor->>SchemaMgr: perform insert/move/delete/update
  SchemaMgr->>Provider: apply Y ops / __app_events__
  Provider-->>Peers: broadcast updates
  Peers-->>Editor: awareness updates (cursor/selection/drag)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Potential attention areas:

  • Merge/conflict resolution correctness and edge cases (MergeResolver, ConflictResolver).
  • Concurrency and initial-sync correctness in SchemaManager / OperationHandler (Yjs observers, app_events handling).
  • Schema diff/stats accuracy (SchemaDiffResolver, SchemaStatsCalculator).
  • Repository API shapes and HTTP error handling.
  • Large UI surface: prop/emit and v-model consistency across many components.
  • Tests relying on hard-coded values (ws://localhost:9090) and mock-server behavior.

Poem

我是小兔子在草间,
光标随风绕屏旋。
提交分支像胡萝卜串,
头像与游标共蹦跃。
协作上线,大家来分享。 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: multiplayer collaboration(Ospp 2025)" is directly related to the main content of the changeset. The PR introduces a comprehensive multiplayer collaboration infrastructure built on Yjs, including real-time synchronization, multi-user awareness (cursors, avatars), and a complete version control plugin system with branch/commit management. While the title uses a broad descriptor rather than listing specific components like "version control" or "real-time schema sync," it accurately reflects the primary objective of enabling collaborative editing across the TinyEngine platform.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added documentation Improvements or additions to documentation ospp ospp labels Jul 28, 2025

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 21

🔭 Outside diff range comments (1)
packages/plugins/materials/src/meta/component/src/Main.vue (1)

71-101: Type safety improved but consider fixing type incompatibilities.

The function now has proper typing, but the multiple @ts-ignore comments indicate underlying type issues.

Instead of suppressing type errors, consider fixing the root cause:

// Define a more accurate type that matches your data structure
interface MaterialComponent {
  groupId?: string
  group: string
  groupName?: string
  label?: Record<string, string>
  children: Array<{
    name?: Record<string, string>
    component: string
    snippetName?: string
    icon?: string
    hidden?: boolean
  }>
}

// Then update the function signature
const fetchComponents = (components: MaterialComponent[], name: string) => {
  // ... no need for @ts-ignore
}
♻️ Duplicate comments (1)
packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1)

186-710: Style duplication confirmed - extract shared dialog styles.

This file contains nearly identical dialog styles to VersionDiffDialog.vue, confirming the code duplication issue. Please refer to the refactor suggestion in the previous file review to extract shared dialog styles to a common LESS file.

🧹 Nitpick comments (18)
packages/plugins/page/src/Tree.vue (1)

98-105: Consider moving TreeNode to a dedicated typings module and adding a generic for rawData

If this type will be consumed by multiple files, placing it in packages/types/tree.ts (or similar) keeps the component lean and avoids circular-dep risks.
Also, replacing rawData?: any with a generic parameter improves type-safety:

-export interface TreeNode {
+export interface TreeNode<T = unknown> {
   id: string | number
   label: string
   parentId?: string | number
   level: number
   collapsed?: boolean
-  rawData?: any
+  rawData?: T
 }

Usage inside this file remains the same (TreeNode defaults to unknown).

packages/plugins/page/src/PageTree.vue (1)

133-133: Consider error handling for the page list fetch.

The removal of await in onMounted means any errors from refreshPageList won't be caught in this context. While this might be intentional, consider if error handling is needed for the initial page list fetch.

If error handling is desired, consider wrapping the call:

 onMounted(() => {
-  refreshPageList(getAppId())
+  refreshPageList(getAppId()).catch((error) => {
+    console.error('Failed to load page list:', error)
+    // Add appropriate error notification if needed
+  })
   subscriber = subscribe({
packages/build/vite-plugin-meta-comments/src/utils.js (1)

163-177: Function name doesn't match implementation

The function name getCurrentScopeBodyAndVarId suggests it returns both body and variable ID, but it only returns the body. Consider renaming to getCurrentScopeBody or updating the implementation to also return the variable ID.

Option 1: Rename the function:

-function getCurrentScopeBodyAndVarId(path) {
+function getCurrentScopeBody(path) {

Option 2: Return both body and variable ID:

 function getCurrentScopeBodyAndVarId(path) {
   // Handle variable declarator pattern
   if (
     path.parentPath &&
     path.parentPath.isVariableDeclarator() &&
+    path.parentPath.node.id &&
+    path.parentPath.node.id.name &&
     path.parentPath.parentPath &&
     path.parentPath.parentPath.isVariableDeclaration() &&
     path.parentPath.parentPath.parent &&
     Array.isArray(path.parentPath.parentPath.parent.body)
   ) {
-    return path.parentPath.parentPath.parent.body
+    return {
+      body: path.parentPath.parentPath.parent.body,
+      varId: path.parentPath.node.id.name
+    }
   }
 
-  return null
+  return { body: null, varId: null }
 }
packages/plugins/materials/index.ts (1)

39-40: Consider adding type annotations and documentation for the new option.

The hiddenBuiltinMaterials option is a useful addition for material management. However, consider:

  1. Adding TypeScript type annotations to specify what the array should contain (component IDs, names, etc.)
  2. Adding JSDoc documentation to describe the expected format and behavior
-    },
-    hiddenBuiltinMaterials: []
+    },
+    /** Array of built-in material component IDs to hide from the material panel */
+    hiddenBuiltinMaterials: [] as string[]
packages/register/src/constants.ts (1)

79-81: Add trailing comma for stylistic consistency

Every other property in META_APP ends with a comma. Keeping that style reduces churn in future diffs.

   // 版本管理
-  VersionControl: 'engine.plugins.versioncontrol'
+  VersionControl: 'engine.plugins.versioncontrol',
scripts/uploadMaterials.mjs (1)

19-27: Consider using FormData API instead of manual multipart construction.

Manual multipart/form-data construction is error-prone and the fixed boundary could conflict with file content. The FormData API is more reliable and handles edge cases automatically.

-const jsonBuffer = Buffer.from(JSON.stringify(bundle))
-const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'
-const formHeaders = {
-  'Content-Type': `multipart/form-data; boundary=${boundary}`,
-}
-
-let body = `--${boundary}\r\n`
-body += 'Content-Disposition: form-data; name="file"; filename="bundle.json"\r\n'
-body += 'Content-Type: application/json\r\n\r\n'
-body += jsonBuffer.toString() + `\r\n--${boundary}--`
+const formData = new FormData()
+const blob = new Blob([JSON.stringify(bundle)], { type: 'application/json' })
+formData.append('file', blob, 'bundle.json')

And update the fetch call:

 fetch(backend_url, {
   method: 'POST',
-  headers: formHeaders,
-  body: body,
+  body: formData,
 })
docs/api/backend-api/material-center.md (2)

330-334: Drop the manual <a> anchor – the heading already provides an ID

GitHub-flavored Markdown automatically generates an element ID for each heading (## 物料同步接口). The explicit <a id=…> directly beneath it is redundant clutter and risks producing duplicate IDs in some renderers.
Remove the <a> tag and rely on the built-in slug.

 ## 物料同步接口
-<a id=物料同步接口> </a>

345-359: Replace bold text with proper sub-headings to satisfy markdownlint MD036

**Headers**, **路径参数**, and **Body** are flagged because bold text masquerades as headings.
Convert them to real fourth-level headings to restore semantic structure and silence the linter.

-**Headers**
+#### Headers-**路径参数**
+#### 路径参数-**Body**
+#### Body
packages/plugins/versioncontrol/src/components/VersionTagCreate.vue (2)

1-46: Well-structured modal dialog with good UX patterns!

The template implements proper modal patterns including click-outside-to-close and keyboard support. The form validation and commit selection UI are intuitive.

Consider adding aria-label attributes for better accessibility.


124-648: Consider extracting common dialog styles to reduce duplication.

The styling is comprehensive and well-organized, but there's significant duplication of dialog-related styles that appear to be shared across multiple dialog components in the version control plugin.

Consider creating a shared stylesheet for common dialog styles:

/* shared-dialog-styles.less */
.dialog-overlay {
  /* Common overlay styles */
}

.dialog-content {
  /* Common content styles */
}

.dialog-header {
  /* Common header styles */
}

/* etc... */

Then import and extend in component-specific styles:

@import './shared-dialog-styles.less';

.dialog-content {
  &.small {
    max-width: 400px;
  }
}
packages/plugins/versioncontrol/src/components/VersionHeader.vue (1)

56-122: Good Vue 3 composition API implementation with minor optimization suggestions.

The script follows Vue 3 best practices with proper computed properties for v-model bindings and clean event forwarding.

Consider this minor optimization to simplify the setup return:

  return {
-   propsBranches: props.branches,
+   propsBranches: toRef(props, 'branches'),
    modelCurrentBranch,
    modelSearchQuery,
    docsUrl,
    close,
    onBranchChange,
    onSearch,
    createTag,
    createBranch
  }

Import toRef from Vue for better reactivity handling.

docs/api/frontend-api/material-api.md (1)

116-116: Fix markdown formatting - use proper heading instead of emphasis.

The static analysis tool correctly identified that emphasis is being used where a heading should be used.

-**使用示例**
+### 使用示例

This follows markdown best practices for document structure.

packages/plugins/versioncontrol/src/Main.vue (6)

348-348: Remove unused variable

The docsUrl variable is defined but never used in the template.

-    const docsUrl = useHelp().getDocsUrl('script')

Also remove it from the return statement at line 848:

     return {
-      docsUrl,
       currentBranch,

615-617: Remove commented console.log statements

Commented debug statements should be removed from production code.

-      // console.log('切换到分支:', currentBranch.value)
-      // 这里可以实现分支切换逻辑
+      // TODO: Implement branch switching logic

710-713: Improve form validation

The validation only checks for empty tag name. Consider adding more robust validation.

     const confirmCreateTag = () => {
       if (!newTagName.value.trim()) {
-        alert('请输入标签名称')
+        showNotification('请输入标签名称', 'warning')
         return
       }
+      
+      // Validate tag name format
+      const tagNameRegex = /^[a-zA-Z0-9._-]+$/
+      if (!tagNameRegex.test(newTagName.value.trim())) {
+        showNotification('标签名称只能包含字母、数字、点、下划线和连字符', 'warning')
+        return
+      }

725-730: Remove commented console.log

Remove commented debug code.

-      // console.log('创建标签:', {
-      //   name: newTagName.value.trim(),
-      //   description: newTagDescription.value,
-      //   commit: targetHash
-      // })

841-845: Remove or implement watch logic

The watch has only commented code. Either implement the logic or remove the watch entirely.

-    // 监听选中的提交变化
-    watch(selectedCommit, (newCommit) => {
-      if (newCommit) {
-        // console.log('选中提交:', newCommit.hash.slice(0, 7), newCommit.message)
-      }
-    })
+    // Watch for selected commit changes if needed
+    // watch(selectedCommit, (newCommit) => {
+    //   // Implement logic here if needed
+    // })

1-906: Consider breaking down this large component

This component is quite large (900+ lines). Consider splitting it into smaller, more manageable components:

  • Extract the timeline visualization into a separate component
  • Extract the commits list into a separate component
  • Move data fetching and state management to a composable or store

This would improve maintainability and testability.

Would you like me to help create a refactored structure for this component?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 108919d and 04bc072.

⛔ Files ignored due to path filters (5)
  • docs/solutions/imgs/bundle_upload.png is excluded by !**/*.png
  • docs/solutions/imgs/component_create_code.png is excluded by !**/*.png
  • packages/design-core/assets/plugin-icon-version-control.svg is excluded by !**/*.svg
  • packages/multi-person-collaboration/public/favicon.ico is excluded by !**/*.ico
  • packages/plugins/versioncontrol/assets/test.png is excluded by !**/*.png
📒 Files selected for processing (49)
  • .env.local (1 hunks)
  • docs/api/backend-api/material-center.md (1 hunks)
  • docs/api/frontend-api/material-api.md (7 hunks)
  • docs/solutions/material-sync-solution.md (3 hunks)
  • package.json (2 hunks)
  • packages/build/vite-config/src/default-config.js (1 hunks)
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1 hunks)
  • packages/build/vite-plugin-meta-comments/src/transform.js (0 hunks)
  • packages/build/vite-plugin-meta-comments/src/utils.js (2 hunks)
  • packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js (4 hunks)
  • packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js (1 hunks)
  • packages/build/vite-plugin-meta-comments/test/legacy/code/output.js (4 hunks)
  • packages/common/js/import-map.json (1 hunks)
  • packages/design-core/package.json (2 hunks)
  • packages/design-core/re-export.js (1 hunks)
  • packages/design-core/registry.js (2 hunks)
  • packages/design-core/src/preview/src/preview/Preview.vue (2 hunks)
  • packages/design-core/src/preview/src/preview/usePreviewData.ts (3 hunks)
  • packages/layout/src/defaultLayout.js (1 hunks)
  • packages/multi-person-collaboration/.gitignore (1 hunks)
  • packages/multi-person-collaboration/README.md (1 hunks)
  • packages/multi-person-collaboration/jsconfig.json (1 hunks)
  • packages/multi-person-collaboration/package.json (1 hunks)
  • packages/multi-person-collaboration/vite.config.js (1 hunks)
  • packages/plugins/materials/index.ts (1 hunks)
  • packages/plugins/materials/src/composable/useMaterial.ts (1 hunks)
  • packages/plugins/materials/src/meta/component/src/Main.vue (4 hunks)
  • packages/plugins/page/src/PageTree.vue (1 hunks)
  • packages/plugins/page/src/Tree.vue (1 hunks)
  • packages/plugins/state/src/CreateStore.vue (2 hunks)
  • packages/plugins/state/src/Main.vue (3 hunks)
  • packages/plugins/state/src/StateTips.vue (2 hunks)
  • packages/plugins/state/src/styles/vars.less (1 hunks)
  • packages/plugins/versioncontrol/index.ts (1 hunks)
  • packages/plugins/versioncontrol/meta.js (1 hunks)
  • packages/plugins/versioncontrol/package.json (1 hunks)
  • packages/plugins/versioncontrol/src/Main.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionHeader.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionTagCreate.vue (1 hunks)
  • packages/plugins/versioncontrol/src/styles/main.less (1 hunks)
  • packages/plugins/versioncontrol/vite.config.js (1 hunks)
  • packages/register/src/constants.ts (1 hunks)
  • packages/toolbars/save/src/js/index.ts (1 hunks)
  • patches/@vue__repl@2.9.0.patch (0 hunks)
  • patches/@vue__repl@4.6.1.patch (1 hunks)
  • pnpm-workspace.yaml (1 hunks)
  • scripts/uploadMaterials.mjs (1 hunks)
💤 Files with no reviewable changes (2)
🧰 Additional context used
🧠 Learnings (39)
📓 Common learnings
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/RenderMain.ts:82-88
Timestamp: 2025-01-14T08:50:50.226Z
Learning: For PR #1011, the focus is on resolving conflicts and migrating code, with architectural improvements deferred for future PRs.
Learnt from: gene9831
PR: opentiny/tiny-engine#1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (8)

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: gene9831
PR: #917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of @opentiny/tiny-engine-cli is 2.0.0, and documentation should reference this version instead of any release candidates.

Learnt from: gene9831
PR: #1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-06-28T07:26:38.511Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

packages/common/js/import-map.json (11)

Learnt from: gene9831
PR: #1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Learnt from: gene9831
PR: #917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of @opentiny/tiny-engine-cli is 2.0.0, and documentation should reference this version instead of any release candidates.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/importMap.js:51-51
Timestamp: 2025-01-13T03:46:13.817Z
Learning: The getImportMapData function in packages/canvas/DesignCanvas/src/importMap.js has default parameter handling that initializes canvasDeps with empty arrays for scripts and styles, making additional null checks unnecessary.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/builtin/CanvasRouterView.vue:4-19
Timestamp: 2025-01-14T06:56:11.072Z
Learning: In the tiny-engine project, component implementations should maintain consistency with other components at the same level, even if modernization improvements (like TypeScript and Composition API) are possible.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:95-98
Timestamp: 2025-01-14T06:55:59.692Z
Learning: The tiny-select component from @opentiny/vue library ensures selected options are valid internally, requiring no additional validation in the change handler.

packages/layout/src/defaultLayout.js (1)

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

packages/register/src/constants.ts (1)

Learnt from: hexqi
PR: #850
File: package.json:4-4
Timestamp: 2024-10-10T06:25:05.109Z
Learning: 当遇到与canvas包中"type": "module"相关的报错时,可以通过将canvas包中的.eslintrc文件改为.cjs后缀来解决,而无需在根目录的package.json中添加"type": "module"

packages/plugins/materials/index.ts (7)

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/support-collection.ts:3-15
Timestamp: 2025-01-14T06:59:02.999Z
Learning: The code in packages/canvas/render/src/material-function/support-collection.ts is migrated code that should not be modified at this time to maintain stability during the migration process.

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/importMap.js:51-51
Timestamp: 2025-01-13T03:46:13.817Z
Learning: The getImportMapData function in packages/canvas/DesignCanvas/src/importMap.js has default parameter handling that initializes canvasDeps with empty arrays for scripts and styles, making additional null checks unnecessary.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/builtin/builtin.json:157-223
Timestamp: 2025-01-14T04:20:02.367Z
Learning: The builtin.json configuration should strictly follow the existing protocol and rules without introducing new fields or properties.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:32-44
Timestamp: 2024-10-09T01:47:35.507Z
Learning: In appendElePlusStylePlugin.js, it's acceptable to silently return when the user's material doesn't have element-plus dependencies.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:32-44
Timestamp: 2024-09-25T11:13:34.258Z
Learning: In appendElePlusStylePlugin.js, it's acceptable to silently return when the user's material doesn't have element-plus dependencies.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/builtin/builtin.json:645-850
Timestamp: 2025-01-14T04:22:02.404Z
Learning: In TinyEngine, components must use inline styles instead of CSS classes because components cannot carry class styles when dragged into the canvas.

packages/multi-person-collaboration/jsconfig.json (2)

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/configure.ts:1-4
Timestamp: 2025-01-14T06:52:56.236Z
Learning: In the configure.ts module, type definitions were intentionally kept as Record<string, any> during the initial refactoring to separate files. Type definitions will be added in a follow-up task after thorough analysis of the configuration structure.

Learnt from: hexqi
PR: #850
File: package.json:4-4
Timestamp: 2024-10-10T06:25:05.109Z
Learning: 当遇到与canvas包中"type": "module"相关的报错时,可以通过将canvas包中的.eslintrc文件改为.cjs后缀来解决,而无需在根目录的package.json中添加"type": "module"

packages/plugins/versioncontrol/index.ts (7)

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

packages/plugins/page/src/Tree.vue (1)

Learnt from: gene9831
PR: #1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.

packages/design-core/re-export.js (9)

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-06-28T07:26:38.511Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/builtin/CanvasRouterView.vue:4-19
Timestamp: 2025-01-14T06:56:11.072Z
Learning: In the tiny-engine project, component implementations should maintain consistency with other components at the same level, even if modernization improvements (like TypeScript and Composition API) are possible.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

packages/design-core/registry.js (8)

Learnt from: gene9831
PR: #1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/importMap.js:51-51
Timestamp: 2025-01-13T03:46:13.817Z
Learning: The getImportMapData function in packages/canvas/DesignCanvas/src/importMap.js has default parameter handling that initializes canvasDeps with empty arrays for scripts and styles, making additional null checks unnecessary.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: gene9831
PR: #512
File: packages/entry/src/entryHash.js:0-0
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The getMergeRegistry function in packages/entry/src/entryHash.js must include an Array.isArray check before using the find method to ensure it handles cases where the registry is an array.

Learnt from: gene9831
PR: #512
File: packages/entry/src/entryHash.js:0-0
Timestamp: 2024-07-27T04:06:09.935Z
Learning: The getMergeRegistry function in packages/entry/src/entryHash.js must include an Array.isArray check before using the find method to ensure it handles cases where the registry is an array.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-10-09T01:47:35.507Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-09-25T11:18:00.771Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

packages/design-core/package.json (12)

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: gene9831
PR: #1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Learnt from: gene9831
PR: #917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of @opentiny/tiny-engine-cli is 2.0.0, and documentation should reference this version instead of any release candidates.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/builtin/CanvasRouterView.vue:4-19
Timestamp: 2025-01-14T06:56:11.072Z
Learning: In the tiny-engine project, component implementations should maintain consistency with other components at the same level, even if modernization improvements (like TypeScript and Composition API) are possible.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:95-98
Timestamp: 2025-01-14T06:55:59.692Z
Learning: The tiny-select component from @opentiny/vue library ensures selected options are valid internally, requiring no additional validation in the change handler.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

packages/toolbars/save/src/js/index.ts (3)

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

packages/plugins/versioncontrol/src/styles/main.less (3)

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-10-09T01:47:35.507Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-09-25T11:18:00.771Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

packages/plugins/state/src/Main.vue (4)

Learnt from: rhlin
PR: #1011
File: packages/canvas/container/src/components/CanvasMenu.vue:228-232
Timestamp: 2025-01-14T04:29:26.886Z
Learning: Error handling for page switching is centralized in the switchPageWithConfirm method of the page service, rather than being duplicated in UI handlers like menu actions.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

packages/plugins/state/src/CreateStore.vue (3)

Learnt from: yy-wow
PR: #886
File: packages/plugins/state/src/js/http.js:19-19
Timestamp: 2024-10-30T02:19:37.775Z
Learning: In the packages/plugins/state/src/js/http.js file, errors for the requestGlobalState function are handled by the user, so additional error handling is unnecessary.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

package.json (1)

Learnt from: yy-wow
PR: #940
File: packages/plugins/materials/src/composable/useMaterial.js:0-0
Timestamp: 2025-01-13T07:49:12.136Z
Learning: In the materials system, componentsDepsMap.scripts in materialState is used as a data store for tracking component dependencies and doesn't require Vue reactivity as changes are handled explicitly through updateCanvasDeps().

packages/plugins/state/src/StateTips.vue (3)

Learnt from: gene9831
PR: #1233
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-20T07:20:12.221Z
Learning: In CanvasDivider.vue, even though state.verLeft and state.horizontalTop already include 'px' suffix, the CSS properties in state.dividerStyle still need to append 'px' again according to gene9831, suggesting that these state variables might be processed differently than expected when used in style binding.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

packages/multi-person-collaboration/package.json (6)

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: gene9831
PR: #1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Learnt from: gene9831
PR: #917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of @opentiny/tiny-engine-cli is 2.0.0, and documentation should reference this version instead of any release candidates.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

packages/plugins/versioncontrol/vite.config.js (8)

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-06-28T07:26:38.511Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: gene9831
PR: #1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/configure.ts:1-4
Timestamp: 2025-01-14T06:52:56.236Z
Learning: In the configure.ts module, type definitions were intentionally kept as Record<string, any> during the initial refactoring to separate files. Type definitions will be added in a follow-up task after thorough analysis of the configuration structure.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

packages/multi-person-collaboration/vite.config.js (5)

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-06-28T07:26:38.511Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/configure.ts:1-4
Timestamp: 2025-01-14T06:52:56.236Z
Learning: In the configure.ts module, type definitions were intentionally kept as Record<string, any> during the initial refactoring to separate files. Type definitions will be added in a follow-up task after thorough analysis of the configuration structure.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

packages/plugins/versioncontrol/package.json (6)

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: gene9831
PR: #917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of @opentiny/tiny-engine-cli is 2.0.0, and documentation should reference this version instead of any release candidates.

Learnt from: gene9831
PR: #1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.

Learnt from: gene9831
PR: #1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

packages/build/vite-config/src/default-config.js (12)

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-06-28T07:26:38.511Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/importMap.js:51-51
Timestamp: 2025-01-13T03:46:13.817Z
Learning: The getImportMapData function in packages/canvas/DesignCanvas/src/importMap.js has default parameter handling that initializes canvasDeps with empty arrays for scripts and styles, making additional null checks unnecessary.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-10-09T01:47:35.507Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-09-25T11:18:00.771Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

Learnt from: hexqi
PR: #702
File: packages/common/composable/index.js:1-2
Timestamp: 2024-10-24T03:55:39.020Z
Learning: 文件名不需要包含 default,直接使用更简洁的名称即可。例如,将 defaultGlobalService.js 修改为 globalService.js

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: hexqi
PR: #850
File: package.json:4-4
Timestamp: 2024-10-10T06:25:05.109Z
Learning: 当遇到与canvas包中"type": "module"相关的报错时,可以通过将canvas包中的.eslintrc文件改为.cjs后缀来解决,而无需在根目录的package.json中添加"type": "module"

packages/design-core/src/preview/src/preview/usePreviewData.ts (16)

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/importMap.js:51-51
Timestamp: 2025-01-13T03:46:13.817Z
Learning: The getImportMapData function in packages/canvas/DesignCanvas/src/importMap.js has default parameter handling that initializes canvasDeps with empty arrays for scripts and styles, making additional null checks unnecessary.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/custom-renderer.ts:28-32
Timestamp: 2025-01-14T07:11:44.138Z
Learning: The locale and webComponent wrapper in packages/canvas/render/src/canvas-function/custom-renderer.ts are part of migrated code and will be improved in a future PR.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

Learnt from: yy-wow
PR: #940
File: packages/plugins/materials/src/composable/useMaterial.js:0-0
Timestamp: 2025-01-13T07:49:12.136Z
Learning: In the materials system, componentsDepsMap.scripts in materialState is used as a data store for tracking component dependencies and doesn't require Vue reactivity as changes are handled explicitly through updateCanvasDeps().

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/support-collection.ts:3-15
Timestamp: 2025-01-14T06:59:02.999Z
Learning: The code in packages/canvas/render/src/material-function/support-collection.ts is migrated code that should not be modified at this time to maintain stability during the migration process.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:66-80
Timestamp: 2025-01-14T04:25:46.281Z
Learning: In the tiny-engine project, styles from block components are processed through Vite's CSS compilation pipeline, and additional style sanitization libraries should be avoided to maintain consistency with this approach.

Learnt from: gene9831
PR: #1011
File: packages/plugins/page/src/PageGeneral.vue:166-178
Timestamp: 2025-01-14T06:40:18.223Z
Learning: The page structure data in PageGeneral.vue cannot have circular dependencies due to design constraints.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:95-98
Timestamp: 2025-01-14T06:55:59.692Z
Learning: The tiny-select component from @opentiny/vue library ensures selected options are valid internally, requiring no additional validation in the change handler.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.

patches/@vue__repl@4.6.1.patch (2)

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/custom-renderer.ts:28-32
Timestamp: 2025-01-14T07:11:44.138Z
Learning: The locale and webComponent wrapper in packages/canvas/render/src/canvas-function/custom-renderer.ts are part of migrated code and will be improved in a future PR.

packages/build/vite-plugin-meta-comments/test/legacy/code/output.js (5)

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/custom-renderer.ts:28-32
Timestamp: 2025-01-14T07:11:44.138Z
Learning: The locale and webComponent wrapper in packages/canvas/render/src/canvas-function/custom-renderer.ts are part of migrated code and will be improved in a future PR.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/controller.ts:1-7
Timestamp: 2025-01-14T08:44:09.485Z
Learning: Type safety improvements for the controller in packages/canvas/render/src/canvas-function/controller.ts should be deferred until the data structure is finalized.

packages/plugins/materials/src/composable/useMaterial.ts (3)

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/support-collection.ts:3-15
Timestamp: 2025-01-14T06:59:02.999Z
Learning: The code in packages/canvas/render/src/material-function/support-collection.ts is migrated code that should not be modified at this time to maintain stability during the migration process.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/data-utils.ts:8-12
Timestamp: 2025-01-14T06:58:38.661Z
Learning: The use of Function constructor in packages/canvas/render/src/data-utils.ts is part of migrated code and was intentionally kept as-is during migration, despite potential security concerns.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

packages/plugins/versioncontrol/meta.js (4)

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-10-09T01:47:35.507Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-09-25T11:18:00.771Z
Learning: In appendElePlusStylePlugin.js, the code uses || {} to set default values when obtaining files, so additional null checks may not be necessary.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

packages/design-core/src/preview/src/preview/Preview.vue (10)

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Learnt from: yy-wow
PR: #940
File: packages/plugins/materials/src/composable/useMaterial.js:0-0
Timestamp: 2025-01-13T07:49:12.136Z
Learning: In the materials system, componentsDepsMap.scripts in materialState is used as a data store for tracking component dependencies and doesn't require Vue reactivity as changes are handled explicitly through updateCanvasDeps().

Learnt from: gene9831
PR: #1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/DesignCanvas.vue:0-0
Timestamp: 2025-01-15T02:19:06.755Z
Learning: In Vue components using message subscriptions from @opentiny/tiny-engine-meta-register, always clean up subscriptions in the onUnmounted hook using useMessage().unsubscribe() to prevent memory leaks.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: hexqi
PR: #1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like __dirname are available at runtime, while ES6 module-specific features like import.meta would cause runtime errors.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

packages/plugins/versioncontrol/src/components/VersionHeader.vue (4)

Learnt from: gene9831
PR: #1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

Learnt from: gene9831
PR: #1226
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-19T03:13:51.520Z
Learning: The CSS bug in packages/canvas/container/src/components/CanvasDivider.vue where verLeft already includes "px" but is being appended again in the style object will be fixed in a future update, as confirmed by gene9831.

Learnt from: gene9831
PR: #1233
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-20T07:20:12.221Z
Learning: In CanvasDivider.vue, even though state.verLeft and state.horizontalTop already include 'px' suffix, the CSS properties in state.dividerStyle still need to append 'px' again according to gene9831, suggesting that these state variables might be processed differently than expected when used in style binding.

packages/plugins/page/src/PageTree.vue (5)

Learnt from: gene9831
PR: #1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.

Learnt from: gene9831
PR: #1011
File: packages/plugins/page/src/PageGeneral.vue:166-178
Timestamp: 2025-01-14T06:40:18.223Z
Learning: The page structure data in PageGeneral.vue cannot have circular dependencies due to design constraints.

Learnt from: gene9831
PR: #1069
File: packages/plugins/page/src/PageGeneral.vue:107-107
Timestamp: 2025-01-24T07:30:38.528Z
Learning: In PageGeneral.vue, pageParentId.value is only used to initialize the oldParentId ref. After initialization, oldParentId is kept in sync with pageSettingState.oldParentId through a watchEffect.

Learnt from: chilingling
PR: #1440
File: packages/plugins/materials/src/composable/useResource.ts:82-84
Timestamp: 2025-05-28T03:58:31.212Z
Learning: In the TinyEngine codebase, there are two different data structures for page information:

  1. App schema components tree (appSchemaState.pageTree) uses nested meta structure with page.meta?.id
  2. API responses from pagePluginApi.getPageById() return flattened structure with pageInfo.id and pageInfo.occupier directly
    The code should use page.meta?.id when working with pageTree data and pageInfo.id when working with API response data.

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/DesignCanvas.vue:0-0
Timestamp: 2025-01-15T02:19:06.755Z
Learning: In Vue components using message subscriptions from @opentiny/tiny-engine-meta-register, always clean up subscriptions in the onUnmounted hook using useMessage().unsubscribe() to prevent memory leaks.

packages/build/vite-plugin-meta-comments/src/utils.js (2)

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-06-28T07:26:38.511Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

Learnt from: chilingling
PR: #583
File: packages/build/vite-config/index.js:1-1
Timestamp: 2024-10-09T01:47:35.507Z
Learning: The getDefaultConfig function inside packages/build/vite-config/src/default-config.js is intended to remain and be called internally. Ensure no references to this function exist outside of this file.

packages/plugins/versioncontrol/src/components/VersionTagCreate.vue (1)

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

docs/solutions/material-sync-solution.md (2)

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/support-collection.ts:3-15
Timestamp: 2025-01-14T06:59:02.999Z
Learning: The code in packages/canvas/render/src/material-function/support-collection.ts is migrated code that should not be modified at this time to maintain stability during the migration process.

Learnt from: hexqi
PR: #850
File: package.json:4-4
Timestamp: 2024-10-10T06:25:05.109Z
Learning: 当遇到与canvas包中"type": "module"相关的报错时,可以通过将canvas包中的.eslintrc文件改为.cjs后缀来解决,而无需在根目录的package.json中添加"type": "module"

packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1)

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

docs/api/frontend-api/material-api.md (13)

Learnt from: chilingling
PR: #1389
File: docs/api/frontend-api/material-api.md:84-117
Timestamp: 2025-05-13T06:27:51.334Z
Learning: The registerBlock method in the useMaterial API is planned for deprecation and should not be documented or recommended.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/support-collection.ts:3-15
Timestamp: 2025-01-14T06:59:02.999Z
Learning: The code in packages/canvas/render/src/material-function/support-collection.ts is migrated code that should not be modified at this time to maintain stability during the migration process.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:32-44
Timestamp: 2024-09-25T11:13:34.258Z
Learning: In appendElePlusStylePlugin.js, it's acceptable to silently return when the user's material doesn't have element-plus dependencies.

Learnt from: chilingling
PR: #817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:32-44
Timestamp: 2024-10-09T01:47:35.507Z
Learning: In appendElePlusStylePlugin.js, it's acceptable to silently return when the user's material doesn't have element-plus dependencies.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/material-getter.ts:55-88
Timestamp: 2025-01-14T04:25:08.323Z
Learning: The BlockLoadError component in packages/canvas/render/src/material-function/material-getter.ts requires a name prop to display which block failed to load.

Learnt from: yy-wow
PR: #886
File: packages/common/composable/defaultGlobalService.js:53-56
Timestamp: 2024-11-06T09:38:05.573Z
Learning: 在 packages/common/composable/defaultGlobalService.js 文件中,对于 fetchAppInfofetchAppList 等函数,错误处理应由调用者负责,不应在函数内部添加错误处理。

Learnt from: yy-wow
PR: #886
File: packages/common/composable/index.js:3-3
Timestamp: 2024-10-30T02:10:38.028Z
Learning: 在 packages/common/composable 目录中,HttpService 模块位于 ./http/index.js,而不是 ./http.js

Learnt from: hexqi
PR: #702
File: packages/common/composable/index.js:1-2
Timestamp: 2024-10-24T03:55:39.020Z
Learning: 文件名不需要包含 default,直接使用更简洁的名称即可。例如,将 defaultGlobalService.js 修改为 globalService.js

Learnt from: yy-wow
PR: #940
File: packages/plugins/materials/src/composable/useMaterial.js:0-0
Timestamp: 2025-01-13T07:49:12.136Z
Learning: In the materials system, componentsDepsMap.scripts in materialState is used as a data store for tracking component dependencies and doesn't require Vue reactivity as changes are handled explicitly through updateCanvasDeps().

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/page-getter.ts:15-26
Timestamp: 2025-01-14T04:27:05.374Z
Learning: Memory cleanup for style sheets and page schemas in packages/canvas/render/src/material-function/page-getter.ts is deferred until a proper loading/unloading mechanism is implemented.

Learnt from: hexqi
PR: #850
File: package.json:4-4
Timestamp: 2024-10-10T06:25:05.109Z
Learning: 当遇到与canvas包中"type": "module"相关的报错时,可以通过将canvas包中的.eslintrc文件改为.cjs后缀来解决,而无需在根目录的package.json中添加"type": "module"

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/importMap.js:51-51
Timestamp: 2025-01-13T03:46:13.817Z
Learning: The getImportMapData function in packages/canvas/DesignCanvas/src/importMap.js has default parameter handling that initializes canvasDeps with empty arrays for scripts and styles, making additional null checks unnecessary.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: The SvgIcon component is globally registered and available throughout the application without requiring explicit imports.

packages/plugins/materials/src/meta/component/src/Main.vue (17)

Learnt from: gene9831
PR: #830
File: packages/common/component/MetaChildItem.vue:50-56
Timestamp: 2024-10-15T02:45:17.168Z
Learning: In packages/common/component/MetaChildItem.vue, when checking if text is an object in the computed property title, ensure that text is not null because typeof null === 'object' in JavaScript. Use checks like text && typeof text === 'object' to safely handle null values.

Learnt from: yy-wow
PR: #940
File: packages/plugins/materials/src/composable/useMaterial.js:0-0
Timestamp: 2025-01-13T07:49:12.136Z
Learning: In the materials system, componentsDepsMap.scripts in materialState is used as a data store for tracking component dependencies and doesn't require Vue reactivity as changes are handled explicitly through updateCanvasDeps().

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in packages/canvas/render/src/canvas-function/design-mode.ts is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/builtin/CanvasRouterView.vue:4-19
Timestamp: 2025-01-14T06:56:11.072Z
Learning: In the tiny-engine project, component implementations should maintain consistency with other components at the same level, even if modernization improvements (like TypeScript and Composition API) are possible.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in packages/canvas/render/src/application-function/global-state.ts is migrated from an existing codebase and should be handled with care when making modifications.

Learnt from: gene9831
PR: #1117
File: packages/canvas/container/src/components/CanvasViewerSwitcher.vue:96-117
Timestamp: 2025-02-17T12:11:22.718Z
Learning: In CanvasViewerSwitcher.vue, state.usedHoverState.element is guaranteed to have a value when handleClick is called, making additional error handling unnecessary.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/controller.ts:1-7
Timestamp: 2025-01-14T08:44:09.485Z
Learning: Type safety improvements for the controller in packages/canvas/render/src/canvas-function/controller.ts should be deferred until the data structure is finalized.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/material-function/support-collection.ts:3-15
Timestamp: 2025-01-14T06:59:02.999Z
Learning: The code in packages/canvas/render/src/material-function/support-collection.ts is migrated code that should not be modified at this time to maintain stability during the migration process.

Learnt from: gene9831
PR: #1233
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-20T07:20:12.221Z
Learning: In CanvasDivider.vue, even though state.verLeft and state.horizontalTop already include 'px' suffix, the CSS properties in state.dividerStyle still need to append 'px' again according to gene9831, suggesting that these state variables might be processed differently than expected when used in style binding.

Learnt from: rhlin
PR: #1011
File: packages/canvas/render/src/canvas-function/custom-renderer.ts:28-32
Timestamp: 2025-01-14T07:11:44.138Z
Learning: The locale and webComponent wrapper in packages/canvas/render/src/canvas-function/custom-renderer.ts are part of migrated code and will be improved in a future PR.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered and available throughout Vue components without requiring explicit imports.

Learnt from: yy-wow
PR: #940
File: packages/canvas/DesignCanvas/src/DesignCanvas.vue:0-0
Timestamp: 2025-01-15T02:19:06.755Z
Learning: In Vue components using message subscriptions from @opentiny/tiny-engine-meta-register, always clean up subscriptions in the onUnmounted hook using useMessage().unsubscribe() to prevent memory leaks.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:63-73
Timestamp: 2025-01-14T06:49:00.797Z
Learning: In the tiny-engine project, the SvgIcon component is globally registered using app.component('SvgIcon', SvgIcon) in packages/svgs/index.js, making it available throughout Vue components without requiring explicit imports.

Learnt from: gene9831
PR: #1011
File: packages/configurator/src/router-select-configurator/RouterSelectConfigurator.vue:95-98
Timestamp: 2025-01-14T06:55:59.692Z
Learning: The tiny-select component from @opentiny/vue library ensures selected options are valid internally, requiring no additional validation in the change handler.

Learnt from: chilingling
PR: #837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the tiny-engine project, @opentiny/tiny-engine-dsl-vue refers to the current package itself, and importing types from it may cause circular dependencies.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In packages/toolbars/preview/src/Main.vue, within the preview function, the getMergeMeta method is used at lines 64 and 65 to retrieve engine.config configurations.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

packages/plugins/versioncontrol/src/Main.vue (2)

Learnt from: gene9831
PR: #1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (2)

Learnt from: gene9831
PR: #1226
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-19T03:13:51.520Z
Learning: The CSS bug in packages/canvas/container/src/components/CanvasDivider.vue where verLeft already includes "px" but is being appended again in the style object will be fixed in a future update, as confirmed by gene9831.

Learnt from: yy-wow
PR: #850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 packages/toolbars/preview/src/Main.vue 文件中,使用 useNotify 而不是 console 来记录错误日志。

🧬 Code Graph Analysis (6)
packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js (2)
packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js (2)
  • sendMessage (89-106)
  • ccc (108-108)
packages/build/vite-plugin-meta-comments/test/legacy/code/output.js (2)
  • sendMessage (96-116)
  • ccc (118-118)
packages/layout/src/defaultLayout.js (1)
packages/register/src/constants.ts (1)
  • META_APP (24-82)
packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js (2)
packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js (2)
  • sendMessage (40-42)
  • ccc (46-46)
packages/build/vite-plugin-meta-comments/test/legacy/code/output.js (2)
  • sendMessage (96-116)
  • ccc (118-118)
packages/build/vite-config/src/default-config.js (1)
packages/design-core/vite.config.js (2)
  • nodeGlobalsPolyfillPlugin (11-11)
  • nodeModulesPolyfillPlugin (12-12)
packages/build/vite-plugin-meta-comments/test/legacy/code/output.js (2)
packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js (2)
  • sendMessage (89-106)
  • ccc (108-108)
packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js (2)
  • sendMessage (40-42)
  • ccc (46-46)
packages/plugins/materials/src/composable/useMaterial.ts (2)
packages/plugins/materials/src/composable/types.ts (1)
  • Material (1-6)
packages/register/src/common.ts (1)
  • getOptions (32-34)
🪛 dotenv-linter (3.3.0)
.env.local

[warning] 5-5: [UnorderedKey] The SQL_DATABASE key should go before the SQL_HOST key


[warning] 7-7: [EndingBlankLine] No blank line at the end of the file


[warning] 7-7: [LowercaseKey] The backend_url key should be in uppercase

🪛 markdownlint-cli2 (0.17.2)
docs/api/frontend-api/material-api.md

116-116: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

docs/api/backend-api/material-center.md

347-347: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


353-353: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


359-359: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (67)
packages/plugins/page/src/Tree.vue (1)

98-105: Interface export looks good

Making TreeNode an exported interface will allow other plugins to share the type without duplication.

packages/plugins/page/src/PageTree.vue (1)

128-128: Handle async invocation in onMounted

We’ve confirmed that getPageList is declared as async, so refreshPageList correctly returns a Promise. To avoid unhandled rejections, update the onMounted call to explicitly await the result or catch errors:

  • File: packages/plugins/page/src/PageTree.vue
    • Line 128: const refreshPageList = (appId: string) => getPageList(appId) – OK.
    • Line 133: change from:
      onMounted(() => {
        refreshPageList(getAppId())
      })
      to:
      onMounted(async () => {
        await refreshPageList(getAppId())
      })
      // or
      onMounted(() => {
        refreshPageList(getAppId()).catch(console.error)
      })

The usage at line 154 (inside pageSettingState.updateTreeData(await refreshPageList(...))) already awaits the promise, so no change is needed there.

⛔ Skipped due to learnings
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/plugins/page/src/PageGeneral.vue:166-178
Timestamp: 2025-01-14T06:40:18.223Z
Learning: The page structure data in PageGeneral.vue cannot have circular dependencies due to design constraints.
Learnt from: gene9831
PR: opentiny/tiny-engine#1069
File: packages/plugins/page/src/PageGeneral.vue:107-107
Timestamp: 2025-01-24T07:30:38.528Z
Learning: In PageGeneral.vue, `pageParentId.value` is only used to initialize the `oldParentId` ref. After initialization, `oldParentId` is kept in sync with `pageSettingState.oldParentId` through a watchEffect.
Learnt from: chilingling
PR: opentiny/tiny-engine#1440
File: packages/plugins/materials/src/composable/useResource.ts:82-84
Timestamp: 2025-05-28T03:58:31.212Z
Learning: In the TinyEngine codebase, there are two different data structures for page information:
1. App schema components tree (appSchemaState.pageTree) uses nested meta structure with page.meta?.id
2. API responses from pagePluginApi.getPageById() return flattened structure with pageInfo.id and pageInfo.occupier directly
The code should use page.meta?.id when working with pageTree data and pageInfo.id when working with API response data.
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.
packages/build/vite-plugin-meta-comments/test/legacy/code/entry.js (1)

44-46: LGTM!

The reordering of sendMessage() invocation before the ccc declaration is valid and appears to be an intentional test case modification to verify the plugin's handling of immediate function calls.

packages/build/vite-plugin-meta-comments/test/expected/entry.js.output.js (1)

38-38: LGTM!

The transformation output correctly reflects the reordered execution and properly includes ccc in the context of functions that can access it after its declaration, while excluding it from sendMessage's context since it's not available at that point.

Also applies to: 58-58, 84-84, 107-108

packages/build/vite-plugin-meta-comments/test/legacy/code/output.js (1)

38-38: LGTM!

The legacy test output correctly mirrors the expected transformation behavior, maintaining consistency across test files.

Also applies to: 61-61, 90-90, 117-118

packages/build/vite-plugin-meta-comments/src/utils.js (1)

187-218: Well-designed context-aware binding strategy

The modification intelligently detects direct function calls and switches between synchronous and asynchronous variable bindings accordingly. This ensures correct variable capture based on the execution context.

packages/multi-person-collaboration/.gitignore (1)

1-25: LGTM! Comprehensive .gitignore configuration.

The .gitignore file follows standard best practices for Node.js/JavaScript projects, covering all essential categories: logs, dependencies, build outputs, local environment files, and editor-specific files. The inclusion of an exception for .vscode/extensions.json is particularly good practice for team collaboration.

packages/plugins/state/src/styles/vars.less (1)

32-32: Ensure the new CSS var is actually consumed

--te-state-tip-highlight-color is defined here but I don’t see any references added in this PR. If the variable remains unused it can confuse maintainers later.
Please double-check that the intended components (e.g. StateTips.vue) reference this var before merging.

packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1)

32-32: Alias entry looks good

The path and naming pattern are consistent with neighbouring aliases. No further action needed.

pnpm-workspace.yaml (2)

2-4: Package globs – no issues spotted

Removing the quotes from the non-negated globs doesn’t change parsing and improves readability. 👍


7-8: Verify patch file presence

patchedDependencies now references patches/@vue__repl@4.6.1.patch.
Please confirm the patch file is committed and that pnpm install succeeds on a clean clone.

packages/layout/src/defaultLayout.js (1)

15-17: UI plugin list updated correctly

META_APP.VersionControl is added after State, matching the new plugin’s meta constant.
No layout conflicts detected.

packages/design-core/re-export.js (1)

30-30: LGTM! Export follows established pattern.

The addition of the VersionControl plugin export follows the consistent naming and structure pattern used for other plugin exports in this file.

packages/design-core/package.json (1)

64-64: LGTM! Workspace dependency follows established pattern.

The version control plugin dependency is correctly added using the workspace:* pattern consistent with other internal dependencies.

packages/plugins/versioncontrol/meta.js (1)

1-7: LGTM! Plugin metadata structure is well-defined.

The metadata object correctly defines all necessary properties for plugin registration:

  • ID follows the established namespace pattern
  • Chinese title is appropriate for the target audience
  • Icon and resizable properties are properly configured
packages/plugins/versioncontrol/index.ts (1)

1-19: LGTM! Plugin entry point follows established architecture.

The file correctly implements the standard TinyEngine plugin pattern:

  • Proper copyright header included
  • Clean separation of metadata and component imports
  • Appropriate use of spread operator to combine plugin configuration
packages/multi-person-collaboration/jsconfig.json (1)

1-15: LGTM! Well-configured JSConfig for the multi-person collaboration package.

The configuration properly sets up modern JavaScript development with ESNext target, type checking for JS files, JSX preservation, and sensible path aliases. This aligns well with the Vue/JSX-based multi-person collaboration plugin architecture.

packages/build/vite-config/src/default-config.js (2)

80-81: LGTM! Proper exclusion of @vue/repl from optimization.

The addition of the exclude array with clear documentation prevents Vite from optimizing away monaco-editor dependencies, which is necessary for the updated @vue/repl version's worker handling.


88-88: LGTM! Simplified plugin configuration aligns with @vue/repl upgrade.

The removal of the esbuildCopy plugin is consistent with the upgrade to @vue/repl 4.6.1, which now handles monaco-editor worker files internally, eliminating the need for manual asset copying during development.

packages/design-core/registry.js (2)

43-43: LGTM! VersionControl plugin properly imported.

The import follows the established pattern for plugin imports from the re-export module.


163-164: LGTM! VersionControl plugin properly registered.

The plugin registration follows the established conditional pattern using __TINY_ENGINE_REMOVED_REGISTRY with the appropriate key 'engine.plugins.versioncontrol', consistent with other plugins in the registry.

docs/api/backend-api/material-center.md (1)

359-368: Body section is empty — essential parameters are undocumented

The “Body” table currently has only headers. At minimum the multipart field file (and any others such as uid, projectId, etc. if applicable) must be described here; otherwise the spec is incomplete.

Please confirm all required form fields and populate the table accordingly. If only file is required, you can delete the empty Body block and rely on the corrected Form-Data table above.

package.json (2)

24-24: LGTM: New upload script added.

The addition of the uploadMaterials script aligns with the material synchronization features mentioned in the PR objectives.


59-59: Action Required: Confirm Compatibility of @vue/repl v2→v4 Upgrade

We found concrete usages of @vue/repl that need manual verification:

• packages/design-core/src/preview/src/preview/Preview.vue
– import { Repl, useStore, useVueImportMap } from '@vue/repl'
– import '@vue/repl/style.css'
– const Monaco = defineAsyncComponent(() => import('@vue/repl/monaco-editor'))

• packages/build/vite-config/src/default-config.js
– optimizeDeps.exclude: ['@vue/repl']

Please ensure:

  1. The public API surface of Repl (component and hooks) in v4.6.1 matches how it’s consumed in Preview.vue.
  2. Styles from '@vue/repl/style.css' still apply correctly.
  3. Lazy-loaded Monaco editor via '@vue/repl/monaco-editor' still initializes as expected.
  4. Your Vite optimizeDeps exclusion remains necessary and valid.

Run the Preview in a local build and exercise all REPL features to catch any breaking changes.

packages/plugins/versioncontrol/vite.config.js (1)

20-39: LGTM: Well-structured plugin build configuration.

The Vite configuration properly:

  • Externalizes Vue and OpenTiny dependencies to avoid bundling peer dependencies
  • Configures library build with correct entry point and ES module format
  • Includes CSS import banner and source maps
  • Follows established patterns in the tiny-engine project
packages/plugins/state/src/Main.vue (3)

233-233: LGTM: Proper async validation handling.

Making the validation callback asynchronous allows for proper await handling in the promise chain.


247-256: LGTM: Smart panel state management during save.

The logic to temporarily fix the panel state prevents unwanted panel closure due to selection clearing during the save operation, then restores the original state. This improves user experience by maintaining panel visibility during the save process.


289-289: LGTM: User feedback improvement.

Adding a success notification for global state saves provides valuable user feedback and improves the overall user experience.

patches/@vue__repl@4.6.1.patch (4)

9-16: LGTM: Cross-origin worker compatibility fix.

The getWorkerURL helper function properly handles cross-origin scenarios by creating blob URLs when needed, which is essential for web worker functionality in different deployment contexts.


25-30: LGTM: Dead code removal.

Removing the commented-out unreachable code branch improves code cleanliness and maintainability.


38-39: LGTM: Improved worker instantiation.

Using the getWorkerURL helper for both editor and Vue language service workers ensures consistent cross-origin handling across all worker types.

Also applies to: 47-48


61-62: LGTM: Enhanced iframe sandbox permissions.

Adding "allow-downloads" to the sandbox permissions expands the functionality of the Vue REPL component while maintaining security through the existing sandbox restrictions.

packages/plugins/versioncontrol/package.json (1)

1-14: LGTM: Well-structured plugin package configuration.

The package.json follows the established patterns for tiny-engine plugins with:

  • Correct naming convention and version
  • Proper workspace dependencies for internal packages
  • Appropriate peer dependencies for Vue and OpenTiny packages
  • Standard build configuration and file exports

Also applies to: 20-43

packages/plugins/state/src/StateTips.vue (2)

26-38: LGTM! Well-structured component enhancement.

The addition of conditional rendering with the type prop is cleanly implemented. The TIPS_TYPE constant provides good maintainability, and the default prop value ensures backward compatibility.


50-60: Clean styling implementation.

The scoped styles for .app-tips are well-organized with proper nesting and use of CSS custom properties for theming consistency.

packages/multi-person-collaboration/vite.config.js (2)

20-31: Excellent dynamic entry collection implementation.

The use of globSync to dynamically collect core modules and Vue components as entry points is a clean and maintainable approach. The path manipulation correctly removes file extensions for entry names.


44-59: Well-configured library build setup.

The build configuration appropriately:

  • Sets up multiple entry points from the dynamic collection
  • Uses ES module format for modern consumption
  • Correctly externalizes peer dependencies
  • Disables CSS code splitting for library builds
packages/plugins/materials/src/composable/useMaterial.ts (2)

407-424: Solid implementation of built-in material filtering.

The filterBuiltinMaterials function is well-designed:

  • Properly accesses configuration through getOptions
  • Uses optional chaining and fallback for safe array access
  • Maintains the original material structure while filtering snippets
  • Clear documentation explains the purpose

427-435: Clean integration of filtering functionality.

The integration in initBuiltinMaterial maintains the existing flow while adding the filtering capability. The consistent application to both canvas and component materials ensures uniform behavior.

packages/design-core/src/preview/src/preview/usePreviewData.ts (4)

316-322: Good API improvement with callback-based import map management.

The updated function signature replacing direct store access with a setImportMap callback promotes better separation of concerns and makes the function more testable.


325-331: Simplified file assignment logic.

The removal of JSX transformation logic in assignFiles streamlines the code and aligns with the broader refactoring to eliminate custom Babel processing.


347-347: Consistent use of new import map callback.

The replacement of store.setImportMap(importMapData) with setImportMap(importMapData) maintains the same functionality while using the new callback-based approach.


391-391: Updated main file reference.

The change from 'src/Main.vue' to 'App.vue' aligns with the updated file structure and naming conventions in the preview system.

docs/solutions/material-sync-solution.md (3)

3-15: Well-documented material upload solution!

The new recommended solution is clearly documented with important notes about version field requirements and backend configuration. The inclusion of visual aids enhances understanding.


16-21: Consistent heading hierarchy maintained.

The heading levels are properly adjusted to maintain a clear document structure with both solutions at the same hierarchical level.


49-49: Important database troubleshooting note added.

Good addition of the note about manually inserting relational data when new components aren't appearing. This will help users troubleshoot common issues.

packages/plugins/state/src/CreateStore.vue (3)

161-186: Robust state validation implementation!

The validation function properly handles all edge cases:

  • Empty/whitespace-only content
  • Invalid JSON syntax
  • Non-object types (arrays, null, primitives)

The error messages are clear and guide users to the correct format.


40-44: Appropriate state tips type specification.

The type="app" attribute correctly specifies the context for state tips in the store creation form, enabling appropriate tip content rendering.


193-193: Good defensive programming with default value.

Using a default empty object prevents potential runtime errors when the state property is undefined.

packages/design-core/src/preview/src/preview/Preview.vue (4)

16-58: Clean migration to useStore hook pattern!

The refactoring properly leverages Vue 3 composition API with reactive refs and the updated @vue/repl API. The import map initialization correctly preserves the built-in imports.


64-66: Clean implementation of import map updates.

The setImportMap function provides a clear interface for updating the import map dynamically.


71-71: Consistent API extension for import map support.

The addition of setImportMap to the usePreviewData hook parameters properly extends the preview functionality.


60-62: Confirm internal handling of resetFlip and initTsConfig in @vue/repl v4.6.1

I searched the entire repo (including patches) and found no remaining references to resetFlip or initTsConfig. Packages/design-core is locked to Vue REPL 4.6.1 and Preview.vue imports and calls only useStore().setFiles(...). Please double-check that in @vue/repl v4.6.1:

  • useStore().setFiles internally resets any flipped state
  • TypeScript configuration is re-initialized as before

Locations to review:

• packages/design-core/src/preview/src/preview/Preview.vue (lines 60–62)

Thanks!

packages/plugins/versioncontrol/src/components/VersionTagCreate.vue (1)

48-122: Excellent Vue 3 composition API implementation!

The component properly implements:

  • Two-way data binding with computed properties
  • Well-typed props with sensible defaults
  • Clear emit declarations
  • Clean separation of concerns
packages/plugins/materials/src/meta/component/src/Main.vue (3)

39-69: Excellent TypeScript type safety improvements!

The additions properly type the injected state and create a filtered, typed component list. The use of TypeScript's type inference from the computed value is a clean approach.


112-128: Clean state typing and initialization!

The reactive state is properly typed, and the logic correctly uses the filtered componentsWithChildren throughout.


126-128: Consistent use of filtered components list.

The function properly uses componentsWithChildren maintaining consistency throughout the component.

packages/plugins/versioncontrol/src/components/VersionHeader.vue (1)

1-54: LGTM! Well-structured template with good accessibility.

The template follows good practices with semantic HTML structure, proper event handling, and accessible form controls. The three-section layout (left/center/right) provides a clean interface for the version control header.

packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (2)

1-69: LGTM! Well-implemented modal dialog with proper event handling.

The template follows good practices for modal dialogs with proper overlay handling, click event management, and safe property access using optional chaining. The comparison data is presented clearly with good semantic structure.


71-110: LGTM! Clean composition API implementation.

The script follows Vue 3 best practices with proper computed properties for v-model bindings and clean event forwarding. The implementation is simple and focused.

packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (2)

1-102: LGTM! Comprehensive commit detail display with good UX.

The template provides a thorough view of commit information with proper conditional rendering, semantic structure, and useful action buttons. The layout is well-organized and user-friendly.


104-184: LGTM! Solid composition API implementation with proper localization.

The script follows Vue 3 best practices with clean event forwarding and proper use of computed properties. The formatDate function appropriately uses Chinese locale formatting.

packages/plugins/versioncontrol/src/styles/main.less (1)

1-867: Excellent stylesheet with comprehensive responsive design and clean architecture.

This is a well-crafted LESS stylesheet that demonstrates strong CSS architecture:

  • Logical organization: Clear nesting structure that follows the component hierarchy
  • Responsive design: Well-thought-out breakpoints (1200px, 900px, 768px) with appropriate layout adaptations
  • Theming support: Good use of CSS custom properties for maintainable color schemes
  • Interactive states: Smooth transitions and hover effects enhance user experience
  • Performance considerations: Efficient selectors and minimal redundancy

The timeline visualization, commit list styling, and responsive adaptations are particularly well-implemented.

docs/api/frontend-api/material-api.md (3)

18-18: Excellent addition of hiddenBuiltinMaterials configuration option.

The new configuration option addresses a real need for customizing the material panel display. The documentation is comprehensive with clear examples and usage scenarios.


30-67: Well-structured documentation with helpful examples.

The detailed documentation for hiddenBuiltinMaterials provides clear configuration examples and explains the use cases effectively. The list of available built-in components is particularly helpful for users.


120-429: Comprehensive API documentation improvements with excellent examples.

The enhanced documentation provides thorough coverage of the material API with:

  • Clear method signatures and parameter descriptions
  • Practical usage examples for common scenarios
  • Well-organized type definitions
  • Helpful use case explanations

This significantly improves the developer experience for working with the material system.

packages/plugins/versioncontrol/src/Main.vue (1)

111-111: Add :key attribute to v-for directive

The v-for directive should have a unique :key attribute for proper Vue reactivity.

-                  <span v-for="tag in commit.tags.slice(0, 2)" :key="tag" class="tag-mini">{{ tag }}</span>
+                  <span v-for="(tag, index) in commit.tags.slice(0, 2)" :key="`${commit.hash}-tag-${index}`" class="tag-mini">{{ tag }}</span>
⛔ Skipped due to learnings
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/plugins/page/src/PageGeneral.vue:166-178
Timestamp: 2025-01-14T06:40:18.223Z
Learning: The page structure data in PageGeneral.vue cannot have circular dependencies due to design constraints.
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 `packages/toolbars/preview/src/Main.vue` 文件中,使用 `useNotify` 而不是 `console` 来记录错误日志。
Learnt from: gene9831
PR: opentiny/tiny-engine#1233
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-20T07:20:12.221Z
Learning: In CanvasDivider.vue, even though state.verLeft and state.horizontalTop already include 'px' suffix, the CSS properties in state.dividerStyle still need to append 'px' again according to gene9831, suggesting that these state variables might be processed differently than expected when used in style binding.
Learnt from: gene9831
PR: opentiny/tiny-engine#1226
File: packages/canvas/container/src/components/CanvasDivider.vue:184-185
Timestamp: 2025-03-19T03:13:51.520Z
Learning: The CSS bug in packages/canvas/container/src/components/CanvasDivider.vue where verLeft already includes "px" but is being appended again in the style object will be fixed in a future update, as confirmed by gene9831.
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In `packages/toolbars/preview/src/Main.vue`, within the `preview` function, the `getMergeMeta` method is used at lines 64 and 65 to retrieve `engine.config` configurations.

Comment thread .env.local
Comment thread docs/api/backend-api/material-center.md
Comment thread packages/build/vite-plugin-meta-comments/src/utils.js
Comment thread packages/common/js/import-map.json
Comment thread packages/design-core/package.json
Comment thread packages/plugins/versioncontrol/src/Main.vue Outdated
Comment thread packages/toolbars/save/src/js/index.ts
Comment thread scripts/uploadMaterials.mjs
Comment thread scripts/uploadMaterials.mjs
Comment thread scripts/uploadMaterials.mjs

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (9)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)

15-44: Consider using Intl.DateTimeFormat consistently for better i18n support.

The function uses inconsistent formatting approaches: manual formatting for 'zh-CN' and Intl.DateTimeFormat for 'en-US'. This creates maintenance overhead and potential inconsistencies.

-export function formatDateTime(timestamp: Timestamp, locale: SupportedLocale = 'zh-CN'): string {
-  const date = new Date(timestamp)
-
-  const pad = (n: number): string => n.toString().padStart(2, '0')
-
-  const hour = pad(date.getHours())
-  const minute = pad(date.getMinutes())
-  const second = pad(date.getSeconds())
-
-  switch (locale) {
-    case 'zh-CN': {
-      const year = date.getFullYear()
-      const month = pad(date.getMonth() + 1)
-      const day = pad(date.getDate())
-      return `${year}-${month}-${day} ${hour}:${minute}:${second}`
-    }
-
-    case 'en-US': {
-      const datePart = new Intl.DateTimeFormat('en-US', {
-        year: 'numeric',
-        month: 'short',
-        day: 'numeric'
-      }).format(date)
-      return `${datePart} ${hour}:${minute}:${second}`
-    }
-
-    default:
-      return date.toISOString()
-  }
-}
+export function formatDateTime(timestamp: Timestamp, locale: SupportedLocale = 'zh-CN'): string {
+  const date = new Date(timestamp)
+  
+  const options: Intl.DateTimeFormatOptions = {
+    year: 'numeric',
+    month: locale === 'zh-CN' ? '2-digit' : 'short',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit',
+    hour12: false
+  }
+  
+  return new Intl.DateTimeFormat(locale, options).format(date)
+}
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (1)

181-189: Consider making protection methods chainable for consistency.

While other setter methods return this for method chaining, these protection methods return void. For API consistency, consider making them chainable.

-  setProtection(protection: BranchProtectionRule): void {
+  setProtection(protection: BranchProtectionRule): this {
     this._protection = protection
     this._updatedAt = Date.now()
+    return this
   }

-  removeProtection(): void {
+  removeProtection(): this {
     this._protection = undefined
     this._updatedAt = Date.now()
+    return this
   }
packages/plugins/versioncontrol/src/js/domain/services/CommitServiceImpl.ts (5)

103-104: Remove duplicate documentation comment.

  /**
-   * 获取提交
+   * 获取分支的提交列表
   * @param branchId
   * @param options
   * @returns
   */

180-181: Remove duplicate comment.

     // 更新验证状态
-    // 更新验证状态

206-207: Remove duplicate comment.

     // 验证目标提交属于该分支
-    // 验证目标提交属于该分支

177-183: Consider immutability pattern for commits.

Modifying commit verification status after creation may violate expectations of immutability. In version control systems, commits are typically immutable once created.

Consider creating a new commit object or tracking verification status separately:

// Option 1: Track verification separately
private verificationCache = new Map<ID, boolean>();

verifyCommit(commitId: ID): boolean {
  const commit = this.getCommit(commitId);
  if (!commit) {
    throw new Error(`Commit with id ${commitId} not found`);
  }
  
  const isVerified = this.signatureVerifier(commit);
  this.verificationCache.set(commitId, isVerified);
  return isVerified;
}

324-324: Consider implementing schema content search.

The commented code suggests a planned feature for searching within schema content. This could be valuable for finding specific components or properties.

Would you like me to help implement schema content search functionality? This could include searching for component names, prop values, or text content within the PageSchema.

packages/plugins/versioncontrol/src/js/domain/services/BranchServiceImpl.ts (1)

470-481: Implement operation persistence.

The branch operation recording is not persisted. This is critical for audit trails and history tracking.

The operation recording logic is incomplete. Would you like me to help implement the persistence layer for operation history? This could include creating an OperationRepository interface and implementation.

packages/plugins/versioncontrol/src/js/shared/type.ts (1)

125-125: Fix typo in Chinese comment.

-  readonly commitsBehind: number //落后上游的分支的提交数
+  readonly commitsBehind: number // 落后上游分支的提交数
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04bc072 and 955322e.

📒 Files selected for processing (11)
  • packages/plugins/versioncontrol/package.json (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/models/Schema.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/services/BranchServiceImpl.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/services/CommitServiceImpl.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/shared/type.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/shared/utils.ts (1 hunks)
  • tsconfig.app.json (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • tsconfig.app.json
  • packages/plugins/versioncontrol/package.json
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/RenderMain.ts:82-88
Timestamp: 2025-01-14T08:50:50.226Z
Learning: For PR #1011, the focus is on resolving conflicts and migrating code, with architectural improvements deferred for future PRs.
Learnt from: gene9831
PR: opentiny/tiny-engine#1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.
📚 Learning: the locale and webcomponent wrapper in `packages/canvas/render/src/canvas-function/custom-renderer.t...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/custom-renderer.ts:28-32
Timestamp: 2025-01-14T07:11:44.138Z
Learning: The locale and webComponent wrapper in `packages/canvas/render/src/canvas-function/custom-renderer.ts` are part of migrated code and will be improved in a future PR.

Applied to files:

  • packages/plugins/versioncontrol/src/js/shared/utils.ts
📚 Learning: in the tinyengine codebase, there are two different data structures for page information: 1. app sch...
Learnt from: chilingling
PR: opentiny/tiny-engine#1440
File: packages/plugins/materials/src/composable/useResource.ts:82-84
Timestamp: 2025-05-28T03:58:31.212Z
Learning: In the TinyEngine codebase, there are two different data structures for page information:
1. App schema components tree (appSchemaState.pageTree) uses nested meta structure with page.meta?.id
2. API responses from pagePluginApi.getPageById() return flattened structure with pageInfo.id and pageInfo.occupier directly
The code should use page.meta?.id when working with pageTree data and pageInfo.id when working with API response data.

Applied to files:

  • packages/plugins/versioncontrol/src/js/domain/models/Schema.ts
📚 Learning: in the configure.ts module, type definitions were intentionally kept as record during t...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/material-function/configure.ts:1-4
Timestamp: 2025-01-14T06:52:56.236Z
Learning: In the configure.ts module, type definitions were intentionally kept as Record<string, any> during the initial refactoring to separate files. Type definitions will be added in a follow-up task after thorough analysis of the configuration structure.

Applied to files:

  • packages/plugins/versioncontrol/src/js/shared/type.ts
📚 Learning: in the tiny-engine project, the team prefers to gradually refine typescript types as they become cle...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/type.d.ts:1-14
Timestamp: 2025-01-14T06:57:07.645Z
Learning: In the tiny-engine project, the team prefers to gradually refine TypeScript types as they become clearer, rather than prematurely defining specific types when the exact structure is not yet well-understood.

Applied to files:

  • packages/plugins/versioncontrol/src/js/shared/type.ts
🧬 Code Graph Analysis (6)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (1)
  • Timestamp (11-11)
packages/plugins/versioncontrol/src/js/domain/models/Schema.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (5)
  • ID (6-6)
  • PageSchema (411-411)
  • User (16-24)
  • Timestamp (11-11)
  • PageState (425-442)
packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (2)
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (4)
  • name (71-73)
  • upstreamBranchId (83-85)
  • description (79-81)
  • Branch (7-325)
packages/plugins/versioncontrol/src/js/shared/type.ts (9)
  • ID (6-6)
  • User (16-24)
  • Branch (109-128)
  • MergeStrategy (186-186)
  • ConflictReport (287-296)
  • BranchStatusResponse (229-243)
  • BranchOperationHistoryResponse (270-275)
  • CommitHistoryRequest (329-337)
  • CommitHistoryResponse (342-349)
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (2)
packages/plugins/versioncontrol/src/js/shared/type.ts (7)
  • Branch (109-128)
  • ID (6-6)
  • BranchType (91-91)
  • BranchStatus (96-104)
  • User (16-24)
  • Timestamp (11-11)
  • BranchProtectionRule (139-145)
packages/plugins/versioncontrol/src/js/domain/models/Schema.ts (3)
  • creator (42-44)
  • createdAt (46-48)
  • updatedAt (50-52)
packages/plugins/versioncontrol/src/js/domain/services/CommitServiceImpl.ts (5)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (4)
  • Commit (7-165)
  • message (47-49)
  • schema (51-53)
  • stats (63-65)
packages/plugins/versioncontrol/src/js/shared/type.ts (9)
  • Commit (69-81)
  • ID (6-6)
  • User (16-24)
  • PageSchema (411-411)
  • DiffResult (47-55)
  • Snapshot (416-420)
  • CommitHistoryRequest (329-337)
  • CommitHistoryResponse (342-349)
  • Timestamp (11-11)
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1)
  • CommitService (16-91)
packages/plugins/versioncontrol/src/js/domain/services/BranchServiceImpl.ts (1)
  • BranchRepository (21-28)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)
  • Memoize (51-101)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (6)
  • Commit (69-81)
  • ID (6-6)
  • User (16-24)
  • Timestamp (11-11)
  • PageSchema (411-411)
  • CommitStats (60-64)
🔇 Additional comments (8)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (3)

73-83: LGTM! Proper immutable tag management.

The tag management methods correctly handle immutable updates by creating new arrays rather than mutating existing ones, which aligns well with the readonly array type declaration.


90-102: LGTM! Well-implemented utility methods.

Both utility methods follow established version control conventions and implement correct logic.


20-44: LGTM! Well-designed constructor and field encapsulation.

The constructor properly initializes all fields, and the mix of readonly and private fields with getters provides appropriate encapsulation for a domain model.

packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (2)

7-68: Well-structured domain model with proper encapsulation.

The Branch class is well-designed with:

  • Clear separation of required and optional parameters
  • Proper use of readonly and private fields
  • Good constructor pattern using options object for optional parameters

254-254: Uncomment or remove the protection field.

The protection field is commented out in both serialization methods. This creates inconsistency as the field exists in the class but isn't serialized.

Should the protection field be included in serialization? If not, consider removing these commented lines entirely. If yes, uncomment them to ensure data consistency.

Also applies to: 274-274, 299-299, 320-320

packages/plugins/versioncontrol/src/js/domain/services/BranchServiceImpl.ts (1)

81-118: Excellent branch creation implementation.

The method properly validates inputs, maintains bidirectional references between branches, and records the operation for audit purposes.

packages/plugins/versioncontrol/src/js/shared/type.ts (2)

127-127: Uncomment or document why protection field is excluded.

The protection field is commented out but exists in the Branch domain model. This creates inconsistency.

Should the Branch interface include the protection field? If it's intentionally excluded from the interface while present in the implementation, please add a comment explaining why.


374-443: Well-designed schema type system.

The PageSchema type hierarchy is well-structured for representing low-code page structures with:

  • Clear separation between regular nodes and root nodes
  • Proper use of recursive types
  • Comprehensive PageState interface for runtime management

Comment thread packages/plugins/versioncontrol/src/js/domain/models/Branch.ts
Comment thread packages/plugins/versioncontrol/src/js/domain/models/Branch.ts
Comment thread packages/plugins/versioncontrol/src/js/domain/models/Branch.ts
Comment thread packages/plugins/versioncontrol/src/js/domain/models/Commit.ts
Comment thread packages/plugins/versioncontrol/src/js/domain/models/Schema.ts
Comment thread packages/plugins/versioncontrol/src/js/domain/services/CommitServiceImpl.ts Outdated
Comment thread packages/plugins/versioncontrol/src/js/shared/type.ts
Comment thread packages/plugins/versioncontrol/src/js/shared/type.ts Outdated
Comment thread packages/plugins/versioncontrol/src/js/shared/type.ts
Comment thread packages/plugins/versioncontrol/src/js/shared/utils.ts

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 25

♻️ Duplicate comments (7)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (2)

32-100: Apply consistent error handling improvements

This class has the same error handling issues as BranchRepository. Errors should be propagated rather than silently returning null/empty arrays.

The same error handling improvements suggested for BranchRepository should be applied here. Consider creating a shared error handling utility or base class to avoid duplication.


92-99: Use proper HTTP method for delete operation

Same issue as in BranchRepository - using GET for DELETE operations.

   async deleteCommit(id: ID): Promise<void> {
     try {
-      await api.get(`${BASE_URL}/delete/${id}`)
+      await api.delete(`${BASE_URL}/${id}`)
     } catch (error) {
       // eslint-disable-next-line no-console
       console.error('Failed to delete commit:', error)
+      throw error
     }
   }
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1)

16-79: Address interface design inconsistencies and error handling.

The interface has several design inconsistencies that could lead to implementation challenges:

  1. Inconsistent return patterns: Some methods return data directly while others use request/response objects
  2. Missing error handling: Operations like rollbackBranchToCommit return void but could fail
  3. Missing async patterns: Real implementations would likely be async, but interface assumes synchronous operations
packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (1)

19-108: Improve interface consistency and type safety.

The interface has design issues similar to those in CommitService:

  1. Inconsistent return patterns: Some methods return response objects while others return void or direct data
  2. Missing error handling: Many operations that could fail return void
  3. Type safety: Line 76 uses any type for protectionRule parameter
  4. Missing async patterns: Real implementations would likely be async
packages/plugins/versioncontrol/src/js/shared/type.ts (3)

30-30: Export FileChangeType for external use.

The type is used in the FileChange interface but not exported, which may cause issues when importing from other modules.

-type FileChangeType = 'added' | 'modified' | 'deleted' | 'renamed' | 'moved'
+export type FileChangeType = 'added' | 'modified' | 'deleted' | 'renamed' | 'moved'

283-289: Export ConflictType for external use.

The type is used in exported interfaces but not exported itself, which prevents external modules from properly typing conflict-related operations.

-type ConflictType =
+export type ConflictType =
  | 'content'
  | 'file_added_by_both'
  | 'file_deleted_by_one'
  | 'file_renamed_by_both'
  | 'property_conflict'

308-313: Export ConflictResolutionStrategy for external use.

The type is used in exported interfaces but not exported itself, preventing external modules from properly typing resolution strategies.

-type ConflictResolutionStrategy =
+export type ConflictResolutionStrategy =
  | 'accept_current' // 接受当前分支的更改
  | 'accept_incoming' // 接受传入分支的更改
  | 'manual' // 手动解决
  | 'auto_merge' // 自动合并(系统尝试解决)
🧹 Nitpick comments (5)
packages/plugins/versioncontrol/src/js/shared/validation.ts (1)

6-82: Consider adding TypeScript generics for better type safety

The class uses any type for values, which reduces type safety. Consider using generics.

-export class Validator {
-  private value: any
+export class Validator<T = any> {
+  private value: T
   private label: string
   private rules: ValidatorRule[] = []

-  private constructor(value: any, label: string) {
+  private constructor(value: T, label: string) {
     this.value = value
     this.label = label
   }

-  static check(value: any, label: string): Validator {
-    return new Validator(value, label)
+  static check<T>(value: T, label: string): Validator<T> {
+    return new Validator<T>(value, label)
   }
packages/plugins/versioncontrol/src/js/domain/strategies/ConflictResolver.ts (2)

52-112: Complete the simplified conflict detection implementation

The comments indicate this is a simplified implementation that needs to be completed. The current logic only handles basic property conflicts and misses important scenarios.

The TODO comments list several missing conflict scenarios:

  • Source deletes a node that target modifies
  • Source modifies a node that target deletes
  • Both branches add nodes with the same ID but different content

Would you like me to help implement these additional conflict detection scenarios or create an issue to track this work?


136-138: Use TypeScript exhaustive check for better type safety

Instead of throwing an error in the default case, use TypeScript's exhaustive check pattern.

       default:
-        throw new Error(`Unsupported resolution strategy: ${resolutionStrategy}`)
+        const _exhaustiveCheck: never = resolutionStrategy
+        throw new Error(`Unsupported resolution strategy: ${resolutionStrategy}`)

This will cause a compile-time error if a new resolution strategy is added but not handled.

packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1)

1-135: Consider extracting common repository patterns

Both CommitRepository and BranchRepository share very similar patterns. Consider creating a generic base repository class to reduce code duplication.

Example base repository:

abstract class BaseRepository<T> {
  protected abstract baseUrl: string
  protected api = getMetaApi(META_SERVICE.Http)
  
  async findById(id: ID): Promise<T | null> {
    try {
      const res = await this.api.get(`${this.baseUrl}/getById/${id}`)
      return res || null
    } catch (error) {
      console.error(`Failed to fetch by ID:`, error)
      throw error
    }
  }
  
  // Common CRUD operations...
}
packages/plugins/versioncontrol/src/js/domain/strategies/MergeResolver.ts (1)

70-82: Simplified merge implementation needs enhancement.

The three-way merge implementation is simplified and doesn't use a common ancestor, as noted in the TODO comment. This could lead to incorrect merge results in complex scenarios.

Would you like me to help implement a proper three-way merge algorithm that:

  1. Finds the common ancestor commit
  2. Calculates diffs from the ancestor to both branches
  3. Applies non-conflicting changes from both sides
  4. Properly detects and reports conflicts
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 955322e and 8c8101a.

📒 Files selected for processing (12)
  • packages/plugins/versioncontrol/package.json (1 hunks)
  • packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/strategies/ConflictResolver.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/strategies/MergeResolver.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/strategies/SchemaDiffResolver.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/shared/type.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/shared/validation.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/plugins/versioncontrol/package.json
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/RenderMain.ts:82-88
Timestamp: 2025-01-14T08:50:50.226Z
Learning: For PR #1011, the focus is on resolving conflicts and migrating code, with architectural improvements deferred for future PRs.
Learnt from: gene9831
PR: opentiny/tiny-engine#1041
File: packages/plugins/datasource/src/DataSourceList.vue:138-138
Timestamp: 2025-01-14T10:06:25.508Z
Learning: PR #1041 in opentiny/tiny-engine is specifically for reverting Prettier v3 formatting to v2, without any logical code changes or syntax improvements.
Learnt from: gene9831
PR: opentiny/tiny-engine#1038
File: packages/plugins/block/index.js:24-24
Timestamp: 2025-01-14T08:42:18.574Z
Learning: In the tiny-engine project, breaking changes are documented in the changelog rather than in JSDoc comments or separate migration guides.
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/builtin/CanvasRouterView.vue:4-19
Timestamp: 2025-01-14T06:56:11.072Z
Learning: In the tiny-engine project, component implementations should maintain consistency with other components at the same level, even if modernization improvements (like TypeScript and Composition API) are possible.
Learnt from: gene9831
PR: opentiny/tiny-engine#917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of `opentiny/tiny-engine-cli` is `2.0.0`, and documentation should reference this version instead of any release candidates.
📚 Learning: in the configure.ts module, type definitions were intentionally kept as record during t...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/material-function/configure.ts:1-4
Timestamp: 2025-01-14T06:52:56.236Z
Learning: In the configure.ts module, type definitions were intentionally kept as Record<string, any> during the initial refactoring to separate files. Type definitions will be added in a follow-up task after thorough analysis of the configuration structure.

Applied to files:

  • packages/plugins/versioncontrol/src/js/domain/strategies/ConflictResolver.ts
  • packages/plugins/versioncontrol/src/js/domain/strategies/MergeResolver.ts
  • packages/plugins/versioncontrol/src/js/shared/type.ts
📚 Learning: for pr #1011, the focus is on resolving conflicts and migrating code, with architectural improvement...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/RenderMain.ts:82-88
Timestamp: 2025-01-14T08:50:50.226Z
Learning: For PR #1011, the focus is on resolving conflicts and migrating code, with architectural improvements deferred for future PRs.

Applied to files:

  • packages/plugins/versioncontrol/src/js/domain/strategies/ConflictResolver.ts
  • packages/plugins/versioncontrol/src/js/domain/strategies/MergeResolver.ts
📚 Learning: the code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts
  • packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts
  • packages/plugins/versioncontrol/src/js/shared/type.ts
📚 Learning: the code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should n...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.

Applied to files:

  • packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts
  • packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts
📚 Learning: type safety improvements for the controller in `packages/canvas/render/src/canvas-function/controlle...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/controller.ts:1-7
Timestamp: 2025-01-14T08:44:09.485Z
Learning: Type safety improvements for the controller in `packages/canvas/render/src/canvas-function/controller.ts` should be deferred until the data structure is finalized.

Applied to files:

  • packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts
📚 Learning: in the tiny-engine project, the team prefers to gradually refine typescript types as they become cle...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/type.d.ts:1-14
Timestamp: 2025-01-14T06:57:07.645Z
Learning: In the tiny-engine project, the team prefers to gradually refine TypeScript types as they become clearer, rather than prematurely defining specific types when the exact structure is not yet well-understood.

Applied to files:

  • packages/plugins/versioncontrol/src/js/shared/type.ts
📚 Learning: the user prefers to keep the `function` type in `packages/canvas/render/src/page-block-function/acce...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/page-block-function/accessor-map.ts:13-13
Timestamp: 2025-01-14T07:11:58.019Z
Learning: The user prefers to keep the `Function` type in `packages/canvas/render/src/page-block-function/accessor-map.ts` for now, despite static analysis warnings.

Applied to files:

  • packages/plugins/versioncontrol/src/js/shared/type.ts
📚 Learning: the code in `packages/canvas/render/src/application-function/global-state.ts` is migrated from an ex...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in `packages/canvas/render/src/application-function/global-state.ts` is migrated from an existing codebase and should be handled with care when making modifications.

Applied to files:

  • packages/plugins/versioncontrol/src/js/shared/type.ts
📚 Learning: the use of `function` type in `packages/canvas/render/src/application-function/utils.ts` is acceptab...
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/application-function/utils.ts:18-18
Timestamp: 2025-01-14T08:47:20.946Z
Learning: The use of `Function` type in `packages/canvas/render/src/application-function/utils.ts` is acceptable per team's decision, despite TypeScript's recommendation to use more specific function types.

Applied to files:

  • packages/plugins/versioncontrol/src/js/shared/type.ts
📚 Learning: 在 `packages/common/composable/defaultglobalservice.js` 文件中,对于 `fetchappinfo` 和 `fetchapplist` 等函数,错误...
Learnt from: yy-wow
PR: opentiny/tiny-engine#886
File: packages/common/composable/defaultGlobalService.js:53-56
Timestamp: 2024-11-06T09:38:05.573Z
Learning: 在 `packages/common/composable/defaultGlobalService.js` 文件中,对于 `fetchAppInfo` 和 `fetchAppList` 等函数,错误处理应由调用者负责,不应在函数内部添加错误处理。

Applied to files:

  • packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts
🧬 Code Graph Analysis (2)
packages/plugins/versioncontrol/src/js/domain/strategies/SchemaDiffResolver.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (1)
  • PageSchema (418-418)
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (4)
packages/plugins/versioncontrol/src/js/shared/type.ts (11)
  • ID (7-7)
  • User (17-25)
  • PageSchema (418-418)
  • CommitStats (61-65)
  • Commit (70-82)
  • DiffResult (48-56)
  • Snapshot (423-427)
  • Branch (110-129)
  • CommitHistoryRequest (336-344)
  • CommitHistoryResponse (349-356)
  • Timestamp (12-12)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (4)
  • message (47-49)
  • schema (51-53)
  • stats (63-65)
  • Commit (7-165)
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (1)
  • Branch (7-325)
packages/plugins/versioncontrol/src/js/domain/strategies/SchemaDiffResolver.ts (1)
  • SchemaDiffResolver (9-77)
🔇 Additional comments (7)
packages/plugins/versioncontrol/src/js/shared/validation.ts (1)

38-45: Add null check before type validation

The maxLength validation should handle null/undefined values gracefully or be used after required validation.

   maxLength(max: number, message?: string): Validator {
     this.rules.push(() => {
+      if (this.value === null || this.value === undefined) {
+        return // Skip validation for null/undefined values
+      }
       if (typeof this.value === 'string' && this.value.length > max) {
         throw new Error(message || `${this.label} must be at most ${max} characters long.`)
       }
     })
     return this
   }
⛔ Skipped due to learnings
Learnt from: gene9831
PR: opentiny/tiny-engine#830
File: packages/common/component/MetaChildItem.vue:50-56
Timestamp: 2024-10-15T02:45:17.168Z
Learning: In `packages/common/component/MetaChildItem.vue`, when checking if `text` is an object in the computed property `title`, ensure that `text` is not `null` because `typeof null === 'object'` in JavaScript. Use checks like `text && typeof text === 'object'` to safely handle `null` values.
Learnt from: chilingling
PR: opentiny/tiny-engine#817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-09-25T11:18:00.771Z
Learning: In `appendElePlusStylePlugin.js`, the code uses `|| {}` to set default values when obtaining files, so additional null checks may not be necessary.
Learnt from: chilingling
PR: opentiny/tiny-engine#817
File: packages/vue-generator/src/plugins/appendElePlusStylePlugin.js:46-50
Timestamp: 2024-10-09T01:47:35.507Z
Learning: In `appendElePlusStylePlugin.js`, the code uses `|| {}` to set default values when obtaining files, so additional null checks may not be necessary.
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1)

182-214: LGTM!

The method properly handles filtering, pagination, and error cases by returning a structured response.

packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (1)

156-170: Good use of soft delete pattern.

The implementation correctly uses soft delete by marking the branch as 'deleted' rather than physically removing it, which is excellent for audit trails and recovery.

packages/plugins/versioncontrol/src/js/shared/type.ts (4)

1-25: LGTM! Well-structured basic type definitions.

The import and basic type definitions are properly structured with appropriate readonly properties and comprehensive documentation.


67-134: Excellent domain modeling with proper TypeScript patterns.

The commit and branch type definitions demonstrate solid domain modeling with appropriate use of readonly properties, utility types, and comprehensive relationship mapping.


135-276: Consistent API patterns with comprehensive branch operations.

The branch operation types follow consistent request/response patterns and provide comprehensive coverage of branch lifecycle management.


380-450: Well-structured page schema types with appropriate flexibility.

The page schema and state definitions appropriately balance type safety with the flexibility needed for a low-code engine, using Record<string, any> where schema structures may vary dynamically.

Comment thread packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts Outdated
Comment thread packages/plugins/versioncontrol/src/js/shared/validation.ts
Comment thread packages/plugins/versioncontrol/src/js/shared/validation.ts

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 25

♻️ Duplicate comments (3)
packages/plugins/versioncontrol/src/components/VersionHeader.vue (1)

275-289: Fix malformed media query syntax
The media query is missing the width value, producing invalid CSS. This was already flagged previously.

-@media (max-width) {
+@media (max-width: 600px) {
   .version-control-header {
     flex-direction: column;
     gap: 12px;
     align-items: stretch;
packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (2)

438-438: Remove unnecessary await on synchronous service methods

BranchService methods are synchronous; awaiting is misleading and unnecessary.

-    await this.branchService.unprotectBranch(branch)
+    this.branchService.unprotectBranch(branch)
-    await this.branchService.archiveBranch(branch)
+    this.branchService.archiveBranch(branch)
-    await this.branchService.unarchiveBranch(branch)
+    this.branchService.unarchiveBranch(branch)

Also applies to: 454-454, 470-470


343-347: Fix error message to reference the correct commit ID

Error message uses the commit object variable instead of the ID.

-  if (!lastCommit) {
-      throw new Error(`Commit with id ${lastCommit} not found.`)
-  }
+  if (!lastCommit) {
+      throw new Error(`Commit with id ${branch.headCommitId} not found.`)
+  }
🧹 Nitpick comments (21)
mockServer/src/services/commit.js (1)

54-57: Unused parameter 'appId' in list()

Either remove the parameter or scope the query to appId if multitenancy is required.

mockServer/src/services/branch.js (2)

22-26: Consider additional indexes for typical branch queries

Name uniqueness is good. Also indexing status and upstream/head commit improves list/filter operations.

     this.db.ensureIndex({
       fieldName: 'name',
       unique: true
     })
+    this.db.ensureIndex({ fieldName: 'status' })
+    this.db.ensureIndex({ fieldName: 'upstreamBranchId' })
+    this.db.ensureIndex({ fieldName: 'headCommitId' })

61-64: Unused parameter 'appId' in list()

Either remove it or implement scoping if multitenancy is expected.

packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (3)

558-563: Pagination semantics: currentPage behaves like “pages loaded”

start is hardcoded to 0, so currentPage controls the number of pages accumulated (infinite-scroll style). If that’s intended, rename to pagesLoaded for clarity; if true pagination is intended, set start = (currentPage - 1) * pageSize.

-  const paginatedCommits = computed<Commit[]>(() => {
-    const start = 0
-    const end = currentPage.value * pageSize.value
+  const paginatedCommits = computed<Commit[]>(() => {
+    const start = 0 // or (currentPage.value - 1) * pageSize.value for page-slice mode
+    const end = currentPage.value * pageSize.value
     return filteredCommits.value.slice(start, end)
   })

4-17: Stronger typing for Commit.type and changedFiles

Consider narrowing type to a union for commit types and making changedFiles consistently string[].

 export interface Commit {
   hash: string
   author: string
   date: string
   message: string
   avatar: string
-  type: string
+  type: 'feature' | 'bugfix' | 'docs' | 'perf' | 'refactor' | 'chore' | 'test' | 'build' | 'ci' | 'revert' | 'style' | 'merge'
   tags?: string[]
   branches?: string[]
   filesChanged?: number
   additions?: number
   deletions?: number
-  changedFiles?: string[]
+  changedFiles?: string[]
 }

568-596: Replace mock data with services when wiring backend

The composable currently ships with rich mock commits. For production wiring, source branches/commits from versionManager.commitAppService/branchAppService (packages/plugins/versioncontrol/src/js/index.ts) to keep data in sync with the domain layer.

I can draft a follow-up composable variant that hydrates from CommitAppService/BranchAppService with loading/error states. Want me to open an issue and prepare the patch?

packages/plugins/versioncontrol/src/js/index.ts (3)

8-17: Expose interfaces on the public surface, not concrete implementations

Typing fields as the interface contracts improves encapsulation and swap-ability in tests.

-import { BranchAppServiceImpl } from './application/service/BranchAppService'
-import { CommitAppServiceImpl } from './application/service/CommitAppService'
-import { BranchServiceImpl } from './domain/services/BranchService'
-import { CommitServiceImpl } from './domain/services/CommitService'
+import { BranchAppServiceImpl } from './application/service/BranchAppService'
+import type { BranchAppService } from './application/service/BranchAppService'
+import { CommitAppServiceImpl } from './application/service/CommitAppService'
+import type { CommitAppService } from './application/service/CommitAppService'
+import { BranchServiceImpl } from './domain/services/BranchService'
+import type { BranchService } from './domain/services/BranchService'
+import { CommitServiceImpl } from './domain/services/CommitService'
+import type { CommitService } from './domain/services/CommitService'
 import { BranchRepositoryImpl } from './infrastructure/repositories/BranchRepository'
+import type { BranchRepository } from './infrastructure/repositories/BranchRepository'
 import { CommitRepositoryImpl } from './infrastructure/repositories/CommitRepository'
+import type { CommitRepository } from './infrastructure/repositories/CommitRepository'
 
 export class VersionManager {
-  commitService: CommitServiceImpl
-  branchService: BranchServiceImpl
+  commitService: CommitService
+  branchService: BranchService
 
-  commitRepository: CommitRepositoryImpl
-  branchRepository: BranchRepositoryImpl
+  commitRepository: CommitRepository
+  branchRepository: BranchRepository
 
-  commitAppService: CommitAppServiceImpl
-  branchAppService: BranchAppServiceImpl
+  commitAppService: CommitAppService
+  branchAppService: BranchAppService

18-33: Allow dependency injection for testability/configurability

Hard-coding new XImpl() here makes unit testing harder and prevents swapping implementations. Consider constructor params with sensible defaults.

-  constructor() {
-    this.commitService = new CommitServiceImpl()
-    this.branchService = new BranchServiceImpl()
-    this.commitRepository = new CommitRepositoryImpl()
-    this.branchRepository = new BranchRepositoryImpl()
-    this.commitAppService = new CommitAppServiceImpl(this.commitService, this.branchRepository, this.commitRepository)
-    this.branchAppService = new BranchAppServiceImpl(
+  constructor(
+    commitService: CommitService = new CommitServiceImpl(),
+    branchService: BranchService = new BranchServiceImpl(),
+    commitRepository: CommitRepository = new CommitRepositoryImpl(),
+    branchRepository: BranchRepository = new BranchRepositoryImpl()
+  ) {
+    this.commitService = commitService
+    this.branchService = branchService
+    this.commitRepository = commitRepository
+    this.branchRepository = branchRepository
+    this.commitAppService = new CommitAppServiceImpl(this.commitService, this.branchRepository, this.commitRepository)
+    this.branchAppService = new BranchAppServiceImpl(
       this.branchService,
       this.commitAppService,
       this.commitRepository,
       this.branchRepository
     )
   }

36-37: Singleton export caveat in micro-frontend builds

Exporting a module-level singleton is convenient, but duplicated bundles or differing resolutions can create multiple instances in-app. If stability is critical, consider a getInstance() or a global symbol guard.

packages/plugins/versioncontrol/src/components/VersionControlFilters.vue (2)

36-42: Tighten prop types and defaults

Use PropType to enforce expected types and provide defaults for counts/arrays.

-const props = defineProps({
-  authorFilter: String,
-  timeFilter: String,
-  uniqueAuthors: Array,
-  filteredCommitsLength: Number,
-  uniqueAuthorsLength: Number
-})
+import { defineProps, defineEmits, computed, PropType } from 'vue'
+const props = defineProps({
+  authorFilter: { type: String, default: '' },
+  timeFilter: { type: String, default: '' },
+  uniqueAuthors: { type: Array as PropType<string[]>, default: () => [] },
+  filteredCommitsLength: { type: Number, default: 0 },
+  uniqueAuthorsLength: { type: Number, default: 0 }
+})

5-21: Minor a11y: label association/aria-labels

Add aria-labels to selects to improve accessibility (labels are not explicitly associated via for/id).

-      <select v-model="authorFilter" @change="applyFilters" class="filter-select">
+      <select v-model="authorFilter" @change="applyFilters" class="filter-select" aria-label="作者筛选">
...
-      <select v-model="timeFilter" @change="applyFilters" class="filter-select">
+      <select v-model="timeFilter" @change="applyFilters" class="filter-select" aria-label="时间范围筛选">
packages/plugins/versioncontrol/src/components/VersionHeader.vue (1)

11-16: Emit payloads for branch-change and search for easier consumption

Send selected values with the events to reduce parent coupling.

-        <select v-model="modelCurrentBranch" @change="onBranchChange" class="branch-select">
+        <select v-model="modelCurrentBranch" @change="onBranchChange(modelCurrentBranch)" class="branch-select">
...
-          @input="onSearch"
+          @input="onSearch(modelSearchQuery)"
...
-    const onBranchChange = () => emit('branch-change')
-    const onSearch = () => emit('search')
+    const onBranchChange = (branch) => emit('branch-change', branch)
+    const onSearch = (q) => emit('search', q)

Also applies to: 103-109, 21-23

packages/plugins/versioncontrol/src/components/VersionBranchCreate.vue (1)

18-24: Small UX nits

  • Add autocomplete="off" on the name input to reduce noise.
  • Keep the disabled state but also trim on input to prevent accidental spaces.
-          <input
+          <input
             v-model="modelNewBranchName"
             placeholder="例如: feature/my-new-feature"
             class="form-input"
+            autocomplete="off"
             @keyup.enter="confirmCreateBranch"
           />

Also applies to: 37-40

packages/plugins/versioncontrol/src/composable/useVersionControlUtils.ts (2)

80-88: Remove commented-out code

The watch statements are commented out and don't provide any functionality. Consider removing them entirely or implementing the actual logic if needed.

-  // 监听器
-  watch(currentBranch, () => {
-    // 当分支改变时,可以触发一些数据重新加载或UI更新
-    // console.log(`当前分支已切换到: ${currentBranch.value}`)
-  })
-
-  watch(searchQuery, () => {
-    // 当搜索查询改变时,可以触发一些数据重新加载或UI更新
-    // console.log(`搜索查询已更新: ${searchQuery.value}`)
-  })

28-77: Extract type mappings as constants for better maintainability

The type mappings are duplicated between getCommitTypePrefix and getCommitTypeText. Consider extracting them as module-level constants to avoid duplication and improve maintainability.

+const COMMIT_TYPE_PREFIXES: Record<string, string> = {
+  feat: '特性',
+  fix: '修复',
+  docs: '文档',
+  style: '样式',
+  refactor: '重构',
+  perf: '性能',
+  test: '测试',
+  build: '构建',
+  ci: 'CI/CD',
+  chore: '杂项',
+  revert: '回滚',
+  merge: '合并'
+}
+
+const COMMIT_TYPE_TEXTS: Record<string, string> = {
+  feat: '新功能',
+  fix: 'Bug修复',
+  docs: '文档更新',
+  style: '代码风格',
+  refactor: '代码重构',
+  perf: '性能优化',
+  test: '测试相关',
+  build: '构建系统',
+  ci: '持续集成',
+  chore: '日常事务',
+  revert: '版本回滚',
+  merge: '分支合并'
+}

 const getCommitTypePrefix = (type: string): string => {
-  const typeMap: { [key: string]: string } = {
-    feat: '特性',
-    fix: '修复',
-    // ... rest of the mappings
-  }
-  return typeMap[type] || type
+  return COMMIT_TYPE_PREFIXES[type] || type
 }

 const getCommitTypeText = (type: string): string => {
-  const typeMap: { [key: string]: string } = {
-    feat: '新功能',
-    fix: 'Bug修复',
-    // ... rest of the mappings
-  }
-  return typeMap[type] || '其他'
+  return COMMIT_TYPE_TEXTS[type] || '其他'
 }
packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1)

160-170: Consider memoizing date formatting for performance

The formatDate function creates new options object on every call. Consider moving the options outside or using a memoized version.

+    const dateFormatOptions = {
+      year: 'numeric',
+      month: '2-digit',
+      day: '2-digit',
+      hour: '2-digit',
+      minute: '2-digit',
+      second: '2-digit'
+    }
+
     const formatDate = (dateString) => {
       const date = new Date(dateString)
-      return date.toLocaleString('zh-CN', {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-        hour: '2-digit',
-        minute: '2-digit',
-        second: '2-digit'
-      })
+      return date.toLocaleString('zh-CN', dateFormatOptions)
     }
packages/plugins/versioncontrol/src/components/TimelineContainer.vue (2)

84-87: Use shared utility function for date formatting

The formatTime function duplicates date formatting logic. Consider using the shared utility from useVersionControlUtils.

+import { useVersionControlUtils } from '../composable/useVersionControlUtils'
+import { ref } from 'vue'

 const props = defineProps({
   timelineView: String,
   filteredCommits: Array,
   selectedCommit: Object
 })

 const emit = defineEmits(['update:timelineView', 'selectCommit'])

+// Use the composable for utility functions
+const currentBranch = ref('')
+const searchQuery = ref('')
+const { formatTime } = useVersionControlUtils({ currentBranch, searchQuery })

 // ... existing code ...

-const formatTime = (dateString) => {
-  const options = { hour: '2-digit', minute: '2-digit' }
-  return new Date(dateString).toLocaleTimeString('zh-CN', options)
-}

89-100: Consider extracting commit type classes to shared constants

The getCommitTypeClass function returns an object with multiple boolean properties, but only one class will be active at a time. Consider simplifying this to return a single class string.

 const getCommitTypeClass = (commit) => {
-  return {
-    'merge-commit': commit.type === 'merge',
-    'tag-commit': commit.tags && commit.tags.length > 0,
-    'feature-commit': commit.type === 'feature',
-    'bugfix-commit': commit.type === 'bugfix',
-    'docs-commit': commit.type === 'docs',
-    'refactor-commit': commit.type === 'refactor',
-    'style-commit': commit.type === 'style',
-    'test-commit': commit.type === 'test'
-  }
+  if (commit.type === 'merge') return 'merge-commit'
+  if (commit.tags && commit.tags.length > 0) return 'tag-commit'
+  
+  const typeClassMap = {
+    'feature': 'feature-commit',
+    'bugfix': 'bugfix-commit',
+    'docs': 'docs-commit',
+    'refactor': 'refactor-commit',
+    'style': 'style-commit',
+    'test': 'test-commit'
+  }
+  
+  return typeClassMap[commit.type] || 'default-commit'
 }
packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (3)

394-405: Memoize cache invalidation risk for reads

@Memoize on getBranch/getAllBranches can serve stale data after updates (rename, protect/unprotect, archive/unarchive, restore). Either add explicit cache invalidation hooks post-update or avoid memoizing these reads.


274-279: Merge commit author should be the operator, not source branch creator

Using sourceBranch.creator misattributes authorship. Prefer an explicit operator: User parameter or derive from session/context.

Example:

-    commitMessage || `Merge branch '${sourceBranch.name}' into '${targetBranch.name}'`,
-    sourceBranch.creator,
+    commitMessage || `Merge branch '${sourceBranch.name}' into '${targetBranch.name}'`,
+    operator, // pass from method parameter/context

If you want, I can refactor the API to accept operator: User.


505-511: No-op early return on identical rename

Avoid extra I/O and uniqueness checks when newName equals the current name.

-    if (await this.branchRepository.findByName(newName)) {
+    if (branch.name === newName) {
+      return
+    }
+    if (await this.branchRepository.findByName(newName)) {
       throw new Error(`Branch with name ${newName} already exists.`)
     }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c8101a and 4e6d03b.

⛔ Files ignored due to path filters (3)
  • mockServer/src/database/branch.db is excluded by !**/*.db
  • mockServer/src/database/commit.db is excluded by !**/*.db
  • mockServer/src/database/pages.db is excluded by !**/*.db
📒 Files selected for processing (17)
  • mockServer/src/services/branch.js (1 hunks)
  • mockServer/src/services/commit.js (1 hunks)
  • mockServer/src/services/mockService.js (3 hunks)
  • packages/plugins/versioncontrol/src/Main.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/CommitsContainer.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/TimelineContainer.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionBranchCreate.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionControlFilters.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionHeader.vue (1 hunks)
  • packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useVersionControlUtils.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/index.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts
  • packages/plugins/versioncontrol/src/Main.vue
🧰 Additional context used
🧬 Code Graph Analysis (6)
mockServer/src/services/mockService.js (2)
mockServer/src/services/commit.js (1)
  • CommitService (15-69)
mockServer/src/services/branch.js (1)
  • BranchService (15-76)
packages/plugins/versioncontrol/src/js/index.ts (6)
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1)
  • CommitServiceImpl (84-257)
packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (1)
  • BranchServiceImpl (113-367)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1)
  • CommitRepositoryImpl (105-135)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (1)
  • BranchRepositoryImpl (100-130)
packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (1)
  • CommitAppServiceImpl (102-291)
packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (1)
  • BranchAppServiceImpl (140-512)
packages/plugins/versioncontrol/src/composable/useVersionControlUtils.ts (1)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)
  • Commit (4-17)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (4)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (2)
  • Commit (4-17)
  • CompareData (19-26)
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1)
  • applyFilters (221-256)
packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (1)
  • createBranch (161-200)
packages/toolbars/save/src/js/index.ts (1)
  • isLoading (40-40)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)
packages/toolbars/save/src/js/index.ts (1)
  • isLoading (40-40)
packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (10)
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (3)
  • upstreamBranchId (83-85)
  • commitsAhead (107-109)
  • commitsBehind (111-113)
packages/plugins/versioncontrol/src/js/shared/type.ts (11)
  • ID (7-7)
  • User (17-25)
  • Branch (110-129)
  • MergeStrategy (187-187)
  • ConflictReport (293-303)
  • BranchStatusResponse (230-244)
  • BranchOperationHistoryResponse (271-276)
  • CommitHistoryRequest (336-344)
  • CommitHistoryResponse (349-356)
  • BranchProtectionRule (140-146)
  • BranchOperationHistory (249-266)
packages/plugins/versioncontrol/src/js/domain/models/Schema.ts (1)
  • creator (42-44)
mockServer/src/services/branch.js (1)
  • BranchService (15-76)
packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (1)
  • BranchService (19-108)
packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (1)
  • CommitAppService (21-96)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1)
  • CommitRepository (23-30)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (1)
  • BranchRepository (23-30)
packages/plugins/versioncontrol/src/js/shared/validation.ts (1)
  • Validator (6-82)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)
  • Memoize (51-101)
🔇 Additional comments (6)
mockServer/src/services/mockService.js (1)

19-21: Wiring CommitService and BranchService looks correct

Both services are properly imported, exposed as class fields, and initialized in the constructor. No API regressions observed here.

Also applies to: 29-31, 40-42

mockServer/src/services/commit.js (1)

48-52: Ambiguity in CommitService.update between internal _id and model id

It looks like your CommitService.commitModel defines an id field, but all CRUD methods—including update(id, params)—only ever match on NeDB’s internal _id. If callers supply the model’s id property (not the DB’s _id), their update will silently no-op.

Please verify how your routes/controllers pass that id argument. If you intend to let clients refer to commits by the id property in your model (instead of NeDB’s _id), you should either:

  • Update your query to support both:
    const query = { $or: [{ _id: id }, { id }] }
    await this.db.updateAsync(query, { $set: params });
    const result = await this.db.findOneAsync(query);
  • Or clearly document that id must be the internal _id, and consider renaming the parameter to _id for clarity.
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)

30-37: Default branch mismatch with header component

Here currentBranch defaults to 'all', while VersionHeader.vue defaults currentBranch to 'main'. This leads to inconsistent initial UI state.

Consider aligning both to the same default (recommend 'all' if you support cross-branch filtering), or remove the default at one end to rely on the other. Do you want a patch to normalize both?

packages/plugins/versioncontrol/src/components/VersionHeader.vue (2)

73-75: Default branch conflicts with data composable (‘main’ vs ‘all’)

Composable defaults to 'all', header defaults to 'main'. Choose one to avoid inconsistent initial states.

-    currentBranch: {
+    currentBranch: {
       type: String,
-      default: 'main'
+      default: 'all'
     },

5-6: Verify LinkButton props

Ensure LinkButton supports an href prop (and optionally target="_blank" rel), or adapt to its expected API.

packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (1)

223-228: Confirm destroy semantics: update vs delete

destroyBranch calls branchService.destroyBranch(branch) then updates the record. If the intended effect is hard-delete, consider branchRepository.delete(branch.id). If soft-delete (status/tombstone), current approach is fine but ensure status changes are applied inside the domain service.

Would you confirm whether destroy should hard-delete or soft-delete? I can adjust the repository call accordingly.

Comment thread mockServer/src/services/branch.js
Comment thread mockServer/src/services/branch.js
Comment thread mockServer/src/services/branch.js
Comment thread mockServer/src/services/branch.js Outdated
Comment thread mockServer/src/services/commit.js
Comment thread packages/plugins/versioncontrol/src/composable/useVersionControlUtils.ts Outdated
Comment thread packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts Outdated

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 34

♻️ Duplicate comments (28)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (4)

35-44: Don’t swallow errors; surface them to callers.

Catching, logging, and returning null hides failures from application/services and impedes retries or UX messaging.

Apply this diff:

   async fetchBranchById(id: ID): Promise<Branch | null> {
     try {
       const res = await getMetaApi(META_SERVICE.Http).get(`${BASE_URL}/getById/${id}`)
       return res || null
     } catch (error) {
-      // eslint-disable-next-line no-console
-      console.error('Failed to fetch branch by ID:', error)
-      return null
+      console.error('Failed to fetch branch by ID:', error)
+      throw new Error(`fetchBranchById(${id}) failed`)
     }
   }

46-55: Same: propagate fetch-by-name errors.

   async fetchBranchByName(name: string): Promise<Branch | null> {
     try {
       const res = await getMetaApi(META_SERVICE.Http).get(`${BASE_URL}/getByName/${encodeURIComponent(name)}`)
       return res || null
     } catch (error) {
-      // eslint-disable-next-line no-console
-      console.error('Failed to fetch branch by name:', error)
-      return null
+      console.error('Failed to fetch branch by name:', error)
+      throw new Error(`fetchBranchByName(${name}) failed`)
     }
   }

57-66: Hardcoded appId in list URL and inconsistent response handling.

/list/${1} bakes in appId. Make it a parameter with a safe default, and propagate errors. Also ensure consistent handling of the HTTP client response (res vs res.data) across methods.

-  async fetchAllBranches() {
+  async fetchAllBranches(appId: ID = '1'): Promise<Branch[]> {
     try {
-      const res = await getMetaApi(META_SERVICE.Http).get(`/app-center/api/version/branch/list/${1}`)
+      const res = await getMetaApi(META_SERVICE.Http).get(`/app-center/api/version/branch/list/${encodeURIComponent(appId)}`)
       return res || []
     } catch (error) {
-      // eslint-disable-next-line no-console
-      console.error('Failed to fetch all branches:', error)
-      return []
+      console.error('Failed to fetch all branches:', error)
+      throw new Error('fetchAllBranches failed')
     }
   }

Follow-up: if you adopt this, update BranchRepository.findAll to accept an optional appId and pass it through (see later diff).


86-93: Use HTTP DELETE for delete operation and surface errors.

Using GET for destructive actions breaks REST and may be cached.

   async deleteBranch(id: ID): Promise<void> {
     try {
-      await getMetaApi(META_SERVICE.Http).get(`${BASE_URL}/delete/${id}`)
+      await getMetaApi(META_SERVICE.Http).delete(`${BASE_URL}/${id}`)
     } catch (error) {
-      // eslint-disable-next-line no-console
-      console.error('Failed to delete branch:', error)
+      console.error('Failed to delete branch:', error)
+      throw error
     }
   }
packages/plugins/versioncontrol/src/js/shared/utils.ts (2)

62-66: Cache key generation with JSON.stringify(args) is brittle.

Order of object keys and circular-refs can break caching. Prefer a stable, shallow key.

-      const key = JSON.stringify(args)
+      const key = args
+        .map((arg) => {
+          if (arg === null || arg === undefined) return String(arg)
+          const t = typeof arg
+          if (t === 'string' || t === 'number' || t === 'boolean') return String(arg)
+          if (arg instanceof Date) return arg.toISOString()
+          try {
+            return JSON.stringify(arg, Object.keys(arg).sort())
+          } catch {
+            return Object.prototype.toString.call(arg)
+          }
+        })
+        .join('::')

88-93: Don’t cache rejected promises.

Failed calls should not poison the cache. Pass through the rejection.

-      if (result instanceof Promise) {
-        return result.then((resolved) => {
-          saveToCache(resolved)
-          return resolved
-        })
-      } else {
+      if (result instanceof Promise) {
+        return result
+          .then((resolved) => {
+            saveToCache(resolved)
+            return resolved
+          })
+          .catch((err) => {
+            // do not cache failures
+            throw err
+          })
+      } else {
         saveToCache(result)
         return result
       }
mockServer/src/services/branch.js (2)

27-46: Static createdAt/updatedAt in the model cause stale timestamps.

These are set once at service construction, not per record.

     this.branchModel = {
       id: '',
       name: '',
       type: 'feature', // 默认
       status: 'active', //默认
       headCommitId: '',
       baseCommitId: '',
       creator: { id: 0, username: '', email: '' },
-      createdAt: Date.now(),
-      updatedAt: Date.now(),
+      createdAt: undefined,
+      updatedAt: undefined,
       description: '',
       upstreamBranchId: undefined,
       downstreamBranchIds: [],
       lastCommitAt: undefined,
       commitsCount: 0,
       commitsAhead: 0,
       commitsBehind: 0,
       protection: undefined,
       metadata: {}
     }

66-70: Update should refresh updatedAt and match by either id or _id.

Current code misses updatedAt and will fail if the caller passes _id.

   async update(id, params) {
-    await this.db.updateAsync({ id: id }, { $set: params })
-    const result = await this.db.findOneAsync({ id: id })
+    const $set = { ...params, updatedAt: Date.now() }
+    await this.db.updateAsync({ $or: [{ id }, { _id: id }] }, { $set })
+    const result = await this.db.findOneAsync({ $or: [{ id }, { _id: id }] })
     return getResponseData(result)
   }
packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1)

83-90: Fix undefined variable in click handler.

Template references selectedCommit, which isn’t defined in setup. Use modelSelectedCommit.

Apply this diff:

-          <button @click="createBranchFromCommit(selectedCommit)" class="action-btn">
+          <button @click="createBranchFromCommit(modelSelectedCommit)" class="action-btn">
mockServer/src/services/commit.js (1)

58-62: Generate stable id/hash and set dynamic timestamp at insert to avoid unique index collisions.

Inserting with id: '' will violate the unique index on subsequent creates. Also ensure timestamp and hash are set per commit.

Apply this diff:

-  async create(params) {
-    const commitData = { app: '1', ...this.commitModel, ...params }
-    const result = await this.db.insertAsync(commitData)
-    return getResponseData(result)
-  }
+  async create(params) {
+    const now = Date.now()
+    const id = params?.id || `${now}-${Math.random().toString(36).slice(2, 10)}`
+    const commitData = {
+      app: '1',
+      ...this.commitModel,
+      ...params,
+      id,
+      hash: params?.hash || id, // simple placeholder; replace with real hash if available
+      timestamp: params?.timestamp || now,
+      // normalize stats shape if caller passed legacy keys
+      stats: params?.stats?.totalAdditions !== undefined
+        ? params.stats
+        : {
+            totalAdditions: params?.stats?.added || 0,
+            totalDeletions: params?.stats?.deleted || 0,
+            changedFiles: params?.stats?.changedFiles || []
+          }
+    }
+    const result = await this.db.insertAsync(commitData)
+    return getResponseData(result)
+  }
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (3)

133-141: Fix JSDoc to match parameters (commitA/commitB) and add types.

Comment mentions commitIdA/commitIdB, which is misleading.

-  /**
-   * 获取提交差异
-   * @param commitIdA
-   * @param commitIdB
-   * @returns
-   */
+  /**
+   * 获取提交差异
+   * @param commitA 第一个提交
+   * @param commitB 第二个提交
+   * @returns 差异结果
+   */

144-155: Fix JSDoc parameter names/order for generateSnapshot.

Matches actual signature (commit, commitId).

-  /**
-   * 生成快照 (即获取该提交的 PageSchema)
-   * @param commitId
-   * @returns
-   */
+  /**
+   * 生成快照 (即获取该提交的 PageSchema)
+   * @param commit 提交对象
+   * @param commitId 提交ID
+   * @returns 快照对象
+   */

157-166: Fix JSDoc parameter for verifyCommit.

It accepts commit, not commitId.

-  /**
-   * 验证提交签名
-   * @param commitId
-   * @returns
-   */
+  /**
+   * 验证提交签名
+   * @param commit 提交对象
+   * @returns 是否验证成功
+   */
packages/plugins/versioncontrol/src/components/CommitsContainer.vue (1)

247-268: DRY: Reuse utilities for formatDate/getCommitTypePrefix instead of duplicating

This duplicates logic that belongs in a composable (e.g., useVersionControlUtils). Import and use the shared helpers.

-<script setup>
-import { defineProps, defineEmits, computed } from 'vue'
+<script setup>
+import { defineProps, defineEmits, computed } from 'vue'
+import { useVersionControlUtils } from '../composable/useVersionControlUtils'
@@
-const formatDate = (dateString) => {
-  const options = { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }
-  return new Date(dateString).toLocaleString('zh-CN', options)
-}
-
-const getCommitTypePrefix = (type) => {
-  const typeMap = {
-    feat: '特性',
-    fix: '修复',
-    docs: '文档',
-    style: '样式',
-    refactor: '重构',
-    perf: '性能',
-    test: '测试',
-    build: '构建',
-    ci: 'CI/CD',
-    chore: '杂项',
-    revert: '回滚',
-    merge: '合并'
-  }
-  return typeMap[type] || type
-}
+const { formatDate, getCommitTypePrefix } = useVersionControlUtils()

If the composable path or API differs, I can adjust the import to match your project.

packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1)

45-60: Validate limit/offset to prevent bad requests

Guard against negative or non-integer values before sending params.

   async fetchCommitsByBranchId(branchId: ID, limit?: number, offset?: number): Promise<Commit[]> {
+    if (limit !== undefined && (limit < 0 || !Number.isInteger(limit))) {
+      throw new Error('Limit must be a non-negative integer')
+    }
+    if (offset !== undefined && (offset < 0 || !Number.isInteger(offset))) {
+      throw new Error('Offset must be a non-negative integer')
+    }
     try {
       const res = await getMetaApi(META_SERVICE.Http).get(`${BASE_URL}/listByBranch`, {
         params: {
           branchId,
           limit,
           offset
         }
       })
       return res || []
packages/plugins/versioncontrol/src/components/VersionHeader.vue (1)

289-304: Fix malformed media query (@media (max-width))

This is invalid CSS and will cause the rule to be dropped. Set a specific width.

-@media (max-width) {
+@media (max-width: 600px) {
   .version-control-header {
     flex-direction: column;
     gap: 12px;
     align-items: stretch;
@@
   }
 }
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (2)

28-29: Use a precise type for CompareData.changedFiles

Align with DisplayCommit.changedFiles which is string[].

-  changedFiles: any[]
+  changedFiles: string[]

31-43: Filename typo breaks imports and discoverability

The composable is exported as useVersionControlData but the file is named uesVersionControlData.ts.

  • Rename file:
- uesVersionControlData.ts
+ useVersionControlData.ts
  • Update imports that reference the old path (e.g., Main.vue).

I can generate a repo-wide rename and import-fix script if you want me to automate this.

packages/plugins/versioncontrol/src/components/VersionBranchCreate.vue (2)

37-39: Fix undefined ‘commits’ in template; expose a reactive prop ref to the template.

The template uses commits but it’s not in the component’s return scope. Use toRefs(props) to expose a reactive commits ref, and reference it in the template.

-            <option v-for="commit in commits" :key="commit.id" :value="commit.id">
+            <option v-for="commit in commits" :key="commit.id" :value="commit.id">
               {{ commit.hash.slice(0, 7) }} - {{ commit.message.slice(0, 50) }}
             </option>

Add in script:

-import { computed, onMounted, ref } from 'vue'
+import { computed, onMounted, ref, toRefs } from 'vue'
@@
   setup(props, { emit }) {
@@
-    // 双向绑定计算属性
+    // 双向绑定计算属性
+    const { commits } = toRefs(props)
@@
     return {
+      commits,
       description,
       upstreamBranchId,
       modelBranchDialogVisible,
       modelNewBranchName,
       modelBranchTargetCommit,
       modelAvailableBranches,
       buttonDisable,
       handleSubmit,
       closeBranchDialog,
       confirmCreateBranch
     }

Also applies to: 56-61, 92-102


21-22: Make Enter submit the form and emit payload for parent.

Enter triggers confirm-create-branch without data; wire it to handleSubmit or emit payload as suggested earlier.

Option A — submit on Enter:

-            @keyup.enter="confirmCreateBranch"
+            @keyup.enter="handleSubmit"

Option B — keep confirm event but include payload:

-    const confirmCreateBranch = () => emit('confirm-create-branch')
+    const confirmCreateBranch = () =>
+      emit('confirm-create-branch', {
+        name: modelNewBranchName.value.trim(),
+        baseCommit: modelBranchTargetCommit.value || null,
+        upstreamBranchId: upstreamBranchId.value || null,
+        description: description.value || ''
+      })

Also applies to: 105-106, 133-153

packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (1)

126-129: Typo in validation label: “Athor Id” → “Author Id”.

This leaks to error messages and looks unpolished.

-      [author.id, 'Athor Id'],
+      [author.id, 'Author Id'],
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (2)

108-113: Replace confirm/alert flows with component-friendly events.

Direct browser dialogs harm UX and block tests. Emit intent and let the parent show proper modals.

-  const revertToCommit = (commit: DisplayCommit) => {
-    if (confirm(`确定要回滚到提交 ${commit.hash.slice(0, 7)} 吗?这将撤销所有后续更改。`)) {
-      alert(`已回滚到提交 ${commit.hash.slice(0, 7)}。 (模拟操作)`)
-      // 实际操作中会调用后端API执行回滚
-    }
-  }
+  const revertToCommit = (commit: DisplayCommit) => {
+    emit('confirm-revert', commit)
+  }
@@
-  const createTag = () => {
-    tagDialogVisible.value = true
-    tagTargetCommit.value = selectedCommit.value ? selectedCommit.value.hash : commits.value[0].hash // 默认当前选中或最新提交
-  }
+  const createTag = () => {
+    tagDialogVisible.value = true
+    tagTargetCommit.value = selectedCommit.value?.hash ?? commits.value?.[0]?.hash ?? ''
+    if (!tagTargetCommit.value) emit('validation-error', '没有可用提交可用于创建标签')
+  }
@@
-  const confirmCreateBranch = () => {
-    if (branchTargetCommit && newBranchName) {
-      alert(`已基于提交 ${branchTargetCommit.value} 创建新分支:${newBranchName.value} (模拟操作)`)
-      branches.value.push(newBranchName.value)
-      currentBranch.value = newBranchName.value
-      // 实际操作中会调用后端API创建分支
-    }
-    branchDialogVisible.value = false
-  }
+  const confirmCreateBranch = () => {
+    if (newBranchName.value?.trim()) {
+      emit('confirm-create-branch', {
+        name: newBranchName.value.trim(),
+        baseCommit: branchTargetCommit.value || null
+      })
+    } else {
+      emit('validation-error', '分支名称不能为空')
+    }
+    branchDialogVisible.value = false
+  }

Also applies to: 120-123, 145-153


145-148: Bug: refs are compared instead of their values.

This never validates as intended; check .value.

-    if (branchTargetCommit && newBranchName) {
+    if (branchTargetCommit.value && newBranchName.value) {

Note: The broader refactor above removes this check entirely by emitting a payload; if you keep this branch, fix it.

packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (5)

438-471: Remove unnecessary await on synchronous domain methods.

BranchService methods are synchronous; awaiting them is misleading.

-    await this.branchService.unprotectBranch(branch)
+    this.branchService.unprotectBranch(branch)
@@
-    await this.branchService.archiveBranch(branch)
+    this.branchService.archiveBranch(branch)
@@
-    await this.branchService.unarchiveBranch(branch)
+    this.branchService.unarchiveBranch(branch)

48-54: Unify return DTO with domain service: add isConflicted and rename to conflictedFiles.

Public API should mirror BranchService to avoid consumer confusion.

-  ): Promise<{ newCommitId?: ID; conflictedReports?: ConflictReport[] }>
+  ): Promise<{ newCommitId?: ID; isConflicted?: boolean; conflictedFiles?: ConflictReport[] }>
@@
-  ): Promise<{ newCommitId?: ID; conflictedReports?: ConflictReport[] }>
+  ): Promise<{ newCommitId?: ID; isConflicted?: boolean; conflictedFiles?: ConflictReport[] }>

Adjust implementation return shapes accordingly (see below).

Also applies to: 61-62


185-193: Null-check base commit and avoid non-null assertions; also verify Branch.fromData vs formData.

createBranch uses baseCommit!.id which can crash; fetch a single baseCommitId and validate.

-    const baseCommit = commitId
-      ? await this.commitAppService.getCommit(commitId)
-      : await this.commitAppService.getCommit(upstreamBranch.headCommitId)
-
-    // 事务开始
-    const newBranch = this.branchService.createBranch(name, upstreamBranchId, creator, baseCommit!.id, description)
+    const baseCommitId = commitId ?? upstreamBranch.headCommitId
+    const baseCommit = await this.commitAppService.getCommit(baseCommitId)
+    if (!baseCommit) {
+      throw new Error(`Base commit with id ${baseCommitId} not found.`)
+    }
+    // 事务开始
+    const newBranch = this.branchService.createBranch(name, upstreamBranchId, creator, baseCommit.id, description)
@@
-    const updateBranchInstance = Branch.formData(upstreamBranch)
+    const updateBranchInstance = (Branch as any).fromData ? (Branch as any).fromData(upstreamBranch) : (Branch as any).formData(upstreamBranch)
#!/bin/bash
# Check Branch.{fromData|formData} presence and usage
rg -nP 'class\s+Branch\b' -C2
rg -nP 'Branch\.(fromData|formData)\s*\(' -C2

Also applies to: 190-197


274-289: Persist head update on successful merge; normalize conflict return.

Missing update causes state loss; also align return DTO naming.

-    if (!result?.isConflicted) {
+    if (!result?.isConflicted) {
       // 创建新的合并提交
@@
-      targetBranch.setHeadCommitId(newCommit.id) // 更新头提交
-      return { newCommitId: newCommit.id }
+      targetBranch.setHeadCommitId(newCommit.id) // 更新头提交
+      await this.branchRepository.update(targetBranch)
+      return { newCommitId: newCommit.id, isConflicted: false }
     }
 
-    await this.branchRepository.update(targetBranch)
-    return result
+    await this.branchRepository.update(targetBranch)
+    return { isConflicted: true, conflictedFiles: result.conflictedFiles }

346-352: Fix error message to reference the commit ID, not the object.

Interpolating lastCommit object results in “[object Object]”.

-    if (!lastCommit) {
-      throw new Error(`Commit with id ${lastCommit} not found.`)
-    }
+    if (!lastCommit) {
+      throw new Error(`Commit with id ${branch.headCommitId} not found.`)
+    }
🧹 Nitpick comments (48)
packages/plugins/versioncontrol/src/components/MergeBranchDialog.vue (3)

2-5: Add dialog accessibility: role/aria and label association.

Declare dialog semantics and associate the header as label.

Apply this diff:

-  <div class="merge-dialog-overlay" v-if="mergeBranchVisible">
-    <div class="merge-dialog-container">
-      <h2 class="dialog-title">合并分支</h2>
+  <div class="merge-dialog-overlay" v-if="mergeBranchVisible">
+    <div class="merge-dialog-container" role="dialog" aria-modal="true" aria-labelledby="merge-dialog-title">
+      <h2 id="merge-dialog-title" class="dialog-title">合并分支</h2>

47-47: Avoid using array index as key for conflicts list.

Use a stable key to prevent DOM reuse issues while resolving conflicts.

Apply this diff:

-        <div v-for="(conflict, index) in conflicts" :key="index" class="conflict-item">
+        <div v-for="conflict in conflicts" :key="conflict.filePath" class="conflict-item">

56-57: Buttons inside interactive sections should specify type="button".

Avoid accidental form submissions and improve intent clarity.

Apply this diff:

-              <button @click="resolveConflict(conflict, 'target')">保留我的更改</button>
+              <button type="button" @click="resolveConflict(conflict, 'target')">保留我的更改</button>
-              <button @click="resolveConflict(conflict, 'source')">接受对方的更改</button>
+              <button type="button" @click="resolveConflict(conflict, 'source')">接受对方的更改</button>
-            <button @click="resolveConflict(conflict, 'manual')">确认手动解决</button>
+            <button type="button" @click="resolveConflict(conflict, 'manual')">确认手动解决</button>
-          <button @click="submitResolvedConflicts" :disabled="isLoading" class="submit-button">
+          <button type="button" @click="submitResolvedConflicts" :disabled="isLoading" class="submit-button">
-          <button type="button" @click="closeDialog" :disabled="isLoading" class="cancel-button">取消</button>
+          <button type="button" @click="closeDialog" :disabled="isLoading" class="cancel-button">取消</button>

Also applies to: 61-62, 71-72, 75-79

packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (3)

68-75: Avoid assuming a domain instance with toData(); accept DTOs or guard at runtime.

If callers pass plain objects, branch.toData() will throw. Either move serialization to the application layer or guard.

   async saveBranch(branch: Branch): Promise<void> {
     try {
-      await getMetaApi(META_SERVICE.Http).post(`${BASE_URL}/create`, branch.toData())
+      // Support both domain entities (with toData) and plain DTOs
+      // @ts-expect-error runtime guard for flexibility
+      const payload = typeof (branch as any)?.toData === 'function' ? (branch as any).toData() : branch
+      await getMetaApi(META_SERVICE.Http).post(`${BASE_URL}/create`, payload)
     } catch (error) {
       // eslint-disable-next-line no-console
       console.error('Failed to save branch:', error)
+      throw error
     }
   }

Apply the same guard in updateBranch.


102-105: Allow dependency injection of BranchApi for testability and flexibility.

Constructing BranchApiImpl internally makes mocking hard.

-  constructor() {
-    this.branchApi = new BranchApiImpl()
-  }
+  constructor(branchApi: BranchApi = new BranchApiImpl()) {
+    this.branchApi = branchApi
+  }

118-120: Pass appId through to Api after parameterizing fetchAllBranches.

If you accept the earlier change, thread appId here.

-  async findAll(): Promise<Branch[]> {
-    return await this.branchApi.fetchAllBranches()
+  async findAll(appId: ID = '1'): Promise<Branch[]> {
+    return await this.branchApi.fetchAllBranches(appId)
   }
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)

108-131: Ensure Web Crypto availability in both browser and Node environments

It looks like your root tsconfig.json references two builds:

  • tsconfig.app.json (includes "DOM", so crypto.subtle is typed and available in browser builds).
  • tsconfig.node.json (no "DOM" lib, Node–only settings, so crypto.subtle may be undefined at runtime)

Since the versioncontrol plugin runs under Node (e.g. in a CLI or server context), you need to guard against crypto.subtle being absent and fall back to Node’s WebCrypto API:

• File: packages/plugins/versioncontrol/src/js/shared/utils.ts

  • Around line 120 (where you call crypto.subtle.digest), replace with:
      const encoder = new TextEncoder()
      const data = encoder.encode(str)
    - const hashBuffer = await crypto.subtle.digest('SHA-1', data)
    + const subtle = globalThis.crypto?.subtle
    +   ?? (await import('crypto')).webcrypto.subtle
    + if (!subtle) {
    +   throw new Error('Web Crypto API is not available in this environment')
    + }
    + const hashBuffer = await subtle.digest('SHA-1', data)
  • This ensures you use the browser’s crypto.subtle when present and Node’s crypto.webcrypto.subtle otherwise.

• You may also add a utility wrapper if you prefer:

async function getSubtle(): Promise<SubtleCrypto> {
  return globalThis.crypto?.subtle
    ?? (await import('crypto')).webcrypto.subtle
    ?? (() => { throw new Error('Web Crypto not available') })()
}

Optional: update types if needed to include Node’s webcrypto types by adding "types": ["node"] under compilerOptions in tsconfig.node.json to ensure proper typing in IDEs.

mockServer/src/services/branch.js (2)

72-75: Support listing by appId but don’t force stringification at call sites.

Minor nit: coerce once here.

   async list(appId) {
-    const result = await this.db.findAsync({ app: appId.toString() })
+    const result = await this.db.findAsync({ app: String(appId ?? '1') })
     return getResponseData(result)
   }

22-26: Uniqueness index likely needs app scoping.

Enforcing unique name across all apps may be too strict; consider a composite uniqueness (app + name). NeDB can’t do compound indexes; a practical workaround is to store a computed appNameKey: \${app}::${name}`` and index that.

mockServer/src/routes/main-routes.js (2)

238-246: Standardize response shape and basic error handling.

Most handlers directly assign service results to ctx.body. For consistency with places that wrap responses using getResponseData, and to avoid leaking stack traces, wrap success and handle failures uniformly.

Example pattern (apply similarly to other routes):

 router.post('/app-center/api/version/branch/create', async (ctx) => {
-  ctx.body = await mockService.branchService.create(ctx.request.body)
+  try {
+    const result = await mockService.branchService.create(ctx.request.body)
+    ctx.body = getResponseData(result)
+  } catch (err) {
+    ctx.status = 500
+    ctx.body = getResponseData(null)
+  }
 })

Also applies to: 258-271


242-246: HTTP method semantics (nit).

Updates are currently exposed as POST. If backward compatibility permits, consider PUT/PATCH for updates to align with REST conventions.

Also applies to: 267-271

packages/plugins/versioncontrol/src/composable/useUtils.ts (3)

6-15: Strengthen typing for useVModel to preserve prop types.

Make useVModel generic so callers get proper intellisense and type safety, and restrict propName to string keys.

Apply this diff:

-  const useVModel = (
-    props: { [x: string]: any },
-    emit: (arg0: string, arg1: any) => void,
-    propName: string | number
-  ) => {
+  const useVModel = <T = any, K extends string = string>(
+    props: Record<K, T>,
+    emit: (event: `update:${K}`, value: T) => void,
+    propName: K
+  ) => {
     return computed({
       get: () => props[propName],
       set: (val) => emit(`update:${propName}`, val)
     })
   }

17-28: Avoid N+1 branch lookups with simple in-memory caching.

transformCommit awaits a branch fetch per commit. For lists, this is N+1. Cache names by branchId within the composable.

Apply this diff:

 export function useUtils() {
+  // simple in-memory cache for branchId -> name
+  const branchNameCache = new Map<string, string>()
   const useVModel = (
@@
   const transformCommit = async (json: Record<any, any>): Promise<DisplayCommit> => {
-    let branchName: string[]
-    if (json.branchId) {
-      const branch = await versionManager.branchAppService.getBranch(json.branchId)
-      if (branch) {
-        branchName = [branch.name]
-      } else {
-        branchName = []
-      }
-    } else {
-      branchName = []
-    }
+    let branchName: string[] = []
+    const bid = json.branchId
+    if (bid) {
+      if (branchNameCache.has(bid)) {
+        branchName = [branchNameCache.get(bid)!]
+      } else {
+        const branch = await versionManager.branchAppService.getBranch(bid)
+        if (branch?.name) {
+          branchNameCache.set(bid, branch.name)
+          branchName = [branch.name]
+        }
+      }
+    }

33-44: Prefer nullish coalescing to avoid overriding valid falsy values.

For strings and numbers, use ?? instead of || so empty strings and zeros are preserved.

Apply this diff:

-      author: json.author?.username || '',
+      author: json.author?.username ?? '',
@@
-      avatar: json.author?.avatar || '',
-      type: json.type || 'commit',
-      tags: json.tags || [],
+      avatar: json.author?.avatar ?? '',
+      type: json.type ?? 'commit',
+      tags: json.tags ?? [],
mockServer/src/services/commit.json (1)

1-122: Optionally include a changedFiles array for richer UI.

If feasible, add representative file paths to stats.changedFiles so the “变更文件” list shows meaningful entries. Otherwise, the UI will show counts only.

I can generate placeholder changedFiles arrays based on filesChanged to keep mock data consistent with the UI. Want me to push a patch?

packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (2)

61-72: Expose commit type via a getter.

_type is private, but consumers need to read it. Add a type getter.

Apply this diff:

   get stats(): CommitStats {
     return this._stats
   }
 
+  get type(): string {
+    return this._type
+  }

148-162: Annotate fromData return type explicitly.

Make the API intention clear and avoid confusion with similarly named interface types.

Apply this diff:

-  static fromData(data: {
+  static fromData(data: {
     id: ID
     hash: string
     message: string
     author: User
     committer: User
     timestamp: Timestamp
     parentCommits: readonly ID[]
     branchId: ID
     schema: PageSchema
     tags: readonly string[]
     verified: boolean
     stats: CommitStats
     type: string
-  }) {
+  }): Commit {
packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1)

15-16: Guard against empty selectedCommit to avoid rendering blanks.

Default prop is {}, which is truthy. Add a stricter check.

Apply this diff:

-      <div class="dialog-body" v-if="modelSelectedCommit">
+      <div class="dialog-body" v-if="modelSelectedCommit && Object.keys(modelSelectedCommit || {}).length">
packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (2)

12-36: Avoid duplicating diff configuration; reuse SchemaDiffResolver to keep behavior consistent.

SchemaDiffResolver already encapsulates a DiffPatcher with nearly identical options. Maintaining two configs risks divergent diffs over time (e.g., objectHash differences: this file uses componentName, resolver uses fileName).

Consider injecting the resolver and calling calculateDiff(oldSchema, newSchema) here, or exporting a shared diff config to DRY this up. If you keep a local instance, align objectHash with the resolver:

-        return obj.id || obj.componentName || `$$index:${index}`
+        return obj.id || obj.fileName || obj.componentName || `$$index:${index}`

101-105: Consider returning a deduplicated, stable-sorted changedFiles for deterministic UIs.

Using includes checks works but can return non-deterministic order depending on traversal. Sorting the final list improves snapshot diffs and tests.

   return {
-    totalAdditions,
-    totalDeletions,
-    changedFiles
+    totalAdditions,
+    totalDeletions,
+    changedFiles: Array.from(new Set(changedFiles)).sort()
   }
packages/plugins/versioncontrol/src/components/VersionControlFilters.vue (3)

5-21: Trigger applyFilters on model changes, not only native change events.

Relying on @change misses programmatic updates (e.g., when parent resets filters). Watching the v-model values is more robust.

-      <select v-model="modelAuthorFilter" @change="applyFilters" class="filter-select">
+      <select v-model="modelAuthorFilter" class="filter-select">
...
-      <select v-model="modelTimeFilter" @change="applyFilters" class="filter-select">
+      <select v-model="modelTimeFilter" class="filter-select">

And in setup:

     const modelAuthorFilter = useVModel(props, emit, 'authorFilter')
     const modelTimeFilter = useVModel(props, emit, 'timeFilter')
 
     // 事件方法
     const applyFilters = () => emit('applyFilters')
     const clearFilters = () => emit('clearFilters')
     const createCommit = () => emit('createCommit')
 
+    watch([modelAuthorFilter, modelTimeFilter], () => {
+      applyFilters()
+    })

24-29: Buttons should declare type="button" to avoid unintended form submission.

In case this component is nested in a form, default type="submit" can cause navigation.

-      <button @click="clearFilters" class="clear-filters-btn">清除筛选</button>
+      <button type="button" @click="clearFilters" class="clear-filters-btn">清除筛选</button>
...
-      <button @click="createCommit" class="commit-btn">提交Commit</button>
+      <button type="button" @click="createCommit" class="commit-btn">提交Commit</button>

31-33: Internationalization: expose the stats text via props or i18n.

Hard-coded Chinese strings may block localization. If the rest of the plugin uses i18n, wire this to it.

mockServer/src/services/commit.js (3)

22-26: Add secondary indexes to support “index by author/branch/hash” PR goals.

You already have a unique index on id. Add non-unique indexes to accelerate common queries.

     this.db.ensureIndex({
       fieldName: 'id',
       unique: true
     })
+    this.db.ensureIndex({ fieldName: 'branchId' })
+    this.db.ensureIndex({ fieldName: 'author.id' })
+    this.db.ensureIndex({ fieldName: 'timestamp' })

44-56: Message nit: console text references “branch” while seeding commits.

Minor clarity fix.

-      console.log('初始化分支数据...')
+      console.log('初始化提交数据...')

70-73: Consider adding a filtered query method to support indexing-by-author/hash use-cases.

Routes elsewhere use .find(ctx.query); this service exposes only .list(appId). Add a find(params) that honors id/branchId/authorId/timespan for parity with other services.

I can draft a find(params = {}) implementation consistent with your NeDB usage if you’d like.

packages/plugins/versioncontrol/src/components/CommitCategorySelect.vue (4)

38-40: Align component name with file name for consistency and better devtools DX.

File is CommitCategorySelect.vue, but name is CommitFeatureSelect.

-  name: 'CommitFeatureSelect',
+  name: 'CommitCategorySelect',

73-99: Option toggling leaves dropdown open; consider closing on selection and supporting ESC/outside click.

Improves UX for multi-selects. Optional but common behavior.

-    const toggleOption = (option) => {
+    const toggleOption = (option) => {
       if (isSelected(option)) {
         selectedOptions.value = selectedOptions.value.filter((item) => item.value !== option.value)
       } else {
         selectedOptions.value = [...selectedOptions.value, option]
       }
-    }
+      // keep open for multi-select or close if desired:
+      // isDropdownOpen.value = false
+    }

Additional (outside selected range) helper to close on outside click:

// add in setup()
const onClickOutside = (e) => {
  if (!e.target.closest('.commit-feature-select')) isDropdownOpen.value = false
}
onMounted(() => document.addEventListener('click', onClickOutside))
onBeforeUnmount(() => document.removeEventListener('click', onClickOutside))

127-256: Accessibility: add roles/ARIA for combobox/listbox patterns.

Adds minimal semantics for screen readers.

-  <div class="commit-feature-select">
+  <div class="commit-feature-select" role="combobox" aria-expanded="isDropdownOpen">
...
-    <div class="dropdown-menu" v-if="isDropdownOpen">
+    <div class="dropdown-menu" v-if="isDropdownOpen" role="listbox" aria-multiselectable="true">
...
-        <li
+        <li
           v-for="option in filteredOptions"
           :key="option.value"
           class="option-item"
           :class="{ 'is-selected': isSelected(option) }"
-          @click="toggleOption(option)"
+          role="option"
+          :aria-selected="isSelected(option)"
+          @click="toggleOption(option)"
         >

77-86: Case-insensitive search on labels written in CJK is a no-op; consider locale-insensitive matching.

For Chinese labels, toLowerCase() doesn’t help. It’s fine, but you can simplify:

-      const lowerSearchText = searchText.value.toLowerCase()
-      return props.options.filter(
-        (option) =>
-          option.label.toLowerCase().includes(lowerSearchText) || option.value.toLowerCase().includes(lowerSearchText)
-      )
+      const q = searchText.value.trim()
+      return props.options.filter((option) => option.label.includes(q) || option.value.toLowerCase().includes(q.toLowerCase()))
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (3)

91-98: Use slice() instead of deprecated substr() in default id generator.

Minor modernization.

-    idGenerator: () => ID = () => `commit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+    idGenerator: () => ID = () => `commit_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,

65-73: Remove redundant IDs from rollback signature; the objects already carry IDs.

Passing both branch and branchId (and both targetCommit and targetCommitId) is error-prone and redundant.

Apply these diffs:

-  rollbackBranchToCommit(branch: Branch, branchId: ID, targetCommitId: ID, targetCommit: Commit): void
+  rollbackBranchToCommit(branch: Branch, targetCommit: Commit): void
-  rollbackBranchToCommit(branch: Branch, branchId: ID, targetCommitId: ID, targetCommit: Commit): void {
-    // 验证目标提交属于该分支
-    if (targetCommit.branchId !== branchId) {
-      throw new Error(`Commit ${targetCommitId} does not belong to branch ${branchId}`)
-    }
-
-    // 执行回滚
-    branch.setHeadCommitId(targetCommitId)
+  rollbackBranchToCommit(branch: Branch, targetCommit: Commit): void {
+    // 验证目标提交属于该分支
+    if (targetCommit.branchId !== branch.id) {
+      throw new Error(`Commit ${targetCommit.id} does not belong to branch ${branch.id}`)
+    }
+    // 执行回滚
+    branch.setHeadCommitId(targetCommit.id)
     branch.setStatus('active')
   }

224-261: Optional: extend keyword filtering to tags and changed paths for richer search.

Users often search by tag or file/path. If available in your Commit model, include tags and stats.changedFiles.

-      if (filters.searchKeyword) {
+      if (filters.searchKeyword) {
         const keyword = filters.searchKeyword.toLowerCase()
-        const messageMatch = commit.message.toLowerCase().includes(keyword)
-        // const schemaMatch = JSON.stringify(commit.schema).toLowerCase().includes(keyword); // 如果需要搜索 Schema 内容
-        if (!messageMatch /* && !schemaMatch */) {
+        const messageMatch = commit.message.toLowerCase().includes(keyword)
+        const tagsMatch = (commit.tags || []).some((t) => t.toLowerCase().includes(keyword))
+        const filesMatch = (commit.stats?.changedFiles || []).some((p) => p.toLowerCase().includes(keyword))
+        if (!messageMatch && !tagsMatch && !filesMatch) {
           return false
         }
       }
packages/plugins/versioncontrol/src/components/CommitsContainer.vue (2)

247-250: Use toLocaleString instead of toLocaleDateString to include time

toLocaleDateString ignores hour/minute in most browsers; you intended to show time. Switch to toLocaleString.

-const formatDate = (dateString) => {
-  const options = { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }
-  return new Date(dateString).toLocaleDateString('zh-CN', options)
-}
+const formatDate = (dateString) => {
+  const options = { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }
+  return new Date(dateString).toLocaleString('zh-CN', options)
+}

53-57: Improve avatar alt text for accessibility

Use the author’s name to make the alt meaningful.

-            <img :src="commit.avatar" alt="avatar" class="commit-avatar" />
+            <img :src="commit.avatar" :alt="`${commit.author} avatar`" class="commit-avatar" />
packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1)

23-31: Tighten repository typing and align with API typing

findById returns Commit | null in the API but the repository interface says any | null. Make them consistent.

 export interface CommitRepository {
   save(commit: Commit): Promise<void>
-  findById(id: ID): Promise<any | null>
+  findById(id: ID): Promise<Commit | null>
   findByBranchId(branchId: ID, limit?: number, offset?: number): Promise<Commit[]>
   findByTag(tag: string): Promise<Commit[]>
   findAll(): Promise<Commit[]>
   update(commit: Commit): Promise<void>
   delete(id: ID): Promise<void>
 }

If the codebase distinguishes between domain model (class Commit with toData) and API DTO (interface Commit in shared/type.ts), consider renaming the types here to CommitModel and CommitDto for clarity and adjusting imports accordingly. I can provide a patch once we confirm the intended layering.

packages/plugins/versioncontrol/src/components/VersionHeader.vue (1)

110-117: Propagate the selected branch in the emitted event

Currently onBranchChange emits 'branch-change' without payload. Consider passing the selected branch to avoid the parent reading state synchronously.

-    const onBranchChange = () => emit('branch-change')
+    const onBranchChange = () => emit('branch-change', modelCurrentBranch.value)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)

136-144: Date sorting could be more robust to unexpected formats

You currently rely on Date.parse which returns NaN for malformed strings. Since transformCommit emits ISO strings, you’re mostly safe, but a tolerant parse avoids edge cases if upstream changes.

-    filtered.sort((a, b) => {
+    filtered.sort((a, b) => {
+      const pa = Date.parse(a.date) || Date.parse(a.date?.replace?.(' ', 'T')) || 0
+      const pb = Date.parse(b.date) || Date.parse(b.date?.replace?.(' ', 'T')) || 0
       switch (sortBy.value) {
         case 'date-desc': {
-          return Date.parse(b.date) - Date.parse(a.date)
+          return pb - pa
         }
         case 'date-asc': {
-          return Date.parse(a.date) - Date.parse(b.date)
+          return pa - pb
         }
         case 'author': {
           return a.author.localeCompare(b.author)
         }
packages/plugins/versioncontrol/src/components/VersionTagCreate.vue (2)

71-77: newTagDescription is collected but never used

Either include description in the service call (if supported) or remove the field to avoid confusing users.

If commitAppService.addTagToCommit can accept a description, I can patch the call and service signature accordingly.

Also applies to: 39-43


55-67: Default dialog visibility should be false

A creation dialog opening by default is surprising; prefer default: false.

     tagDialogVisible: {
       type: Boolean,
-      default: true
+      default: false
     },
packages/plugins/versioncontrol/src/components/VersionBranchCreate.vue (2)

35-41: Optional commit select is marked required.

Label says 可选 but select has required. Drop required or update copy.

-          <select id="commitId" v-model="modelBranchTargetCommit" class="form-select" required>
+          <select id="commitId" v-model="modelBranchTargetCommit" class="form-select">

2-4: Ensure dialog visibility is actually toggled locally to prevent stuck overlay.

close-branch-dialog event relies on parent; also flip the model to false locally for robustness.

-  <div v-if="modelBranchDialogVisible" class="dialog-overlay" @click="closeBranchDialog">
+  <div v-if="modelBranchDialogVisible" class="dialog-overlay" @click="closeBranchDialog">

@@
-    const closeBranchDialog = () => emit('close-branch-dialog')
+    const closeBranchDialog = () => {
+      modelBranchDialogVisible.value = false
+      emit('close-branch-dialog')
+    }
@@
-        closeBranchDialog()
+        closeBranchDialog()

Also applies to: 103-106, 151-152

packages/plugins/versioncontrol/src/components/VersionCommitCreate.vue (3)

73-76: Reduce duplicate useCanvas() calls and stale schema reads; capture once.

Multiple useCanvas() invocations can be avoided and keeps schema consistent for the operation.

-    const { pageState } = useCanvas()
+    const { pageState, exportSchema } = useCanvas()
@@
-      pageShema: string2Obj(useCanvas().exportSchema()),
+      pageSchema: string2Obj(exportSchema()),
       pageData: obj2String(pageState.pageSchema)
@@
-          string2Obj(useCanvas().exportSchema()),
+          string2Obj(exportSchema()),

Also fix the pageShema typo (rename to pageSchema or remove if unused).

Also applies to: 117-118, 138-155


25-26: Unused ref attribute.

ref="container" isn’t used. Drop it or use it to resize/fit Monaco on open.


104-107: Also toggle the v-model when closing to prevent stale visibility.

Relying solely on an event can leave the dialog open if the parent ignores it.

-    const closeCommitDialog = () => {
-      isLoading.value = false
-      emit('close-commit-dialog')
-    }
+    const closeCommitDialog = () => {
+      isLoading.value = false
+      modelCommitDialogVisible.value = false
+      emit('close-commit-dialog')
+    }
packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (1)

204-207: Grammar in validation messages.

“Commit A are required…” should be “is required…”.

-      [commitIdA, 'Commit A', 'Commit A are required for diff.'],
-      [commitIdB, 'Commit B', 'Commit B are required for diff.']
+      [commitIdA, 'Commit A', 'Commit A is required for diff.'],
+      [commitIdB, 'Commit B', 'Commit B is required for diff.']
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1)

115-118: Guard compareWithCurrent when no commit is selected.

Non-null assertion may throw.

-  const compareWithCurrent = () => {
-    compareCommit(selectedCommit.value!)
-    closeDialog()
-  }
+  const compareWithCurrent = () => {
+    if (!selectedCommit.value) return
+    compareCommit(selectedCommit.value)
+    closeDialog()
+  }
packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (1)

509-511: Consider converting branch to domain instance before rename.

If repository returns POJO, branchService.renameBranch may rely on domain methods/getters.

-    this.branchService.renameBranch(branch, newName)
+    const branchInstance = (Branch as any).fromData ? (Branch as any).fromData(branch) : (Branch as any).formData(branch)
+    this.branchService.renameBranch(branchInstance, newName)
-    await this.branchRepository.update(branch)
+    await this.branchRepository.update(branchInstance)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4e6d03b and 18f2c80.

⛔ Files ignored due to path filters (2)
  • mockServer/src/database/branch.db is excluded by !**/*.db
  • mockServer/src/database/commit.db is excluded by !**/*.db
📒 Files selected for processing (30)
  • mockServer/src/routes/main-routes.js (1 hunks)
  • mockServer/src/services/branch.js (1 hunks)
  • mockServer/src/services/branch.json (1 hunks)
  • mockServer/src/services/commit.js (1 hunks)
  • mockServer/src/services/commit.json (1 hunks)
  • packages/plugins/versioncontrol/src/Main.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/CommitCategorySelect.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/CommitsContainer.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/MergeBranchDialog.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/TimelineContainer.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionBranchCreate.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionCommitCreate.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionCommitInfo.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionControlFilters.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionHeader.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionTagCreate.vue (1 hunks)
  • packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useUtils.ts (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useVersionControlUtils.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/shared/type.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/shared/utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/plugins/versioncontrol/src/Main.vue
  • packages/plugins/versioncontrol/src/components/TimelineContainer.vue
  • packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue
  • packages/plugins/versioncontrol/src/composable/useVersionControlUtils.ts
  • packages/plugins/versioncontrol/src/js/shared/type.ts
🧰 Additional context used
🧬 Code graph analysis (13)
packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (2)
  • PageSchema (417-417)
  • CommitStats (58-62)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (3)
packages/plugins/versioncontrol/src/js/shared/type.ts (2)
  • ID (7-7)
  • Branch (109-128)
packages/register/src/common.ts (1)
  • getMetaApi (20-30)
packages/register/src/constants.ts (1)
  • META_SERVICE (1-22)
packages/plugins/versioncontrol/src/composable/useUtils.ts (2)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)
  • DisplayCommit (6-20)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (2)
packages/plugins/versioncontrol/src/composable/useUtils.ts (1)
  • useUtils (5-51)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (8)
packages/plugins/versioncontrol/src/js/shared/type.ts (9)
  • ID (7-7)
  • User (17-22)
  • PageSchema (417-417)
  • Commit (67-81)
  • DiffResult (45-53)
  • Snapshot (422-426)
  • CommitHistoryRequest (335-343)
  • CommitHistoryResponse (348-355)
  • Branch (109-128)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (4)
  • message (53-55)
  • schema (57-59)
  • Commit (7-179)
  • stats (69-71)
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1)
  • CommitService (21-81)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (1)
  • BranchRepository (22-29)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1)
  • CommitRepository (23-31)
packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (1)
  • SchemaStatsCalculator (9-107)
packages/plugins/versioncontrol/src/js/shared/validation.ts (1)
  • Validator (6-82)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)
  • Memoize (51-101)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (4)
packages/plugins/versioncontrol/src/js/shared/type.ts (2)
  • ID (7-7)
  • Commit (67-81)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (1)
  • Commit (7-179)
packages/register/src/common.ts (1)
  • getMetaApi (20-30)
packages/register/src/constants.ts (1)
  • META_SERVICE (1-22)
packages/plugins/versioncontrol/src/js/application/service/BranchAppService.ts (8)
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (3)
  • upstreamBranchId (83-85)
  • commitsAhead (107-109)
  • commitsBehind (111-113)
packages/plugins/versioncontrol/src/js/shared/type.ts (11)
  • ID (7-7)
  • User (17-22)
  • Branch (109-128)
  • MergeStrategy (186-186)
  • ConflictReport (292-302)
  • BranchStatusResponse (229-243)
  • BranchOperationHistoryResponse (270-275)
  • CommitHistoryRequest (335-343)
  • CommitHistoryResponse (348-355)
  • BranchProtectionRule (139-145)
  • BranchOperationHistory (248-265)
packages/plugins/versioncontrol/src/js/domain/services/BranchService.ts (1)
  • BranchService (19-108)
packages/plugins/versioncontrol/src/js/application/service/CommitAppService.ts (1)
  • CommitAppService (23-99)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/CommitRepository.ts (1)
  • CommitRepository (23-31)
packages/plugins/versioncontrol/src/js/infrastructure/repositories/BranchRepository.ts (1)
  • BranchRepository (22-29)
packages/plugins/versioncontrol/src/js/shared/validation.ts (1)
  • Validator (6-82)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)
  • Memoize (51-101)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (3)
packages/plugins/versioncontrol/src/composable/useUtils.ts (1)
  • useUtils (5-51)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)
  • DisplayCommit (6-20)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (6)
mockServer/src/services/commit.js (1)
  • CommitService (15-85)
packages/plugins/versioncontrol/src/js/shared/type.ts (11)
  • ID (7-7)
  • User (17-22)
  • PageSchema (417-417)
  • CommitStats (58-62)
  • Commit (67-81)
  • DiffResult (45-53)
  • Snapshot (422-426)
  • Branch (109-128)
  • CommitHistoryRequest (335-343)
  • CommitHistoryResponse (348-355)
  • Timestamp (12-12)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (4)
  • message (53-55)
  • schema (57-59)
  • stats (69-71)
  • Commit (7-179)
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (1)
  • Branch (7-325)
packages/plugins/versioncontrol/src/js/domain/strategies/SchemaDiffResolver.ts (1)
  • SchemaDiffResolver (9-77)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)
  • sha1 (108-131)
mockServer/src/services/commit.js (2)
packages/plugins/versioncontrol/src/js/domain/services/CommitService.ts (1)
  • CommitService (21-81)
mockServer/src/routes/main-routes.js (3)
  • result (107-107)
  • result (141-141)
  • result (188-188)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (6)
  • Commit (67-81)
  • ID (7-7)
  • User (17-22)
  • Timestamp (12-12)
  • PageSchema (417-417)
  • CommitStats (58-62)
mockServer/src/services/branch.js (1)
mockServer/src/routes/main-routes.js (3)
  • result (107-107)
  • result (141-141)
  • result (188-188)
packages/plugins/versioncontrol/src/js/shared/utils.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (1)
  • Timestamp (12-12)

Comment thread mockServer/src/routes/main-routes.js
Comment thread mockServer/src/routes/main-routes.js
Comment thread mockServer/src/services/branch.js
Comment thread mockServer/src/services/branch.js
Comment thread mockServer/src/services/branch.js
Comment thread packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts Outdated
Comment thread packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts Outdated
@hexqi hexqi closed this Sep 5, 2025
@hexqi hexqi reopened this Sep 5, 2025
@github-actions github-actions Bot added breaking-change and removed documentation Improvements or additions to documentation labels Sep 7, 2025

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/canvas/container/src/composables/useMultiSelect.ts (1)

7-18: SelectionState is too narrow for actual usage below. Add missing fields.

Call sites pass componentName, type, and doc (see setSelectRect at Lines 470-484). This will trigger TS excess property errors with the exported interface.

 export interface SelectionState {
   id: string
   top?: number
   left?: number
   width?: number
   height?: number
   schema?: any
   parent?: {
     id: string
     children: Node[]
   }
+  // present in setSelectRect usage, make them explicit
+  componentName?: string
+  type?: string
+  doc?: Document
 }
packages/canvas/container/src/CanvasContainer.vue (1)

489-501: Ensure all listeners are cleaned up to prevent leaks.

Use AbortController to auto-clean window/document/iframe listeners.

+    const windowAbort = new AbortController()
+    const documentAbort = new AbortController()
+    const frameAbort = ref(null)
...
-      window.addEventListener('mousedown', (e) => {
+      window.addEventListener('mousedown', (e) => {
         insertPosition.value = insertPanel.value?.contains(e.target)
         insertContainer.value = containerPanel.value?.contains(e.target)
         target.value = e.target
-      })
+      }, { signal: windowAbort.signal })
 
-      window.addEventListener('dragenter', () => {
+      window.addEventListener('dragenter', () => {
         clearLineState()
-      })
+      }, { signal: windowAbort.signal })
 
-      window.addEventListener('message', updateI18n)
+      window.addEventListener('message', updateI18n, { signal: windowAbort.signal })
...
-    document.addEventListener('beforeCanvasReady', beforeCanvasReady)
-    document.addEventListener('canvasReady', canvasReady)
+    document.addEventListener('beforeCanvasReady', beforeCanvasReady, { signal: documentAbort.signal })
+    document.addEventListener('canvasReady', canvasReady, { signal: documentAbort.signal })
...
-        win.addEventListener('scroll', syncNodeScroll, true)
-        win.addEventListener('scroll', syncRemoteNode, true)
+        const ctrl = new AbortController()
+        frameAbort.value = ctrl
+        win.addEventListener('scroll', syncNodeScroll, { capture: true, signal: ctrl.signal })
+        win.addEventListener('scroll', syncRemoteNode, { capture: true, signal: ctrl.signal })
...
     onUnmounted(() => {
       if (iframe.value?.contentDocument) {
         removeHotkeyEvent(iframe.value.contentDocument)
       }
-      window.removeEventListener('message', updateI18n, false)
+      windowAbort.abort()
+      documentAbort.abort()
+      frameAbort.value?.abort()
+      window.removeEventListener('message', updateI18n, false) // safe even after abort
     })

Also applies to: 523-525, 516-522, 451-452

♻️ Duplicate comments (1)
packages/design-core/package.json (1)

92-92: Stay current on @vue/repl version (already flagged by another reviewer)

Previous comment suggests bumping 4.6.1 → 4.6.2. Echoing for visibility; no further action if handled elsewhere.

🧹 Nitpick comments (35)
mockServer/package.json (1)

43-47: Yjs stack deps: verify compatibility and avoid unnecessary direct deps.

  • Check yjs@^13.6.8 with y-websocket@^1.5.0 and ws@^8.13.0 for compatibility on Node 16.
  • lib0 is usually pulled transitively by yjs/y-websocket; having it as a direct runtime dep may be unnecessary and can drift. Consider removing unless you import it directly.

If you confirm no direct imports of lib0, apply:

   "y-websocket": "^1.5.0",
   "ws": "^8.13.0",
-  "lib0": "^0.2.55"
+  "lib0": "^0.2.55"

…and then remove it from dependencies (keeping lockfile integrity). Also, run a quick search to ensure no direct lib0 imports remain.

mockServer/src/app.js (1)

17-19: Avoid brittle deep-imports from y-websocket internals.

y-websocket/bin/utils is an internal path and may break across releases. Prefer the documented server entry or a stable export; if you must deep-import, pin the exact file (with extension) and monitor for breaking changes.

Would you like me to propose an alternative import pattern based on the version you’re standardizing on?

packages/register/src/hooks.ts (2)

96-96: Type the new hook for safer consumption

Export a typed API to prevent “any” leakage.

Apply:

+import type { UseRealtimeCollabApi } from './types'
-export const useRealtimeCollab = (...args: any[]) => getHook(HOOK_NAME.useRealtimeCollab, args)
+export const useRealtimeCollab = (...args: any[]): UseRealtimeCollabApi =>
+  getHook(HOOK_NAME.useRealtimeCollab, args)

And add export interface UseRealtimeCollabApi { /* connect(roomId:string):void; etc. */ } to types.ts.


41-43: Public hook added — document and version clearly

Adding useRealtimeCollab: 'collaboration' expands the public API surface. Please document this hook and call out the breaking/feature semver change in the changelog to align with the PR’s “breaking changes” note.

Run to check for name collisions and registrations:

#!/bin/bash
rg -nC2 -e "['\"]collaboration['\"]" -e "HOOK_NAME\.useRealtimeCollab" -e "initHook\([^)]*useRealtimeCollab" --glob '!**/dist/**'
packages/multi-person-collaboration/src/services/docManager.ts (1)

24-31: Auto-prune destroyed docs to avoid stale map entries

If callers destroy Y.Doc directly, the map retains a dead doc. Attach a destroy listener to keep the registry clean.

   public getOrCreateDoc(docName: string): Y.Doc {
     if (!this.docs.has(docName)) {
-      const ydoc = new Y.Doc()
+      const ydoc = new Y.Doc()
+      ydoc.on('destroy', () => {
+        if (this.docs.get(docName) === ydoc) {
+          this.docs.delete(docName)
+        }
+      })
       this.docs.set(docName, ydoc)
     }
     return this.docs.get(docName)!
   }
packages/multi-person-collaboration/src/services/providerManager.ts (1)

73-78: Nice: status subscription with unsubscribe

API is ergonomic. Consider exposing awareness later to support remote-user UIs without reaching into provider directly.

public getAwareness(roomId: string) {
  return this.providers.get(roomId)?.awareness
}
packages/multi-person-collaboration/src/index.ts (1)

1-2: Also re-export types for DX and future-proofing.

Expose named exports (types/utilities) alongside the default to enable tree-shaking and better IDE hints.

 import { useCollabSchema } from './composables/useCollabSchema'
 export default useCollabSchema
+export * from './composables/useCollabSchema'
packages/canvas/container/src/composables/useMultiSelect.ts (1)

117-119: Guard collaboration hook usage to avoid runtime errors if not registered.

Avoid throwing when collab is disabled or not initialized; also avoid repeated function calls.

-    // 多人协作
-    useRealtimeCollab().updateUserSelection(selectState)
+    // 多人协作
+    const collab = useRealtimeCollab()
+    collab?.updateUserSelection?.(selectState)

Additional (outside the shown hunk): consider calling onUnmounted(cancelSelectionUpdate) within useMultiSelect to ensure timers are cleared when the consumer unmounts.

packages/canvas/container/src/container.ts (1)

315-317: Make collab calls defensive (no-op if collab not available).

If the collab plugin isn’t registered/connected, these should not throw or block local operations.

-  useRealtimeCollab().insertSharedNode({ node, parent, data }, POSITION.BOTTOM)
+  useRealtimeCollab().insertSharedNode?.({ node, parent, data }, POSITION.BOTTOM)
-    useRealtimeCollab().insertSharedNode({ node, parent, data }, POSITION.REPLACE)
+    useRealtimeCollab().insertSharedNode?.({ node, parent, data }, POSITION.REPLACE)
-  useRealtimeCollab().insertSharedNode({ node, parent, data }, POSITION.TOP)
+  useRealtimeCollab().insertSharedNode?.({ node, parent, data }, POSITION.TOP)
-  useRealtimeCollab().deleteSharedNode(id)
+  useRealtimeCollab().deleteSharedNode?.(id)
-  useRealtimeCollab().insertSharedNode({ node, parent, data }, POSITION.OUT)
+  useRealtimeCollab().insertSharedNode?.({ node, parent, data }, POSITION.OUT)

Optional follow-up (outside the shown hunks): prefer importing a single source of truth for POSITION from your collaboration types to avoid divergence with other packages.

Also applies to: 335-337, 353-355, 379-380, 396-398

packages/multi-person-collaboration/src/composables/useAwareness.ts (1)

25-47: Reduce noisy console logs; fix minor typo.

Console noise in a library hurts DX; gate logs behind env checks (and fix “contected”→“connected”) or remove them.

-        // eslint-disable-next-line no-console
-        console.log(`[useAwareness] useAwareness is contected`)
+        process.env.NODE_ENV !== 'production' &&
+          // eslint-disable-next-line no-console
+          console.log('[useAwareness] useAwareness is connected')
@@
-          // eslint-disable-next-line no-console
-          console.log('User entered:', clientId, remoteStates)
+          process.env.NODE_ENV !== 'production' &&
+            // eslint-disable-next-line no-console
+            console.log('User entered:', clientId, remoteStates)
@@
-          // eslint-disable-next-line no-console
-          console.log('User changed:', clientId, remoteStates)
+          process.env.NODE_ENV !== 'production' &&
+            // eslint-disable-next-line no-console
+            console.log('User changed:', clientId, remoteStates)
@@
-          // eslint-disable-next-line no-console
-          console.log('User left:', clientId, remoteStates)
+          process.env.NODE_ENV !== 'production' &&
+            // eslint-disable-next-line no-console
+            console.log('User left:', clientId, remoteStates)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)

69-75: Return readonly refs for provider/awareness for consistency.

The types indicate Readonly<Ref<...>>; return readonly proxies to match.

   return {
     ydoc: ydoc,
-    provider: provider,
-    awareness: awareness,
+    provider: readonly(provider),
+    awareness: readonly(awareness),
     status: readonly(status)
   }
packages/multi-person-collaboration/src/utils/index.ts (2)

10-54: Batch Yjs writes in a single transaction

Wrapping mutations in a transaction reduces event churn and improves consistency.

-export function toYjs(target: Y.Map<any> | Y.Array<any>, obj: any) {
+export function toYjs(target: Y.Map<any> | Y.Array<any>, obj: any) {
+  const doc = (target as any).doc as Y.Doc | undefined
+  const apply = () => {
   if (Array.isArray(obj)) {
@@
   }
-}
+  }
+  doc ? doc.transact(apply) : apply()
+}

11-31: Preserve sparse arrays (optional)

forEach skips holes; use an index loop if sparse arrays are expected.

-    obj.forEach((item) => {
+    for (let i = 0; i < obj.length; i++) {
+      const item = obj[i]
       if (item === undefined) {
         target.push([UNDEFINED_PLACEHOLDER])
@@
-      }
-    })
+      }
+    }
packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1)

66-75: Tighten position typing to prevent invalid values

Constrain position to a union of supported literals to catch mapping errors at compile time.

-  private insert(parentId: string, newNodeData: Node, position: string, referTargetNodeId?: string) {
+  private insert(
+    parentId: string,
+    newNodeData: Node,
+    position: 'before' | 'after' | 'replace' | typeof POSITION.IN | typeof POSITION.OUT,
+    referTargetNodeId?: string
+  ) {
packages/multi-person-collaboration/src/composables/useCollabSchema.ts (3)

46-47: Make WebSocket URL configurable and production-safe

Hardcoding ws://localhost breaks non-local deployments and TLS (wss://) setups.

-  const { awareness, provider } = useYjs(roomId, { websocketUrl: `ws://localhost:${PORT}` })
+  const defaultWs = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host
+  const { awareness, provider } = useYjs(roomId, {
+    websocketUrl: `${defaultWs.replace(/\/$/, '')}:${PORT}`
+  })

Or accept an optional websocketUrl in options and fall back to config/env.


50-52: Guard provider readiness

provider.value may be undefined on first render; avoid non-null assertion and fail-fast.

-  const schemaModel = schemaManager.createSchema(roomId, provider.value!)
+  if (!provider.value) throw new Error('[useCollabSchema] Yjs provider not ready')
+  const schemaModel = schemaManager.createSchema(roomId, provider.value)

66-73: Type the selection update to the declared state shape

Avoid any; align with SchemaAwarenessState to catch mismatches.

-  const updateUserSelection = (selectedNode: any) => {
+  const updateUserSelection = (selectedNode: SchemaAwarenessState['selection']) => {
     updateLocalStateField('selection', selectedNode)
   }
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (3)

38-44: Avoid emitting "enter" for the local client.

Filter out this.awareness.clientID on added to prevent self-enter noise in consumers.

-    for (const clientId of changes.added) {
+    for (const clientId of changes.added) {
+      if (clientId === this.awareness.clientID) continue
       const state = newStates.get(clientId)
       if (state) {
         this.emitter.emit('enter', { clientId, state })
       }
     }

45-53: Replace JSON stringify diff with a faster deep-equal.

JSON.stringify on every update is expensive and order-sensitive. Use a lightweight deep-equal (e.g., fast-deep-equal) or a keyed shallow-compare on fields you care about.

-      if (newState && JSON.stringify(newState) !== JSON.stringify(oldState)) {
+      // e.g., import equal from 'fast-deep-equal'
+      if (newState && !equal(newState, oldState)) {
         this.emitter.emit('change', { clientId, state: newState })
       }

62-65: Harden destroy and clear local snapshot.

Clear previousStates to free references and guard against double-destroy.

   public destroy(): void {
     this.awareness.off('update', this.handleStateChange)
     this.emitter.all.clear()
+    this.previousStates.clear()
   }
packages/multi-person-collaboration/src/services/schemaManager.ts (4)

148-160: Skip remote patches until initial sync completes (OK), but consider batching logs and reducing noise.

The guard is correct; throttle or remove the console logs in production builds.

-        console.log(`[${docName}] Ignoring patches during initial sync.`)
+        if (process.env.NODE_ENV !== 'production') {
+          // eslint-disable-next-line no-console
+          console.log(`[${docName}] Ignoring patches during initial sync.`)
+        }

175-194: Deletion signaling relies on _node_deleted key (good), but duplicated array-delete patches may occur.

You push an array-delete patch both on the Map-key change and possibly again via Array delta with empty deletedIds. Either enrich the Array-delete with IDs or filter duplicates to avoid double handling.

-          if (event.changes.keys.has('_node_deleted')) {
+          if (event.changes.keys.has('_node_deleted')) {
             const change = event.changes.keys.get('_node_deleted')
             if (change?.action === 'add' || change?.action === 'update') {
               if (yMapNode.get('_node_deleted') === true) {
                 const nodeId = yMapNode.get('id')
                 if (nodeId) {
-                  patches.push({ type: 'array-delete', path: event.path, count: 1, deletedIds: [nodeId] })
+                  // Tag to dedupe later
+                  patches.push({ type: 'array-delete', path: event.path, count: 1, deletedIds: [nodeId] })
                 }
               }
             }
           }

And before apply:

-      if (patches.length) {
+      if (patches.length) {
+        // optional: dedupe identical array-delete patches by id
+        const seen = new Set<string>()
+        const filtered = patches.filter(p => {
+          if (p.type !== 'array-delete' || !p.deletedIds?.length) return true
+          const key = p.deletedIds.join(',')
+          if (seen.has(key)) return false
+          seen.add(key)
+          return true
+        })
         try {
-          this.applyPatches(docName, patches)
+          this.applyPatches(docName, filtered)

197-249: Array delta handling: derive inserted items from delta rather than from target slice to avoid race on concurrent ops.

Reading from yArray.slice after applying the delta can misalign under interleaved ops. Prefer mapping d.insert directly through fromYjs if items are Yjs types.

-            if (d.insert) {
-              const insertCount = d.insert.length
-              const insertedYjsItems = yArray.slice(index, index + insertCount)
-              const items = insertedYjsItems.map((yjsItem) => fromYjs(yjsItem))
+            if (d.insert) {
+              const items = (d.insert as any[]).map((yjsItem) => fromYjs(yjsItem))
               patches.push({
                 type: 'array-insert',
                 path: [...basePath, index],
                 items
               })
-              if (insertedYjsItems.length > 0) {
-                console.log('===insert yjsItem[0].toJSON() (from target)===', insertedYjsItems[0].toJSON())
-              }
-              index += insertCount
+              index += (d.insert as any[]).length
             }

241-243: Remove debug logging or guard by env.

These logs will spam consoles in production.

-                // eslint-disable-next-line no-console
-                console.log('===insert yjsItem[0].toJSON() (from target)===', insertedYjsItems[0].toJSON())
+                if (process.env.NODE_ENV !== 'production') {
+                  // eslint-disable-next-line no-console
+                  console.log('inserted item (debug)', items[0])
+                }
packages/multi-person-collaboration/src/operation/operationHandler .ts (2)

21-23: Consider explicit transactions for grouped operations.

Wrapping inserts/removes in a ydoc.transact (with an origin) improves atomicity and lets observers filter reliably.

Would you like me to wire OperationHandler with the owning Y.Doc to transact writes with a custom origin (e.g., IGNORE_OBSERVER_ORIGIN)?


31-37: Throw or surface an error when parent is missing.

Returning {} masks failures and makes upstream logic harder. Consider throwing or returning a result type with an error.

-    if (!referenceNode) {
-      throw new Error(`Yjs Reference node with ID ${referTargetNodeId} not found`)
-    }
+    if (!referenceNode) {
+      throw new Error(`Yjs reference node with ID ${referTargetNodeId} not found`)
+    }

And similarly for missing parent:

-    if (!parentNode) {
-      return {}
-    }
+    if (!parentNode) {
+      throw new Error(`Yjs parent node with ID ${parentId} not found`)
+    }
packages/canvas/container/src/CanvasContainer.vue (7)

225-230: Remove duplicate right-click menu handling.

The second check is unreachable due to the earlier early-return.

-        // 如果是点击右键则打开右键菜单
-        if (event.button === 2) {
-          openMenu(event)
-          return
-        }

Also applies to: 244-249


417-427: Guard dataTransfer on dragover.

Avoid potential NPE in some browsers.

-        win.addEventListener('dragover', (ev) => {
-          ev.dataTransfer.dropEffect = 'move'
+        win.addEventListener('dragover', (ev) => {
+          if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move'
           ev.preventDefault()

453-469: Avoid shadowing “remoteStates” and clarify ownership.

Shadowing the top-level ref with the collab return value is confusing. Rename for clarity.

-        const { insertSharedNode, deleteSharedNode, updateUserSelection, remoteStates } = useCollabSchema({
+        const {
+          insertSharedNode,
+          deleteSharedNode,
+          updateUserSelection,
+          remoteStates: collabRemoteStates
+        } = useCollabSchema({
           roomId: 'schema-yjs',
           currentUser: {
             id: 2,
             name: 'Bob',
             color: '#4ECDC4',
             avatarUrl: 'https://i.pravatar.cc/150?img=2'
           }
         })
 
-        initHook(HOOK_NAME.useRealtimeCollab, {
+        initHook(HOOK_NAME.useRealtimeCollab, {
           insertSharedNode,
           deleteSharedNode,
           updateUserSelection,
-          remoteStates
+          remoteStates: collabRemoteStates
         })

526-531: Bind to realtime store once; avoid copying reactive objects.

Prefer holding a direct reference to the collaboration store over reassigning the object.

-    watch(isReady, (newVal) => {
-      if (newVal) {
-        const newStates = useRealtimeCollab().remoteStates
-        remoteStates.value = newStates
-      }
-    })
+    const realtime = useRealtimeCollab()
+    watch(isReady, (ready) => {
+      if (ready) remoteStates.value = realtime.remoteStates
+    })

160-175: Verify selection shape; code assumes selection.id exists.

Types (Selection) define anchor/head only. Here we read selection.id. Ensure the awareness payload uses a node-selection shape or adjust mapping.

I can align the shared types (add NodeSelection) and update the collaboration package accordingly.

Also applies to: 176-185


541-548: Remove console logging.

Avoid noisy logs in production.

-    watch(
-      remoteStates,
-      (newVal) => {
-        // eslint-disable-next-line no-console
-        console.log('remoteStates变动', newVal)
-      },
-      { deep: true }
-    )
+    // consider devtools-only diagnostics if needed

321-353: Drop unused isScrolling logic.

Variable is never read; trim for clarity.

-        // eslint-disable-next-line @typescript-eslint/no-unused-vars
-        let isScrolling = false
-
-        win.addEventListener('scroll', () => {
-          isScrolling = true
-        })
+        // removed unused scroll flag
packages/multi-person-collaboration/src/type.ts (2)

26-26: Prefer unknown over any for props.

Improves type safety without breaking consumers.

-  props: Record<string, any> & { columns?: { slots?: Record<string, any> }[] }
+  props: Record<string, unknown> & { columns?: { slots?: Record<string, unknown> }[] }

55-59: Align InsertOptions naming with InsertOperation.

Use a single, consistent “newNodeData” naming to reduce confusion.

 export interface InsertOptions {
   parent: Node | RootNode
-  node: Node | RootNode
-  data: Node
+  node: Node | RootNode
+  newNodeData: Node
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18f2c80 and 949c395.

📒 Files selected for processing (25)
  • mockServer/package.json (1 hunks)
  • mockServer/src/app.js (2 hunks)
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (2 hunks)
  • packages/canvas/container/src/CanvasContainer.vue (7 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (7 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (2 hunks)
  • packages/canvas/container/src/container.ts (6 hunks)
  • packages/design-core/package.json (3 hunks)
  • packages/multi-person-collaboration/package.json (1 hunks)
  • packages/multi-person-collaboration/src/composables/useAwareness.ts (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabSchema.ts (1 hunks)
  • packages/multi-person-collaboration/src/composables/useYjs.ts (1 hunks)
  • packages/multi-person-collaboration/src/config/index.ts (1 hunks)
  • packages/multi-person-collaboration/src/index.ts (1 hunks)
  • packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1 hunks)
  • packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1 hunks)
  • packages/multi-person-collaboration/src/operation/operationHandler .ts (1 hunks)
  • packages/multi-person-collaboration/src/services/docManager.ts (1 hunks)
  • packages/multi-person-collaboration/src/services/providerManager.ts (1 hunks)
  • packages/multi-person-collaboration/src/services/schemaManager.ts (1 hunks)
  • packages/multi-person-collaboration/src/type.ts (1 hunks)
  • packages/multi-person-collaboration/src/utils/index.ts (1 hunks)
  • packages/multi-person-collaboration/vite.config.js (1 hunks)
  • packages/register/src/hooks.ts (3 hunks)
  • tsconfig.app.json (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/multi-person-collaboration/src/config/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js
  • packages/multi-person-collaboration/package.json
  • tsconfig.app.json
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-01-14T08:45:57.032Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in `packages/canvas/render/src/application-function/global-state.ts` is migrated from an existing codebase and should be handled with care when making modifications.

Applied to files:

  • packages/canvas/container/src/composables/useMultiSelect.ts
📚 Learning: 2025-07-03T09:22:59.512Z
Learnt from: hexqi
PR: opentiny/tiny-engine#1501
File: mockServer/src/tool/Common.js:79-82
Timestamp: 2025-07-03T09:22:59.512Z
Learning: In the tiny-engine project, the mockServer code uses ES6 import syntax but is compiled to CommonJS output. This means CommonJS globals like `__dirname` are available at runtime, while ES6 module-specific features like `import.meta` would cause runtime errors.

Applied to files:

  • mockServer/package.json
📚 Learning: 2024-10-10T02:48:10.881Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 `packages/toolbars/preview/src/Main.vue` 文件中,使用 `useNotify` 而不是 `console` 来记录错误日志。

Applied to files:

  • packages/canvas/container/src/components/CanvasAction.vue
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/design-core/package.json
🔇 Additional comments (14)
packages/design-core/package.json (1)

65-65: Version Control plugin dependency — LGTM

Dependency aligns with the new plugin integration mentioned in the PR.

mockServer/src/app.js (1)

63-65: LGTM: proper HTTP server wrapping for Koa.

Creating an explicit http.Server for WS upgrade handling is correct. No issues here.

packages/register/src/hooks.ts (1)

67-67: State slot wired correctly

Registering [HOOK_NAME.useRealtimeCollab]: {} aligns with existing hook patterns. Looks good.

packages/canvas/container/src/components/CanvasAction.vue (4)

12-12: UI gating is correct

Hiding quick actions while a remote edit is present avoids conflicting affordances. Good.


269-272: haveRemoteState computed is fine (post-fix)

With the prop fix, this computed works as intended.


74-74: Right-panel gating is correct

Hiding destructive actions during remote edits reduces conflict risk. Good.


557-557: Expose haveRemoteState from setup — OK

This keeps template conditions reactive. Looks good.

packages/multi-person-collaboration/src/services/docManager.ts (1)

48-51: Bulk destroy looks good

Clean shutdown path is clear and safe.

packages/multi-person-collaboration/src/services/providerManager.ts (1)

33-51: Provider lifecycle and reuse are well-handled

Reusing by room with forceNew override is solid; erroring on missing options is appropriate.

packages/canvas/container/src/composables/useMultiSelect.ts (1)

2-2: Import wiring looks good.

No issues with the new hook import.

packages/canvas/container/src/container.ts (1)

29-29: Import addition is fine.

Hook is introduced without altering public API.

packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1)

29-31: Verify: root insertion semantic

Passing raw PositionType to OperationHandler for root insert may not be accepted.

Run to confirm accepted position values and where PositionType is defined:

#!/bin/bash
# Find PositionType and allowed insert positions
rg -nP -C3 'type\s+Position(Type)?\b|enum\s+POSITION\b|interface\s+InsertOptions\b|insert\(\s*\{[^}]*position' --type ts
# Inspect OperationHandler insert signature/usage
rg -nP -C3 'class\s+OperationHandler\b|insert\s*\(' --type ts packages/
packages/multi-person-collaboration/src/services/schemaManager.ts (1)

266-291: Gracefully handle array-delete without IDs.

If deletedIds is empty (e.g., pure structural deletes), no UI action happens. Optionally resolve IDs from current canvas/schema at this path or short-circuit to avoid misleading no-ops.

Would you like me to add a fallback resolver that maps [path, count] to node IDs using the current schema before calling operateNode?

packages/canvas/container/src/CanvasContainer.vue (1)

2-2: Confirm CanvasAction prop compatibility for remoteStatesLength.

Ensure CanvasAction.vue defines and defaults the new prop to avoid runtime warnings when older consumers render it.

Also applies to: 11-11

Comment thread mockServer/src/app.js
Comment thread packages/canvas/container/src/CanvasContainer.vue
Comment thread packages/canvas/container/src/CanvasContainer.vue Outdated
Comment thread packages/canvas/container/src/components/CanvasAction.vue Outdated
Comment thread packages/canvas/container/src/components/CanvasAction.vue
Comment thread packages/multi-person-collaboration/src/type.ts
Comment thread packages/multi-person-collaboration/src/utils/index.ts
Comment thread packages/multi-person-collaboration/src/utils/index.ts
Comment thread packages/multi-person-collaboration/src/utils/index.ts Outdated
Comment thread packages/multi-person-collaboration/vite.config.js Outdated

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/canvas/container/src/CanvasContainer.vue (2)

515-522: Clean up scroll listener and pending rAF on unmount

Prevent leaks by removing the syncRemoteNode listener and canceling the rAF.

Add:

onUnmounted(() => {
  if (iframe.value?.contentDocument) {
    removeHotkeyEvent(iframe.value.contentDocument)
  }
  if (iframe.value?.contentWindow) {
    // capture must match the addEventListener option
    iframe.value.contentWindow.removeEventListener('scroll', syncRemoteNode, { capture: true })
  }
  cancelAnimationFrame(syncRaf)
  window.removeEventListener('message', updateI18n, false)
})

2-12: Fix remoteStatesLength prop in CanvasAction.vue

  • Change type: Numeric to type: Number
  • Update default: () => {} to return a numeric value (e.g. default: () => 0)
♻️ Duplicate comments (3)
packages/multi-person-collaboration/package.json (1)

26-30: Fix repository.directory path to match actual package location.

Directory points to packages/plugins/... but file lives under packages/multi-person-collaboration.

  "repository": {
    "type": "git",
    "url": "https://github.com/opentiny/tiny-engine",
-   "directory": "packages/plugins/multi-person-collaboration"
+   "directory": "packages/multi-person-collaboration"
  },
packages/canvas/container/src/CanvasContainer.vue (2)

186-190: Throttle remote selection reflow on scroll; add rAF and passive listener

Mapping DOM rects on every scroll is hot; throttle with requestAnimationFrame and use passive listeners.

Apply:

-// 手动刷新远端节点位置
-const syncRemoteNode = () => {
-  syncRemoteStatesSelections.value = syncRemoteStatesSelections.value.map(mapStateToSelection).filter(Boolean)
-}
+// 手动刷新远端节点位置(rAF 节流)
+let syncRaf = 0
+const syncRemoteNode = () => {
+  cancelAnimationFrame(syncRaf)
+  syncRaf = requestAnimationFrame(() => {
+    syncRemoteStatesSelections.value = syncRemoteStatesSelections.value
+      .map(mapStateToSelection)
+      .filter(Boolean)
+  })
+}
-win.addEventListener('scroll', syncRemoteNode, true)
+win.addEventListener('scroll', syncRemoteNode, { capture: true, passive: true })

Additionally, cancel any pending rAF and remove the scroll listener on unmount (see separate cleanup comment).

Also applies to: 451-451


453-462: Don’t hard-code room/user in production

Externalize roomId/currentUser (env, config, or props) and avoid committing test avatars/PII.

Example:

const collabConfig = getCollabConfig?.() ?? {}
const { insertSharedNode, deleteSharedNode, updateUserSelection, remoteStates: awarenessStates } =
  useCollabSchema({
    roomId: collabConfig.roomId ?? 'schema-yjs',
    currentUser: collabConfig.currentUser ?? { id: 0, name: 'Guest' }
  })
🧹 Nitpick comments (9)
packages/multi-person-collaboration/package.json (3)

15-20: Expose ESM entry and typings via exports; mark sideEffects for better tree-shaking.

Improves consumer resolution and bundling.

   "main": "dist/index.js",
   "module": "dist/index.js",
   "types": "dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "import": "./dist/index.js"
+    }
+  },
+  "sideEffects": false,
   "files": [
     "dist"
   ],

42-43: Move monaco-editor to peer + devDependencies

monaco-editor@0.51.0 is officially supported by y-monaco@0.1.6; peering it avoids duplicate Monaco instances in host apps.

   "dependencies": {
     "@opentiny/tiny-engine-canvas": "workspace:*",
     "@opentiny/tiny-engine-common": "workspace:*",
     "@opentiny/tiny-engine-meta-register": "workspace:*",
     "@opentiny/tiny-engine-utils": "workspace:*",
-    "monaco-editor": "0.51.0"
+    // moved to peer + dev to keep runtime light
   },
   "devDependencies": {
+    "monaco-editor": "0.51.0",
     "glob": "^10.0.0",
     "lib0": "^0.2.85",
     "lodash-es": "^4.17.21",
     "mitt": "^3.0.1",
     "uuid": "^9.0.1",
     "vite": "^5.4.2",
     "vitest": "^1.4.0",
     "vue": "^3.4.21",
     "y-monaco": "^0.1.6",
     "y-protocols": "^1.0.6",
     "y-websocket": "^1.5.0",
     "yjs": "^13.6.8"
   },
   "peerDependencies": {
     "vue": "^3.4.21",
+    "monaco-editor": "0.51.x",
     "y-monaco": "^0.1.6",
     "y-protocols": "^1.0.6",
     "y-websocket": "^1.5.0",
     "yjs": "^13.6.8"
   }

21-25: Add engines field to enforce Node.js ≥18 for Vite 5 builds
Vite 5 drops support for Node 14/16/17/19 and requires Node 18 or 20+, so include

"engines": {
  "node": ">=18"
}

in package.json to prevent installs on unsupported versions. (vite.dev, v5.vite.dev)

packages/plugins/script/src/js/method.ts (1)

234-246: Avoid hard-coded user and duplicate inits; scope color and room per page

  • currentUser is statically defined here, not sourced from auth or context—risk of ID/name collisions.
  • The 8-char hex (“#ff0000ff”) isn’t supported by common #RGB/#RRGGBB parsers; use 6-char (“#ff0000”) or rgba().
  • “page-js” room/field is global—different pages will collide.
  • onActivated may fire repeatedly; wrapping useCollabMonaco in a one-time guard prevents duplicate bindings.
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)

14-31: makeTransparent should support 8-digit hex (#RRGGBBAA) and 4-digit (#RGBA).
This prevents unexpected passthrough when callers supply hex with alpha.

 function makeTransparent(hex: string, alpha: number): string {
   // hex: "#RRGGBB"
-  if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex
+  if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4 && hex.length !== 9 && hex.length !== 5)) return hex
   let r: number, g: number, b: number
+  let aFromHex: number | null = null
 
-  if (hex.length === 7) {
+  if (hex.length === 7 || hex.length === 9) {
     r = parseInt(hex.slice(1, 3), 16)
     g = parseInt(hex.slice(3, 5), 16)
     b = parseInt(hex.slice(5, 7), 16)
+    if (hex.length === 9) aFromHex = parseInt(hex.slice(7, 9), 16) / 255
   } else {
-    // "#RGB" 短写
-    r = parseInt(hex[1] + hex[1], 16)
-    g = parseInt(hex[2] + hex[2], 16)
-    b = parseInt(hex[3] + hex[3], 16)
+    // "#RGB" / "#RGBA" 短写
+    r = parseInt(hex[1] + hex[1], 16)
+    g = parseInt(hex[2] + hex[2], 16)
+    b = parseInt(hex[3] + hex[3], 16)
+    if (hex.length === 5) aFromHex = parseInt(hex[4] + hex[4], 16) / 255
   }
 
-  return `rgba(${r},${g},${b},${alpha})`
+  const finalA = aFromHex == null ? alpha : Math.min(1, +(aFromHex * alpha).toFixed(3))
+  return `rgba(${r},${g},${b},${finalA})`
 }

53-59: Type drift: awareness.user includes colorLight not present in UserAwareness.
Either extend the type or store UI-only fields under a nested key (e.g., metadata) to keep typings accurate.


39-49: Editor readiness race can skip binding.
If getEditor() returns null initially, the current watch won’t re-run. Consider also watching editorRef.value.getEditor() readiness or invoking binding after an editor-ready event.

packages/canvas/container/src/CanvasContainer.vue (2)

526-531: Prefer referencing the collab source directly over copying it

Assigning a new object breaks identity; if useRealtimeCollab().remoteStates is reactive, keep a reference instead of copying.

Option:

-watch(isReady, (newVal) => {
-  if (newVal) {
-    const newStates = useRealtimeCollab().remoteStates
-    remoteStates.value = newStates
-  }
-})
+watch(isReady, (ready) => {
+  if (ready) {
+    // keep reference so nested updates propagate
+    remoteStates.value = useRealtimeCollab().remoteStates
+  }
+})

541-548: Remove console.log in production

Drop noisy logs or gate behind dev flags.

Apply:

-        console.log('remoteStates变动', newVal)
+        // dev-only: console.debug('remoteStates changed', newVal)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 949c395 and d9e81a5.

📒 Files selected for processing (7)
  • packages/canvas/container/src/CanvasContainer.vue (7 hunks)
  • packages/multi-person-collaboration/package.json (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1 hunks)
  • packages/multi-person-collaboration/src/index.ts (1 hunks)
  • packages/plugins/script/package.json (1 hunks)
  • packages/plugins/script/src/Main.vue (1 hunks)
  • packages/plugins/script/src/js/method.ts (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • packages/plugins/script/src/Main.vue
  • packages/plugins/script/package.json
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/multi-person-collaboration/package.json
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/canvas/container/src/CanvasContainer.vue
🧬 Code graph analysis (2)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (87-92)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/plugins/script/src/js/method.ts (1)
  • watch (224-257)
packages/plugins/script/src/js/method.ts (1)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)
  • useCollabMonaco (33-114)
🔇 Additional comments (7)
packages/multi-person-collaboration/package.json (2)

38-41: Workspace deps look correct.


58-64: Peer deps are well-scoped for host-managed installs.

If the host app already pins different yjs/y-websocket minors, confirm no duplicate installations under your workspace tool (pnpm/yarn/npm).

packages/plugins/script/src/js/method.ts (1)

20-20: Import looks correct and aligns with the new package entry.
No issues spotted with the re-exported API usage.

packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)

104-110: Cleanup looks correct.
Binding is destroyed and local awareness is cleared on unmount.

packages/multi-person-collaboration/src/index.ts (1)

1-4: Re-exports look good.
Simple, tree-shakeable entry; no issues.

packages/canvas/container/src/CanvasContainer.vue (2)

95-101: Imports look correct and cohesive

New deps wire collaboration and geometry mapping cleanly.


146-159: Merging local + remote selections is a good approach

The computed allMultiSelectedStates simplifies the template logic.

Comment thread packages/canvas/container/src/CanvasContainer.vue
Comment thread packages/canvas/container/src/CanvasContainer.vue
Comment thread packages/multi-person-collaboration/src/composables/useCollabMonaco.ts Outdated

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (7)
packages/plugins/versioncontrol/package.json (1)

15-19: Repository directory fix looks correct.
The directory now points to packages/plugins/versioncontrol as expected.

packages/canvas/container/src/CanvasContainer.vue (6)

195-199: Throttle remote selection reflow on scroll and clean up on unmount.

Avoids hot-path recompute and memory leaks.

-    const syncRemoteNode = () => {
-      syncRemoteStatesSelections.value = syncRemoteStatesSelections.value.map(mapStateToSelection).filter(Boolean)
-    }
+    let syncRaf = 0
+    const syncRemoteNode = () => {
+      cancelAnimationFrame(syncRaf)
+      syncRaf = requestAnimationFrame(() => {
+        syncRemoteStatesSelections.value = syncRemoteStatesSelections.value.map(mapStateToSelection).filter(Boolean)
+      })
+    }-        win.addEventListener('scroll', syncRemoteNode, true)
+        win.addEventListener('scroll', syncRemoteNode, true)
…
     onUnmounted(() => {
       if (iframe.value?.contentDocument) {
         removeHotkeyEvent(iframe.value.contentDocument)
       }
       window.removeEventListener('message', updateI18n, false)
+      cancelAnimationFrame(syncRaf)
     })

Additionally, remove document-level listeners:

-    document.addEventListener('beforeCanvasReady', beforeCanvasReady)
-    document.addEventListener('canvasReady', canvasReady)
+    document.addEventListener('beforeCanvasReady', beforeCanvasReady)
+    document.addEventListener('canvasReady', canvasReady)
…
     onUnmounted(() => {
+      document.removeEventListener('beforeCanvasReady', beforeCanvasReady)
+      document.removeEventListener('canvasReady', canvasReady)
     })

Consider tracking and removing all win.addEventListener handlers similarly.

Also applies to: 467-469, 527-534


148-153: Don’t hard-code currentUser in production.

Externalize to props/config/auth; avoid committing PII/test avatars.


333-336: Don’t hard-code roomId strings.

Use a config/prop to align with deployments and tests.


528-533: Clean up message subscriptions (per team learning).

Ensure useMessage subscriptions are unsubscribed on unmount.

     onUnmounted(() => {
       if (iframe.value?.contentDocument) {
         removeHotkeyEvent(iframe.value.contentDocument)
       }
       window.removeEventListener('message', updateI18n, false)
+      // cleanup any message subscriptions created by children
+      try { useMessage().unsubscribe && useMessage().unsubscribe() } catch (e) { /* no-op */ }
     })

I can wire a scoped subscribe/unsubscribe utility if you want a tighter pattern.


169-183: Guard against missing selection.id to avoid runtime errors.

Prevents querySelectById(undefined) and mapping nulls.

-    const mapStateToSelection = (state) => {
-      const element = querySelectById(state.selection.id)
+    const mapStateToSelection = (state) => {
+      const id = state?.selection?.id
+      if (!id) return null
+      const element = querySelectById(id)
       if (!element) return null
       const { top, left, width, height } = getRect(element)
       return {
         ...state,
         isRemote: true,
         user: state.user,
         top,
         left,
         width,
         height
       }
     }

470-480: Fix shadowing/redeclaration of remoteStates (build error).

const remoteStates = ref({}) already exists; destructuring remoteStates again redeclares the identifier.

-        const { insertSharedNode, deleteSharedNode, updateUserSelection, remoteStates } = useCollabSchema({
+        const { insertSharedNode, deleteSharedNode, updateUserSelection, remoteStates: awarenessStates } = useCollabSchema({
           roomId: 'schema-yjs',
           currentUser
         })
 
         initHook(HOOK_NAME.useRealtimeCollab, {
           insertSharedNode,
           deleteSharedNode,
           updateUserSelection,
-          remoteStates
+          remoteStates: awarenessStates
         })
🧹 Nitpick comments (16)
packages/collab-ui/cursor/src/composables/useViewport.ts (4)

11-16: Throttle scroll updates to animation frames to reduce churn.

Scroll fires frequently; throttle via rAF to ~60 FPS and keep passive listener.

   const updateViewport = () => {
     if (!isClient) return
     viewport.width = window.innerWidth
     viewport.height = window.innerHeight
     viewport.scrollX = window.scrollX
     viewport.scrollY = window.scrollY
   }
+  let rafId: number | null = null
+  const onScroll = () => {
+    if (rafId !== null) return
+    rafId = requestAnimationFrame(() => {
+      rafId = null
+      updateViewport()
+    })
+  }
 
   onMounted(() => {
     window.addEventListener('resize', updateViewport)
-    window.addEventListener('scroll', updateViewport, { passive: true })
+    window.addEventListener('scroll', onScroll, { passive: true })
     updateViewport()
   })
 
   onUnmounted(() => {
     window.removeEventListener('resize', updateViewport)
-    window.removeEventListener('scroll', updateViewport)
+    window.removeEventListener('scroll', onScroll)
+    if (rafId !== null) cancelAnimationFrame(rafId)
   })

Also applies to: 18-26


1-1: Return toRefs for ergonomic destructuring without breaking current API.

Keeps viewport while also exposing width/height/scrollX/scrollY as refs.

-import { onMounted, onUnmounted, reactive } from 'vue'
+import { onMounted, onUnmounted, reactive, toRefs } from 'vue'
@@
-  return { viewport }
+  return { viewport, ...toRefs(viewport) }

Also applies to: 28-29


3-9: Add lightweight types for the reactive object.

Improves TS intellisense and prevents accidental shape drift.

+interface Viewport {
+  width: number
+  height: number
+  scrollX: number
+  scrollY: number
+}
 export function useViewport() {
-  const isClient = typeof window !== 'undefined'
-  const viewport = reactive({
+  const isClient = typeof window !== 'undefined'
+  const viewport = reactive<Viewport>({
     /* ... */
   })

3-29: Avoid N listeners across many instances; consider a shared singleton store.

If multiple components use this composable, each adds listeners. Centralize listeners and share one reactive viewport across consumers (e.g., module-scoped const viewport = reactive(...) with ref-count or always-on listeners).

tsconfig.app.json (1)

6-8: Disable unused decorator flags in tsconfig.app.json (lines 6–8)
No TypeScript decorators or reflect-metadata imports were found—set "experimentalDecorators" and "emitDecoratorMetadata" to false. The three new engine aliases are already mirrored in devAliasPlugin.js.

packages/plugins/versioncontrol/package.json (1)

10-14: Publish ergonomics: add type declarations and exports.
Expose d.ts and consider explicit exports for better Node/ESM interop.

   "main": "dist/index.js",
   "module": "dist/index.js",
+  "types": "dist/index.d.ts",
   "files": [
     "dist"
   ],
+  "sideEffects": false,
+  "exports": {
+    ".": {
+      "import": "./dist/index.js",
+      "types": "./dist/index.d.ts"
+    }
+  },

Also add a DTS generator (e.g., vite-plugin-dts) in build config.

packages/collab-ui/cursor/package.json (1)

10-13: Add types entry, sideEffects, exports, and vite-plugin-dts
Add declaration support via vite-plugin-dts and surface types in package.json:

   "type": "module",
   "main": "dist/index.js",
   "module": "dist/index.js",
+  "types": "dist/index.d.ts",
   "files": [
     "dist"
   ],
+  "sideEffects": false,
+  "exports": {
+    ".": {
+      "import": "./dist/index.js",
+      "types": "./dist/index.d.ts"
+    }
+  },
@@
   "devDependencies": {
     "@opentiny/tiny-engine-vite-plugin-meta-comments": "workspace:*",
     "@vitejs/plugin-vue": "^5.1.2",
     "@vitejs/plugin-vue-jsx": "^4.0.1",
-    "vite": "^5.4.2"
+    "vite": "^5.4.2",
+    "vite-plugin-dts": "^4.2.0"
   },

Peer dependencies are already aligned to ^3.20.0.

packages/collab-ui/cursor/src/Main.vue (3)

27-27: Provide safe fallbacks for user color.
Avoid undefined style/attribute when user info is missing.

-              :fill="cursor.state.user.color"
+              :fill="cursor.state.user?.color || '#1296db'"
@@
-            :style="{ backgroundColor: cursor.state.user.color }"
+            :style="{ backgroundColor: cursor.state.user?.color || '#1296db' }"

Also applies to: 35-36


11-13: Optional: defensive class binding.
Even with filtering, a defensive optional chain is cheap insurance.

-            active: !cursor.state.cursor.pressed,
+            active: !(cursor.state.cursor?.pressed),

130-131: z-index: consider a scoped token.
99999 can eclipse modals/tooltips; prefer a shared z-index token or lower tier.

packages/multi-person-collaboration/src/composables/useCollabCursor.ts (3)

15-18: Don’t hard-code ws://localhost; support wss and allow override via options.

Improve deployability and security.

 interface UserCollabCursorOptions {
   roomId: string
   currentUser: UserAwareness
+  websocketUrl?: string
 }
…
-  const { awareness } = useYjs(roomId, { websocketUrl: `ws://localhost:${PORT}` })
+  const isSecure = typeof location !== 'undefined' && location.protocol === 'https:'
+  const defaultUrl = `${isSecure ? 'wss' : 'ws'}://${typeof location !== 'undefined' ? location.hostname : 'localhost'}:${PORT}`
+  const { awareness } = useYjs(roomId, { websocketUrl: options.websocketUrl ?? defaultUrl })

Also applies to: 29-31


32-39: Consider throttling broadcasting on mousemove.

Awareness updates on every mousemove can be chatty; throttle via rAF.

-  const updateCursorPosition = (event: MouseEvent) => {
-    updateLocalStateField('cursor', {
-      x: event.pageX,
-      y: event.pageY,
-      pressed: (event.buttons & 1) === 1
-    })
-  }
+  let rafId = 0
+  const updateCursorPosition = (event: MouseEvent) => {
+    cancelAnimationFrame(rafId)
+    rafId = requestAnimationFrame(() => {
+      updateLocalStateField('cursor', {
+        x: event.pageX,
+        y: event.pageY,
+        pressed: (event.buttons & 1) === 1
+      })
+    })
+  }

Cancel rAF on unmount if needed (handled by GC here, but explicit cancel is fine).


33-39: Coordinate space: iframe vs parent overlay.

If the cursor overlay renders outside the iframe, pageX/pageY are relative to the iframe document and will misalign. Normalize to a shared space (e.g., convert to parent coords using iframe.getBoundingClientRect or store normalized [0..1] positions).

packages/canvas/container/src/CanvasContainer.vue (3)

545-560: Drop noisy console logging or gate by env.

Logs may leak user data in production.

-        console.log('remoteStates变动', newVal)
+        if (process.env.NODE_ENV !== 'production') {
+          // eslint-disable-next-line no-console
+          console.log('remoteStates变动', newVal)
+        }

2-2: Guard optional cursor plugin to prevent runtime errors when not registered.

-  <component :is="cursorComponent.entry"></component>
+  <component v-if="cursorComponent?.entry" :is="cursorComponent.entry"></component>
-    const cursorComponent = getMergeMeta('engine.collabUI.cursor')
+    const cursorComponent = getMergeMeta('engine.collabUI.cursor') || {}

Also applies to: 122-123


331-369: Remove unused isScrolling or wire it; it’s dead code.

-        let isScrolling = false
+        // removed unused isScrolling

And drop the assignment in the scroll listener if not used.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9e81a5 and 57230d2.

⛔ Files ignored due to path filters (1)
  • packages/collab-ui/cursor/assets/test.png is excluded by !**/*.png
📒 Files selected for processing (15)
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (3 hunks)
  • packages/canvas/container/src/CanvasContainer.vue (12 hunks)
  • packages/collab-ui/cursor/index.ts (1 hunks)
  • packages/collab-ui/cursor/meta.js (1 hunks)
  • packages/collab-ui/cursor/package.json (1 hunks)
  • packages/collab-ui/cursor/src/Main.vue (1 hunks)
  • packages/collab-ui/cursor/src/composables/useViewport.ts (1 hunks)
  • packages/collab-ui/cursor/vite.config.ts (1 hunks)
  • packages/design-core/package.json (3 hunks)
  • packages/design-core/re-export.js (1 hunks)
  • packages/design-core/registry.js (3 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabCursor.ts (1 hunks)
  • packages/multi-person-collaboration/src/index.ts (1 hunks)
  • packages/plugins/versioncontrol/package.json (1 hunks)
  • tsconfig.app.json (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/design-core/package.json
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js
  • packages/design-core/re-export.js
  • packages/design-core/registry.js
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/collab-ui/cursor/package.json
  • packages/plugins/versioncontrol/package.json
  • tsconfig.app.json
📚 Learning: 2024-12-14T05:53:28.501Z
Learnt from: gene9831
PR: opentiny/tiny-engine#917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of `opentiny/tiny-engine-cli` is `2.0.0`, and documentation should reference this version instead of any release candidates.

Applied to files:

  • packages/plugins/versioncontrol/package.json
📚 Learning: 2025-01-15T02:19:06.755Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#940
File: packages/canvas/DesignCanvas/src/DesignCanvas.vue:0-0
Timestamp: 2025-01-15T02:19:06.755Z
Learning: In Vue components using message subscriptions from opentiny/tiny-engine-meta-register, always clean up subscriptions in the onUnmounted hook using useMessage().unsubscribe() to prevent memory leaks.

Applied to files:

  • packages/canvas/container/src/CanvasContainer.vue
🧬 Code graph analysis (1)
packages/multi-person-collaboration/src/composables/useCollabCursor.ts (4)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (87-92)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-64)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • updateLocalStateField (31-33)
🔇 Additional comments (3)
packages/collab-ui/cursor/meta.js (1)

1-5: LGTM: concise, correct metadata.
No runtime logic; id/type/title look consistent with registry usage.

packages/multi-person-collaboration/src/index.ts (1)

1-5: LGTM: clean aggregator entry.
Re-exports are straightforward and useful for consumers.

packages/collab-ui/cursor/index.ts (1)

13-19: LGTM: entry + metadata merge is correct.
Default export shape matches expected collabUI plugin pattern.

Comment thread packages/canvas/container/src/CanvasContainer.vue Outdated
Comment thread packages/collab-ui/cursor/src/composables/useViewport.ts
Comment thread packages/collab-ui/cursor/src/Main.vue
Comment thread packages/collab-ui/cursor/src/Main.vue
Comment thread packages/collab-ui/cursor/vite.config.ts

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1)

9-13: Fix boolean/object union handling to avoid TypeError when useSourceAlias === true

Accessing useSourceAlias.basePath will throw if useSourceAlias is a boolean. Guard by type-checking before property access.

Apply:

 const getDevAlias = (useSourceAlias) => {
   if (!useSourceAlias) return {}
   const defaultBasePath = path.resolve(process.cwd(), '..') // parent path
-  const basePath = useSourceAlias.basePath || defaultBasePath
+  const basePath =
+    typeof useSourceAlias === 'object' && useSourceAlias?.basePath
+      ? useSourceAlias.basePath
+      : defaultBasePath
   return {
🧹 Nitpick comments (8)
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1)

19-22: Use explicit extension for consistency and to avoid resolution edge-cases

Most aliases here specify file extensions. Align multi-person-collaboration to .ts.

-    '@opentiny/tiny-engine-multi-person-collaboration': path.resolve(
-      basePath,
-      'packages/multi-person-collaboration/src/index'
-    ),
+    '@opentiny/tiny-engine-multi-person-collaboration': path.resolve(
+      basePath,
+      'packages/multi-person-collaboration/src/index.ts'
+    ),
packages/collab-ui/avatar/src/Main.vue (4)

73-80: Avoid wrapping the result of a composable with reactive() unless necessary

If useCollabCursor already returns reactive refs/state, wrapping with reactive can cause unexpected unwrapping. Prefer the returned object directly.

-    const collabState = reactive(
-      useCollabCursor({
-        roomId: 'cursor-yjs',
-        currentUser
-      })
-    )
+    const collabState = useCollabCursor({
+      roomId: 'cursor-yjs',
+      currentUser
+    })

146-174: Debounce watcher triggers only on membership changes; drop deep watching

deep: true causes unnecessary re-runs on value changes; you only diff keys. Also ensure timer is cleared on unmount.

-    watch(
-      () => ({ ...collabState.remoteCursors }),
-      (newStates, oldStates) => {
+    watch(
+      () => ({ ...collabState.remoteCursors }), // tracks ITERATE_KEY
+      (newStates, oldStates) => {
         const newClientIds = Object.keys(newStates)
         const oldClientIds = Object.keys(oldStates)
@@
-      },
-      { deep: true }
+      },
+      { deep: false }
     )

Add unmount cleanup (also covers pending debounce):

@@
-    let notificationTimer = null
+    let notificationTimer = null
@@
+    onBeforeUnmount(() => {
+      clearTimeout(notificationTimer)
+      notificationTimer = null
+    })

47-52: Make notifications screen-reader friendly

Expose as a live region to announce join/leave events.

-    <transition-group name="notifications" tag="div" class="notification-area">
+    <transition-group
+      name="notifications"
+      tag="div"
+      class="notification-area"
+      role="status"
+      aria-live="polite"
+    >

1-54: Consider replacing external avatar CDN with first-party assets or a configurable placeholder

Third-party avatar URLs may leak client IPs and break in offline/air-gapped installs.

packages/collab-ui/avatar/meta.js (1)

1-5: Use an i18n key for the title in meta.js

  • In packages/collab-ui/avatar/meta.js, replace the hard-coded Chinese string ('用户头像展示') with an i18n key (e.g. title: t('collabUI.avatar.title')) to enable translation.
packages/design-core/package.json (1)

50-50: Unify workspace range for consistency

Most engine deps here use workspace:*; consider aligning multi-person-collaboration to reduce confusion unless pinning is intentional.

-    "@opentiny/tiny-engine-multi-person-collaboration": "workspace:~",
+    "@opentiny/tiny-engine-multi-person-collaboration": "workspace:*",
packages/collab-ui/avatar/package.json (1)

1-46: Package manifest OK; consider exports map and peer Vue alignment

Optional improvements:

  • Add exports to mirror other packages and aid ESM resolvers.
  • Align vue peer to project’s ^3.4.23 (current design-core peer) for consistency.
 {
   "name": "@opentiny/tiny-engine-collab-ui-avatar",
   "version": "2.7.0",
+  "exports": {
+    ".": "./dist/index.js"
+  },
   "publishConfig": {
     "access": "public"
   },
@@
   "peerDependencies": {
-    "@opentiny/vue": "^3.20.0",
-    "@opentiny/vue-icon": "^3.20.0",
-    "@opentiny/vue-renderless": "^3.20.0",
-    "vue": "^3.4.15"
+    "@opentiny/vue": "^3.20.0",
+    "@opentiny/vue-icon": "^3.20.0",
+    "@opentiny/vue-renderless": "^3.20.0",
+    "vue": "^3.4.23"
   }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57230d2 and 5db21da.

⛔ Files ignored due to path filters (1)
  • packages/collab-ui/avatar/assets/test.png is excluded by !**/*.png
📒 Files selected for processing (13)
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (3 hunks)
  • packages/collab-ui/avatar/index.ts (1 hunks)
  • packages/collab-ui/avatar/meta.js (1 hunks)
  • packages/collab-ui/avatar/package.json (1 hunks)
  • packages/collab-ui/avatar/src/Main.vue (1 hunks)
  • packages/collab-ui/avatar/vite.config.ts (1 hunks)
  • packages/design-core/package.json (3 hunks)
  • packages/design-core/re-export.js (1 hunks)
  • packages/design-core/registry.js (3 hunks)
  • packages/layout/src/defaultLayout.js (2 hunks)
  • packages/multi-person-collaboration/src/composables/useYjs.ts (1 hunks)
  • packages/register/src/constants.ts (1 hunks)
  • tsconfig.app.json (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/layout/src/defaultLayout.js
  • packages/design-core/re-export.js
  • packages/design-core/registry.js
  • tsconfig.app.json
  • packages/multi-person-collaboration/src/composables/useYjs.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/design-core/package.json
  • packages/collab-ui/avatar/package.json
  • packages/build/vite-config/src/vite-plugins/devAliasPlugin.js
🔇 Additional comments (5)
packages/build/vite-config/src/vite-plugins/devAliasPlugin.js (1)

36-36: Alias targets exist and no self-import cycles detected.

packages/collab-ui/avatar/src/Main.vue (1)

23-40: Status always shows “online”; bind to real presence if available

If collab state tracks presence, toggle .online conditionally; otherwise this misleads users.

Do you have an isOnline/presence flag in collabState.remoteCursors[id]? If yes, I can wire it here.

packages/register/src/constants.ts (1)

79-83: Verified registry and layout wiring for VersionControl and Avatar
Registry entries in packages/design-core/registry.js and META_APP placements in packages/layout/src/defaultLayout.js are present and correct. No further changes needed.

packages/collab-ui/avatar/index.ts (1)

13-19: LGTM — minimal entry wrapper is correct

Default export shape { ...metaData, entry } matches existing plugin pattern. No issues.

packages/design-core/package.json (1)

65-65: Downstream wiring for new plugins verified Dev aliases, re-exports, registry entries, and layout usage for VersionControl, Cursor, and Avatar are all present.

Comment thread packages/collab-ui/avatar/src/Main.vue
Comment thread packages/collab-ui/avatar/vite.config.ts

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (7)
packages/canvas/container/src/CanvasContainer.vue (6)

169-183: Null‑safety for selection.id to avoid runtime errors

Guard before querySelectById; remote payloads can be partial.

-    const mapStateToSelection = (state) => {
-      const element = querySelectById(state.selection.id)
+    const mapStateToSelection = (state) => {
+      const id = state?.selection?.id
+      if (!id) return null
+      const element = querySelectById(id)
       if (!element) return null
       const { top, left, width, height } = getRect(element)
       return {
         ...state,
         isRemote: true,
         user: state.user,
         top,
         left,
         width,
         height
       }
     }

196-199: Throttle expensive reflow on scroll

Map DOM rects at most once per frame; prevents scroll jank.

-    const syncRemoteNode = () => {
-      syncRemoteStatesSelections.value = syncRemoteStatesSelections.value.map(mapStateToSelection).filter(Boolean)
-    }
+    let syncRaf = 0
+    const syncRemoteNode = () => {
+      cancelAnimationFrame(syncRaf)
+      syncRaf = requestAnimationFrame(() => {
+        syncRemoteStatesSelections.value = syncRemoteStatesSelections.value
+          .map(mapStateToSelection)
+          .filter(Boolean)
+      })
+    }

Follow-up: cancel the pending rAF in onUnmounted (see cleanup diff below).


333-337: Align with cursor API and pass mousedown event

Rename method and forward the event for accurate positioning.

-        const { updateCursorPositioin, mouseUpHandler, mouseDownHandler } = useCollabCursor({
+        const { updateCursorPosition, mouseUpHandler, mouseDownHandler } = useCollabCursor({
           roomId: 'cursor-yjs',
           currentUser
         })
@@
-        win.addEventListener('mousedown', (event) => {
-          mouseDownHandler()
+        win.addEventListener('mousedown', (event) => {
+          mouseDownHandler(event)
@@
-            updateCursorPositioin(ev)
+            updateCursorPosition(ev)

Also applies to: 339-341, 371-374


148-153: Externalize user identity and room ids

Hard‑coded user/rooms are not production‑safe; wire via props/config or DI and avoid committing PII/test avatars.

-    const currentUser = {
-      id: 2,
-      name: 'Bob',
-      color: '#4ECDC4',
-      avatarUrl: 'https://i.pravatar.cc/150?img=2'
-    }
+    // TODO: inject via props/config; this is a dev default
+    const currentUser = getMergeMeta('engine.collab.currentUser') || { id: 2, name: 'Dev', color: '#4ECDC4' }
@@
-        } = useCollabSchema({
-          roomId: 'schema-yjs',
+        } = useCollabSchema({
+          roomId: getMergeMeta('engine.collab.room.schema') || 'schema-yjs',
           currentUser
         })

If a config service exists, prefer it over getMergeMeta.

Also applies to: 482-484


470-497: Duplicate identifier: remoteStates declared twice

Shadowing causes a compile error and confusion. Rename the destructured prop.

-        const {
+        const {
           insertSharedNode,
           deleteSharedNode,
           updateUserSelection,
           moveDownSharedNode,
           moveUpSharedNode,
           updateStyleNode,
           updatePropsNode,
           updateMethodNode,
-          updateAttributesNode,
-          remoteStates
+          updateAttributesNode,
+          remoteStates: awarenessRemoteStates
         } = useCollabSchema({
           roomId: 'schema-yjs',
           currentUser
         })
@@
-        initHook(HOOK_NAME.useRealtimeCollab, {
+        initHook(HOOK_NAME.useRealtimeCollab, {
           insertSharedNode,
           deleteSharedNode,
           updateUserSelection,
           moveDownSharedNode,
           moveUpSharedNode,
           updateStyleNode,
           updatePropsNode,
           updateMethodNode,
-          updateAttributesNode,
-          remoteStates
+          updateAttributesNode,
+          remoteStates: awarenessRemoteStates
         })

Also update any downstream references if they assumed the old name.


2-4: Stabilize key and guard optional cursor component

Remote states may lack id; current key is unstable. Also guard cursorComponent access.

-  <component :is="cursorComponent.entry"></component>
-  <div v-for="state in allMultiSelectedStates" :key="state.id">
+  <component v-if="cursorComponent && cursorComponent.entry" :is="cursorComponent.entry"></component>
+  <div v-for="(state, idx) in allMultiSelectedStates"
+       :key="state.id || (state.selection && state.selection.id) || (state.user && state.user.id) || `sel-${idx}`">
packages/multi-person-collaboration/src/composables/useCollabSchema.ts (1)

112-121: Fix event listener leak: off() uses a different function reference

Register a stable handler and remove the same reference on unmount; otherwise listeners accumulate across mounts.

Apply:

-  // 等 provider 同步完成后,重建映射
-  provider.value!.on('sync', (isSynced: boolean) => {
-    if (isSynced) {
-      // eslint-disable-next-line no-console
-      console.log(`[schema-yjs] Yjs 同步完成,重建映射`)
-      const pageSchema = toRaw(useCanvas().getPageSchema())
-      schemaModel.operationHandler.rebuildYNodeMap(pageSchema as RootNode)
-    }
-  })
+  // 等 provider 同步完成后,重建映射
+  const onSync = (isSynced: boolean) => {
+    if (!isSynced) return
+    // eslint-disable-next-line no-console
+    console.log(`[schema-yjs] Yjs 同步完成,重建映射`)
+    const pageSchema = toRaw(useCanvas().getPageSchema())
+    schemaModel.operationHandler.rebuildYNodeMap(pageSchema as RootNode)
+  }
+  provider.value!.on('sync', onSync)
@@
   // 组件卸载时取消监听
   onUnmounted(() => {
     schemaManager.destroyObserver(roomId)
-    provider.value?.off('sync', () => {})
+    provider.value?.off('sync', onSync)
     // awareness.value?.destroy()
   })

Also applies to: 123-127

🧹 Nitpick comments (9)
packages/multi-person-collaboration/src/composables/useCollabSchema.ts (2)

57-60: Guard provider before createSchema

Avoid non-null assertion; fail fast with a clear error if provider isn’t ready.

Apply:

   // 获取 NodeSchemaModel 实例
   const schemaManager = SchemaManager.getInstance()
-  const schemaModel = schemaManager.createSchema(roomId, provider.value!)
+  if (!provider.value) {
+    throw new Error(`[useCollabSchema] Provider not initialized for room ${roomId}`)
+  }
+  const schemaModel = schemaManager.createSchema(roomId, provider.value)

If ProviderManager guarantees non-null, confirm and we can drop the check.


104-107: Tighten typing for selection payload

Use the declared Shape to prevent accidental shape drift.

-  const updateUserSelection = (selectedNode: any) => {
+  const updateUserSelection = (selectedNode: SchemaAwarenessState['selection']) => {
     updateLocalStateField('selection', selectedNode)
   }
packages/canvas/container/src/CanvasContainer.vue (1)

544-551: Clean up global/iframe listeners and pending rAF on unmount

Prevents leaks across mounts.

     onUnmounted(() => {
       if (iframe.value?.contentDocument) {
         removeHotkeyEvent(iframe.value.contentDocument)
       }
       window.removeEventListener('message', updateI18n, false)
+      if (iframe.value?.contentWindow) {
+        const w = iframe.value.contentWindow
+        w.removeEventListener('scroll', syncNodeScroll, true)
+        w.removeEventListener('scroll', syncRemoteNode, true)
+      }
+      document.removeEventListener('beforeCanvasReady', beforeCanvasReady)
+      document.removeEventListener('canvasReady', canvasReady)
+      if (typeof cancelAnimationFrame === 'function') {
+        // syncRaf declared near syncRemoteNode
+        try { cancelAnimationFrame(syncRaf) } catch {}
+      }
     })

Optional follow‑up: also store/remove the window mousedown/dragenter handlers created in run().

packages/settings/events/src/components/AdvanceConfig.vue (6)

83-83: Hoist and reuse the collab hook instance.

Avoid repeated useRealtimeCollab() calls; keep a single instance for readability, testability, and to prevent any accidental per-call re-instantiation. Also consider passing an origin/clientId if supported to prevent echo loops.

Apply:

@@
-import { useProperties, useCanvas, useRealtimeCollab } from '@opentiny/tiny-engine-meta-register'
+import { useProperties, useCanvas, useRealtimeCollab } from '@opentiny/tiny-engine-meta-register'
@@
   setup() {
     const { pageState } = useCanvas()
+    const collab = useRealtimeCollab()
@@
-        useRealtimeCollab().updateAttributesNode({ type: 'condition', value, nodeId: schema.id })
+        collab.updateAttributesNode({ type: 'condition', value, nodeId: schema.id })
@@
-        useRealtimeCollab().updateAttributesNode({ type: 'condition', value, nodeId: schema.id })
+        collab.updateAttributesNode({ type: 'condition', value, nodeId: schema.id })
@@
-      useRealtimeCollab().updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })
+      collab.updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })
@@
-        useRealtimeCollab().updateAttributesNode({ type: 'loop', value: newLoop, nodeId: schema.id })
+        collab.updateAttributesNode({ type: 'loop', value: newLoop, nodeId: schema.id })
@@
-        useRealtimeCollab().updateAttributesNode({ type: 'clean', nodeId: schema.id })
+        collab.updateAttributesNode({ type: 'clean', nodeId: schema.id })
@@
-      useRealtimeCollab().updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })
+      collab.updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })

201-215: Guard absent schema and reduce event noise in setLoopIndex.

schema can be falsy; add a fast return. Also, @update:modelValue on inputs may fire per keystroke—consider debouncing or emitting on blur to avoid flooding collab.

 const setLoopIndex = (value) => {
-  const schema = useProperties().getSchema()
+  const schema = useProperties().getSchema()
+  if (!schema) return
   let loopArgs = schema.loopArgs
   const { operateNode } = useCanvas()
@@
   operateNode({ type: 'updateAttributes', id: schema.id, value: { loopArgs } })
   // 多人协同实现
-  collab.updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })
+  collab.updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })

222-229: Batch loop + loopArgs updates to minimize churn.

Setting loop triggers a second update via setLoopIndex. If the collab API supports a multi-attr payload, send both in one message to reduce latency and flicker on peers.

Example (if supported by API):

// after computing both newLoop and loopArgs
collab.updateAttributesNode({
  type: 'updateAttributes',
  value: { loop: newLoop, loopArgs },
  nodeId: schema.id
})

230-236: Clarify what clean wipes and ensure key changes are synced.

type: 'clean' is vague. If it wipes only loop/loopArgs, great—document it. If broader, risk of drift. Also, setLoopKey() mutates props.key locally but is not broadcast—peers may diverge after loop toggles.

Recommend: broadcast key alongside loop cleanup, or make setLoopKey itself emit.

See proposed change to setLoopKey below.


129-161: Sync props.key over collab and fix numeric check.

  • Numeric check: Number(value).toString() !== 'NaN' is brittle. Use Number.isNaN.
  • Missing collab emit: peers won't receive key updates.
 const setLoopKey = (value = '') => {
   value = value.replace(/\s*/g, '')
   const { getSchema, setProp } = useProperties()
   const schema = getSchema()
@@
-  const isNumber = Number(value).toString() !== 'NaN'
+  const isNumber = !Number.isNaN(Number(value))
@@
-  setProp('key', newPropsKey)
+  setProp('key', newPropsKey)
+  // 多人协同实现:同步 key 变化
+  collab.updateAttributesNode({ type: 'key', value: newPropsKey, nodeId: schema.id })
 }

If type: 'key' isn’t a recognized verb, emit a generic updateAttributes with { props: { key: newPropsKey } } or whatever your handler expects.


242-256: Mirror safeguards from setLoopIndex in setLoopItem.

Add a null-guard for schema and consider debouncing to avoid per-keystroke collab spam.

 const setLoopItem = (value) => {
-  const schema = useProperties().getSchema()
+  const schema = useProperties().getSchema()
+  if (!schema) return
   let loopArgs = schema.loopArgs
   const { operateNode } = useCanvas()
@@
   operateNode({ type: 'updateAttributes', id: schema.id, value: { loopArgs } })
   // 多人协同实现
-  collab.updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })
+  collab.updateAttributesNode({ type: 'loopArgs', value: loopArgs, nodeId: schema.id })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e11b4d0 and a9c4119.

📒 Files selected for processing (7)
  • packages/canvas/container/src/CanvasContainer.vue (12 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabSchema.ts (1 hunks)
  • packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1 hunks)
  • packages/multi-person-collaboration/src/operation/operationHandler .ts (1 hunks)
  • packages/multi-person-collaboration/src/services/schemaManager.ts (1 hunks)
  • packages/multi-person-collaboration/src/type.ts (1 hunks)
  • packages/settings/events/src/components/AdvanceConfig.vue (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/multi-person-collaboration/src/operation/operationHandler .ts
  • packages/multi-person-collaboration/src/type.ts
  • packages/multi-person-collaboration/src/models/NodeSchemaModel.ts
  • packages/multi-person-collaboration/src/services/schemaManager.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2024-10-10T02:47:46.239Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:16-16
Timestamp: 2024-10-10T02:47:46.239Z
Learning: In `packages/toolbars/preview/src/Main.vue`, within the `preview` function, the `getMergeMeta` method is used at lines 64 and 65 to retrieve `engine.config` configurations.

Applied to files:

  • packages/settings/events/src/components/AdvanceConfig.vue
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/canvas/container/src/CanvasContainer.vue
📚 Learning: 2025-01-14T08:45:57.032Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in `packages/canvas/render/src/application-function/global-state.ts` is migrated from an existing codebase and should be handled with care when making modifications.

Applied to files:

  • packages/canvas/container/src/CanvasContainer.vue
📚 Learning: 2025-01-15T02:19:06.755Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#940
File: packages/canvas/DesignCanvas/src/DesignCanvas.vue:0-0
Timestamp: 2025-01-15T02:19:06.755Z
Learning: In Vue components using message subscriptions from opentiny/tiny-engine-meta-register, always clean up subscriptions in the onUnmounted hook using useMessage().unsubscribe() to prevent memory leaks.

Applied to files:

  • packages/canvas/container/src/CanvasContainer.vue
🧬 Code graph analysis (1)
packages/multi-person-collaboration/src/composables/useCollabSchema.ts (7)
packages/multi-person-collaboration/src/type.ts (7)
  • UserAwareness (117-122)
  • Node (23-34)
  • RootNode (36-48)
  • PositionType (72-72)
  • POSITION (62-70)
  • UpdateMethodsOperation (105-107)
  • UpdateAttributesOperation (109-113)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)
  • useYjs (25-90)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-58)
packages/multi-person-collaboration/src/services/schemaManager.ts (1)
  • SchemaManager (47-538)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • updateLocalStateField (31-33)
packages/register/src/hooks.ts (1)
  • useCanvas (79-79)

Comment thread packages/settings/events/src/components/AdvanceConfig.vue

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (12)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)

34-37: Don’t hard‑code ws://localhost; make websocketUrl configurable and auto‑derive ws/wss.

Hardcoding breaks HTTPS (mixed content) and non-local deployments. Add websocketUrl?: string to options and derive a safe default from window.location (wss for https).

Apply:

 interface UseCollabMonacoOptions {
   currentUser: UserAwareness
   editorRef: any
   roomId: string
   fieldName: string
+  websocketUrl?: string
 }
 
 export function useCollabMonaco(options: UseCollabMonacoOptions) {
-  const { currentUser, editorRef, roomId, fieldName } = options
-  const { ydoc, awareness, provider } = useYjs(roomId, { websocketUrl: `ws://localhost:${PORT}` })
+  const { currentUser, editorRef, roomId, fieldName, websocketUrl } = options
+  const wsDefault = (() => {
+    if (typeof window === 'undefined') return `ws://localhost:${PORT}`
+    const isHttps = window.location.protocol === 'https:'
+    const proto = isHttps ? 'wss' : 'ws'
+    // prefer explicit PORT only for localhost; otherwise reuse current host:port
+    const host = window.location.hostname === 'localhost' ? `localhost:${PORT}` : window.location.host
+    return `${proto}://${host}`
+  })()
+  const { ydoc, awareness, provider } = useYjs(roomId, { websocketUrl: websocketUrl || wsDefault })
packages/multi-person-collaboration/src/type.ts (2)

62-73: De-duplicate POSITION: re-export from canvas container.

Keep a single source of truth to prevent drift.

-// 定义插入位置类型
-export const POSITION = Object.freeze({
-  TOP: 'top',
-  BOTTOM: 'bottom',
-  LEFT: 'left',
-  RIGHT: 'right',
-  IN: 'in',
-  OUT: 'out',
-  REPLACE: 'replace'
-} as const)
-
-export type PositionType = typeof POSITION[keyof typeof POSITION]
+// Re-export shared constants/types to avoid duplication
+export { POSITION } from '@opentiny/tiny-engine-canvas-container'
+export type { PositionType } from '@opentiny/tiny-engine-canvas-container'

130-133: Selection shape mismatch with canvas usage. Add NodeSelection and use a union.

-export interface Selection {
-  anchor: number
-  head: number
-}
+export interface TextSelection {
+  anchor: number
+  head: number
+}
+export interface NodeSelection {
+  id: string
+}
+export type Selection = TextSelection | NodeSelection
packages/multi-person-collaboration/src/services/schemaManager.ts (4)

186-194: Bug: schemaId property typo breaks move events (schemeId → schemaId).

-              schemaId: payload.schemeId,
+              schemaId: payload.schemaId,

473-478: Bounds check for array swap is incomplete. Validate both indices.

-          if (patch.targetIndex > -1 && patch.swapIndex < childrenArray.length) {
+          if (
+            patch.targetIndex > -1 &&
+            patch.targetIndex < childrenArray.length &&
+            patch.swapIndex > -1 &&
+            patch.swapIndex < childrenArray.length
+          ) {

101-122: Initial-sync gate never lifts when provider is absent; also store and later remove the sync listener.

+  private providerSync = new Map<string, { provider: YjsProvider; onSync: (s: boolean) => void }>()
@@
-      if (provider) {
-        provider.on('sync', (isSynced: boolean) => {
+      if (provider) {
+        const onSync = (isSynced: boolean) => {
           if (isSynced) {
@@
-            this.initialSyncDone.set(docName, true)
+            this.initialSyncDone.set(docName, true)
           }
-        })
+        }
+        provider.on('sync', onSync)
+        this.providerSync.set(docName, { provider, onSync })
+      } else {
+        // No remote provider; consider initial sync done so remote patches aren't blocked
+        this.initialSyncDone.set(docName, true)
       }
@@
-      if (yMap.size === 0) {
+      if (yMap.size === 0) {
         ydoc.transact(() => {
           toYjs(yMap!, toRaw(useCanvas().getPageSchema()))
         }, IGNORE_OBSERVER_ORIGIN)
+        if (!provider) this.initialSyncDone.set(docName, true)
       }

And detach in destroySchema:

   public destroySchema(docName: string): void {
     this.destroyObserver(docName)
+    // detach provider sync listener
+    const ps = this.providerSync.get(docName)
+    if (ps) {
+      ps.provider?.off?.('sync', ps.onSync)
+      this.providerSync.delete(docName)
+    }
     this.schemaMap.delete(docName)
     this.nodeSchemaModelMap.delete(docName)
   }

Also applies to: 124-129


151-156: Leak: eventListeners for app_events not cleaned in destroySchema.

   public destroySchema(docName: string): void {
     this.destroyObserver(docName)
+    // Clean up event listeners
+    if (this.eventListeners.has(docName)) {
+      const { map, cb } = this.eventListeners.get(docName)
+      map.unobserve(cb)
+      this.eventListeners.delete(docName)
+    }
     this.schemaMap.delete(docName)
     this.nodeSchemaModelMap.delete(docName)
   }
packages/multi-person-collaboration/src/operation/operationHandler .ts (5)

31-33: Use the owning Y.Doc of yMap to avoid cross‑doc transactions.

-    // 获得 yDoc 用于执行事务
-    const docManager = DocManager.getInstance()
-    this.yDoc = docManager.getOrCreateDoc(docName)
+    // Use the doc that owns yMap; fallback to DocManager
+    const existingDoc = (this.yMap as any).doc as Y.Doc | undefined
+    if (existingDoc) {
+      this.yDoc = existingDoc
+    } else {
+      const docManager = DocManager.getInstance()
+      this.yDoc = docManager.getOrCreateDoc(docName)
+    }

82-90: 'out' insertion corrupts Yjs structure (assigns non-Y.Array children).

       case 'out':
         if (referenceNode) {
-          const childrenNode = Array.isArray(referenceNode) ? [...referenceNode] : [referenceNode]
-          yNode.get('newNode').set('children', childrenNode)
-
-          yChildren.get(index).set('_node_deleted', true)
-          yChildren.insert(index, [yNode])
+          if (index === -1) index = yChildren.length
+          const original = yChildren.get(index) as Y.Map<any>
+          const yNewNode = yNode.get('newNode') as Y.Map<any>
+          const childArray = new Y.Array<Y.Map<any>>()
+          childArray.insert(0, [original])
+          yNewNode.set('children', childArray)
+          original.set('_node_deleted', true)
+          yChildren.insert(index, [yNode])
         }
         break

180-217: updatedProps: illegal Yjs struct reuse and non-atomic updates. Deep-clone via JSON→toYjs inside a transaction.

   public updatedProps(operation: UpdatePropsOperation) {
     const { newProps, nodeId, overwrite } = operation
     let node = this.getYNode(nodeId)
@@
-    const yNewProps = new Y.Map<any>() // 新的 props
-    const propsMap = node.get('props') as Y.Map<any> // 旧的 props
-
-    if (overwrite) {
-      // 覆盖模式
-      for (const [k, v] of Object.entries(newProps || {})) {
-        yNewProps.set(k, v)
-      }
-    } else {
-      // 先复制旧的
-      if (propsMap) {
-        propsMap.forEach((val, key) => {
-          yNewProps.set(key, val)
-        })
-      }
-
-      // 再合并新的
-      for (const [k, v] of Object.entries(newProps) || {}) {
-        yNewProps.set(k, v)
-      }
-    }
-
-    // 元数据,用于补丁操作
-    const meta = new Y.Map<any>()
-    meta.set('nodeId', nodeId)
-    meta.set('overwrite', overwrite)
-
-    yNewProps.set('meta', meta)
-    node.set('props', yNewProps)
+    this.yDoc.transact(() => {
+      const propsMap = node.get('props') as Y.Map<any> | undefined
+      const baseJson = overwrite ? {} : (propsMap?.toJSON?.() ?? {})
+      const merged = { ...baseJson, ...(newProps || {}) }
+
+      const meta = new Y.Map<any>()
+      meta.set('nodeId', nodeId)
+      meta.set('overwrite', overwrite)
+
+      const yMerged = new Y.Map<any>()
+      toYjs(yMerged, merged)
+      yMerged.set('meta', meta)
+      node.set('props', yMerged)
+    })
   }

219-249: updatedMethods: ensure props map exists, wrap in one transaction, and don’t delete immediately on soft-delete.

   public updatedMethods(operation: UpdateMethodsOperation) {
-    if (operation.type === 'root') {
-      const methods = operation.methods
-      // 根节点直接设置 methods 不需要 id
-      this.yMap.set('methods', methods)
-    } else if (operation.type === 'node') {
-      const { nodeId, methodsName, methods } = operation
-      const node = this.getYNode(nodeId)
-      if (node) {
-        const nodeProps = node.get('props')
-        nodeProps.set(methodsName, {
-          ...methods,
-          meta: { nodeId }
-        })
-      }
-    } else if (operation.type === 'delete-method') {
-      const { nodeId, methodsName } = operation
-      const node = this.getYNode(nodeId)
-      if (node) {
-        const nodeProps = node.get('props')
-        // 依旧软删除
-        nodeProps.set(methodsName, {
-          _methods_deleted: true,
-          meta: { nodeId }
-        })
-        // 软删除后直接硬删除删除,保证 yMap 数据干净
-        nodeProps.delete(methodsName)
-      }
-    }
+    this.yDoc.transact(() => {
+      if (operation.type === 'root') {
+        this.yMap.set('methods', operation.methods)
+        return
+      }
+      if (operation.type === 'node') {
+        const { nodeId, methodsName, methods } = operation
+        const node = this.getYNode(nodeId)
+        if (!node) return
+        let nodeProps = node.get('props') as Y.Map<any> | undefined
+        if (!(nodeProps instanceof Y.Map)) {
+          nodeProps = new Y.Map()
+          node.set('props', nodeProps)
+        }
+        nodeProps.set(methodsName, { methods, meta: { nodeId } })
+        return
+      }
+      if (operation.type === 'delete-method') {
+        const { nodeId, methodsName } = operation
+        const node = this.getYNode(nodeId)
+        if (!node) return
+        const nodeProps = node.get('props') as Y.Map<any> | undefined
+        if (!nodeProps) return
+        // Soft-delete only; let observers emit a delete patch
+        nodeProps.set(methodsName, { _methods_deleted: true, meta: { nodeId } })
+        return
+      }
+    })
   }

172-177: updatedStyle: guard nulls and ensure props map exists.

-    const targetNode = this.getYNode(nodeId)
-    targetNode?.get('props').set('className', `${className}_${nodeId}`)
+    const targetNode = this.getYNode(nodeId)
+    if (targetNode) {
+      let props = targetNode.get('props') as Y.Map<any> | undefined
+      if (!(props instanceof Y.Map)) {
+        props = new Y.Map()
+        targetNode.set('props', props)
+      }
+      props.set('className', `${className}_${nodeId}`)
+    }
🧹 Nitpick comments (12)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (4)

107-113: Missing cleanup for provider ‘sync’ listener → potential memory/dup listeners.

Detach the event handler on unmount (and before rebinding if provider changes).

Apply:

-        yjsProvider.on('sync', syncHandler)
+        yjsProvider.on('sync', syncHandler)
+        // keep unbind for cleanup
+        let unbindSync = () => yjsProvider.off('sync', syncHandler)
 
         // 如果 provider 已经同步完成(例如,在热重载后),手动触发一次
         if (yjsProvider.synced) {
           syncHandler(true)
         }
       }
     },
     { immediate: true }
   )
 
 onUnmounted(() => {
+  // remove provider listener if present
+  try {
+    // yjsProvider may differ; guard via awareness/provider refs
+    provider.value?.off?.('sync', syncHandler as any)
+  } catch {}
   if (monacoBinding.value) {
     monacoBinding.value.destroy()
     monacoBinding.value = null
   }
   if (awareness.value) {
     awareness.value.setLocalStateField('user', null)
   }
 })

Also applies to: 118-126


45-51: Avoid double getEditor() call; compute once.

Minor tidy to prevent duplicate invocation.

-        const editor = monacoComponent.getEditor() ? monacoComponent.getEditor() : monacoComponent
+        const editor =
+          typeof monacoComponent.getEditor === 'function'
+            ? monacoComponent.getEditor()
+            : monacoComponent

59-61: Reuse yText instance for binding.

Slight efficiency/readability improvement.

-        const yText = ydoc.getText(fieldName)
+        const yText = ydoc.getText(fieldName)
...
-            monacoBinding.value = new MonacoBinding(ydoc.getText(fieldName), model, new Set([editor]), yjsAwareness)
+            monacoBinding.value = new MonacoBinding(yText, model, new Set([editor]), yjsAwareness)

Also applies to: 100-101


15-32: Provide safe fallback when color isn't a valid hex.

Returning the original invalid color can break consumers expecting rgba. Fall back to a default translucent color.

-  if (!hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return hex
+  if (!hex || !hex.startsWith('#') || (hex.length !== 7 && hex.length !== 4)) return `rgba(0,0,0,${alpha})`

Also applies to: 97-98

packages/plugins/script/src/js/method.ts (2)

247-259: Don’t hard‑code demo user or fixed room; derive from real identity and context.

Use authenticated user info and scope roomId by project/page to avoid cross‑document collisions; pass websocketUrl through when available.

-      const currentUser = {
-        id: 2,
-        name: 'Bob',
-        color: '#4ECDC4',
-        avatarUrl: 'https://i.pravatar.cc/150?img=2'
-      }
+      const user = /* TODO: read from app auth/user store */
+        { id: 'unknown', name: 'Guest', color: '#4ECDC4', avatarUrl: '' }
+      const pageId = useCanvas().pageState?.pageSchema?.id || 'default'
+      const roomId = `monaco-yjs:${pageId}`
 
-      useCollabMonaco({
-        currentUser,
+      useCollabMonaco({
+        currentUser: user,
         editorRef: monaco,
-        roomId: 'monaco-yjs',
-        fieldName: 'monaco-code'
+        roomId,
+        fieldName: 'monaco-code',
+        // websocketUrl: runtimeConfig.wsUrl // if available
       })

94-99: Guard realtime sync calls and make them non‑blocking.

If the collab hook isn’t registered, these calls will throw. Also avoid blocking saves on network.

-  useRealtimeCollab().updateMethodNode({
+  try {
+    useRealtimeCollab()?.updateMethodNode?.({
       type: 'root',
       methods: { ...methods, [name]: methodItem }
-  })
+    })
+  } catch (e) {
+    // eslint-disable-next-line no-console
+    console.warn('[realtime-collab] updateMethodNode failed (saveMethod)', e)
+  }
-  useRealtimeCollab().updateMethodNode({
+  try {
+    useRealtimeCollab()?.updateMethodNode?.({
       type: 'root',
       methods: { ...newMethods }
-  })
+    })
+  } catch (e) {
+    // eslint-disable-next-line no-console
+    console.warn('[realtime-collab] updateMethodNode failed (saveMethods)', e)
+  }

Also applies to: 147-151

packages/multi-person-collaboration/src/type.ts (2)

74-79: Strongly type InsertOperation.position with an internal InsertPosition union (not PositionType).

This position is the low-level insertion intent ('before'|'after'|...); using PositionType ('top'|'bottom'|...) would be semantically wrong.

+// Low-level insertion intent for OperationHandler
+export type InsertPosition = 'before' | 'after' | 'replace' | 'out' | 'bottom'
 export interface InsertOperation {
   parentId: string
   newNodeData: Node
-  position: string
+  position: InsertPosition
   referTargetNodeId?: string
 }

110-116: Align loopArgs type with Node.loopArgs (string[]).

 export type UpdateAttributesOperation =
   | { type: 'loop'; value: Record<string, any>; nodeId: string }
-  | { type: 'loopArgs'; value: Record<string, any>; nodeId: string }
+  | { type: 'loopArgs'; value: string[]; nodeId: string }
   | { type: 'condition'; value: boolean; nodeId: string }
   | { type: 'clean'; nodeId: string }
packages/multi-person-collaboration/src/services/schemaManager.ts (1)

419-423: Remove noisy debug log or guard behind env.

-              if (insertedYjsItems.length > 0) {
-                // eslint-disable-next-line no-console
-                console.log('===insert yjsItem[0].toJSON() (from target)===', insertedYjsItems[0].toJSON())
-              }
+              // optional: add debug logging behind a dev flag if needed
packages/multi-person-collaboration/src/operation/operationHandler .ts (1)

97-103: Handle missing reference for 'bottom' insertion.

       case 'bottom':
-        yChildren.insert(index + 1, [yNode])
+        index = index === -1 ? yChildren.length - 1 : index
+        yChildren.insert(index + 1, [yNode])
         break
packages/settings/events/src/components/BindEvents.vue (2)

91-93: Guard the collab hook for non-collab/disabled builds.

If the collab plugin isn’t registered, calling useRealtimeCollab() may fail. Use optional chaining or a type check where you invoke it.


198-203: Guard the collab call; event-name payload and 'delete-method' are correct.

  • Verified: updateMethodNode expects the event prop key (methodsName) and OperationHandler.updatedMethods handles 'delete-method' (marks _methods_deleted then deletes the prop). See packages/multi-person-collaboration/src/type.ts and packages/multi-person-collaboration/src/operation/operationHandler .ts.
  • Action (file: packages/settings/events/src/components/BindEvents.vue — deleteAction): wrap the RTC call with a nodeId + method existence check; do not manually mutate state.bindActions (removing pageState.currentSchema.props already updates the UI via the existing watchEffect).

Suggested minimal replacement:

-        // 多人协作适配
-        useRealtimeCollab().updateMethodNode({
-          type: 'delete-method',
-          nodeId: pageState?.currentSchema?.id,
-          methodsName: action.eventName
-        })
+        // 多人协作适配(防御式调用)
+        const nodeId = pageState?.currentSchema?.id
+        const collab = useRealtimeCollab && useRealtimeCollab()
+        if (collab?.updateMethodNode && nodeId) {
+          collab.updateMethodNode({
+            type: 'delete-method',
+            nodeId,
+            methodsName: action.eventName
+          })
+        }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9c4119 and d6aa92e.

📒 Files selected for processing (7)
  • packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabSchema.ts (1 hunks)
  • packages/multi-person-collaboration/src/operation/operationHandler .ts (1 hunks)
  • packages/multi-person-collaboration/src/services/schemaManager.ts (1 hunks)
  • packages/multi-person-collaboration/src/type.ts (1 hunks)
  • packages/plugins/script/src/js/method.ts (4 hunks)
  • packages/settings/events/src/components/BindEvents.vue (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/multi-person-collaboration/src/composables/useCollabSchema.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-01-15T02:19:06.755Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#940
File: packages/canvas/DesignCanvas/src/DesignCanvas.vue:0-0
Timestamp: 2025-01-15T02:19:06.755Z
Learning: In Vue components using message subscriptions from opentiny/tiny-engine-meta-register, always clean up subscriptions in the onUnmounted hook using useMessage().unsubscribe() to prevent memory leaks.

Applied to files:

  • packages/settings/events/src/components/BindEvents.vue
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/multi-person-collaboration/src/type.ts
📚 Learning: 2025-01-14T08:44:09.485Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/controller.ts:1-7
Timestamp: 2025-01-14T08:44:09.485Z
Learning: Type safety improvements for the controller in `packages/canvas/render/src/canvas-function/controller.ts` should be deferred until the data structure is finalized.

Applied to files:

  • packages/multi-person-collaboration/src/type.ts
📚 Learning: 2025-01-14T08:45:57.032Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/application-function/global-state.ts:12-25
Timestamp: 2025-01-14T08:45:57.032Z
Learning: The code in `packages/canvas/render/src/application-function/global-state.ts` is migrated from an existing codebase and should be handled with care when making modifications.

Applied to files:

  • packages/multi-person-collaboration/src/type.ts
📚 Learning: 2025-01-14T06:59:23.602Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/page-block-function/methods.ts:9-21
Timestamp: 2025-01-14T06:59:23.602Z
Learning: The code in packages/canvas/render/src/page-block-function/methods.ts is migrated code that should not be modified during the migration phase. Error handling improvements can be addressed in future PRs.

Applied to files:

  • packages/multi-person-collaboration/src/type.ts
📚 Learning: 2025-01-14T06:59:02.999Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/material-function/support-collection.ts:3-15
Timestamp: 2025-01-14T06:59:02.999Z
Learning: The code in `packages/canvas/render/src/material-function/support-collection.ts` is migrated code that should not be modified at this time to maintain stability during the migration process.

Applied to files:

  • packages/multi-person-collaboration/src/type.ts
🧬 Code graph analysis (5)
packages/multi-person-collaboration/src/type.ts (1)
packages/canvas/container/src/container.ts (2)
  • POSITION (42-50)
  • PositionType (52-52)
packages/multi-person-collaboration/src/services/schemaManager.ts (7)
packages/multi-person-collaboration/src/type.ts (2)
  • UpdateAttributesRole (116-116)
  • RootNode (36-48)
packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1)
  • NodeSchemaModel (19-110)
packages/multi-person-collaboration/src/services/providerManager.ts (1)
  • YjsProvider (4-4)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/register/src/hooks.ts (1)
  • useCanvas (79-79)
packages/multi-person-collaboration/src/config/index.ts (2)
  • ROOT_SCHEMA_MAP (5-5)
  • IGNORE_OBSERVER_ORIGIN (8-8)
packages/multi-person-collaboration/src/utils/index.ts (3)
  • fromYjs (57-73)
  • sanitizeSchema (96-120)
  • toYjs (10-54)
packages/plugins/script/src/js/method.ts (2)
packages/register/src/hooks.ts (1)
  • useRealtimeCollab (96-96)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)
  • useCollabMonaco (34-131)
packages/multi-person-collaboration/src/operation/operationHandler .ts (3)
packages/multi-person-collaboration/src/type.ts (9)
  • PageSchema (50-50)
  • NodeOperation (81-81)
  • DeleteOperation (83-85)
  • MoveOperation (87-91)
  • UpdateStyleOperation (93-97)
  • UpdatePropsOperation (99-103)
  • UpdateMethodsOperation (105-108)
  • UpdateAttributesOperation (110-114)
  • Node (23-34)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/multi-person-collaboration/src/utils/index.ts (1)
  • toYjs (10-54)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/plugins/script/src/js/method.ts (1)
  • watch (236-270)

Comment thread packages/plugins/script/src/js/method.ts Outdated

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/canvas/container/src/components/CanvasAction.vue (1)

519-556: Always update remoteStyle (don’t gate on showQuickAction).

Remote indicator needs positioning even when quick actions are hidden.

-    watchPostEffect(async () => {
+    watchPostEffect(async () => {
       const { left, top, width, height, doc } = props.selectState || props.selectState.selection
-
-      // template上虽然已经判断了showQuickAction,这里再加上主要是为了watchPostEffect能够监听它,然后刷新action
-      if (!showQuickAction.value) {
-        return
-      }
+      // We still compute remoteStyle even if quick actions are hidden.

       // nextTick后ref才能获取到元素。需要把监听的依赖放在await之前,否则无法监听变化
       await nextTick()

-      if (labelRef.value && !optionRef.value) {
+      if (labelRef.value && !optionRef.value && showQuickAction.value) {
         // 选中body的情况
         labelStyle.value = `left: 0; right: unset; top: unset; bottom: 0`
         return
       }

-      if (!labelRef.value || !optionRef.value) {
-        return
-      }
+      // Prefer labelRef/optionRef when visible; otherwise measure remoteRef for remoteStyle
+      const labelEl = showQuickAction.value ? labelRef.value : (remoteRef.value || labelRef.value)
+      const optionEl = showQuickAction.value ? optionRef.value : null
+      if (!labelEl && !optionEl) return

       const scale = useLayout().getScale()
       const canvasRect = canvasState.iframe.getBoundingClientRect()
-      const { width: labelWidth } = labelRef.value.getBoundingClientRect()
-      const { width: optionWidth } = optionRef.value.getBoundingClientRect()
+      const { width: labelWidth } = (labelEl?.getBoundingClientRect?.() || { width: 0 })
+      const { width: optionWidth } = (optionEl?.getBoundingClientRect?.() || { width: 0 })

       // canvas容器中,iframe以及iframe之外的元素clientRect的尺寸都是缩放过的,除以scale得到原始大小
       const { labelStyleValue, optionStyleValue, remoteStyleValue } = getStyleValues(
         { left, top, width, height, doc },
         { width: canvasRect.width / scale, height: canvasRect.height / scale },
         labelWidth / scale,
         optionWidth / scale
       )

-      labelStyle.value = labelStyleValue
-      fixStyle.value = optionStyleValue
+      if (showQuickAction.value) {
+        labelStyle.value = labelStyleValue
+        fixStyle.value = optionStyleValue
+      }
       remoteStyle.value = remoteStyleValue
     })
♻️ Duplicate comments (22)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)

8-13: Make WebSocket URL configurable and wss-aware; avoid hard-coded localhost.

Hard-coding ws://localhost:${PORT} breaks in non-local and HTTPS contexts (mixed content). Add an optional websocketUrl and derive a secure default from window.location.

 interface UseCollabMonacoOptions {
   currentUser: UserAwareness
   editorRef: any
   roomId: string
   fieldName: string
+  websocketUrl?: string
 }
 
 export function useCollabMonaco(options: UseCollabMonacoOptions) {
-  const { currentUser, editorRef, roomId, fieldName } = options
-  const { ydoc, awareness, provider } = useYjs(roomId, { websocketUrl: `ws://localhost:${PORT}` })
+  const { currentUser, editorRef, roomId, fieldName, websocketUrl } = options
+  const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
+  const protocol = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss' : 'ws'
+  const defaultWs = `${protocol}://${host}:${PORT}`
+  const { ydoc, awareness, provider } = useYjs(roomId, { websocketUrl: websocketUrl || defaultWs })

Also applies to: 31-33

packages/collab-ui/avatar/src/Main.vue (1)

60-63: Do not hard‑code current user; accept via props (add roomId/maxVisible).

Hard-coding identity will leak demo data and block integration. Make currentUser, roomId, and maxVisible props with safe defaults. Also drop the extra reactive(...) wrapper.

 export default {
   name: 'Avatar',
-  setup() {
+  props: {
+    currentUser: { type: Object, required: false },
+    roomId: { type: String, default: 'cursor-yjs' },
+    maxVisible: { type: Number, default: 4 }
+  },
+  setup(props) {
     const notifications = ref([])
     let notificationId = 0
-    const currentUser = {
+    const demoUser = {
       id: 'user-2',
       name: 'Bob',
       color: '#1296db',
       email: 'opentiny@tiny-engine',
       avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4'
     }
+    const currentUser = props.currentUser || demoUser
 
-    // 远程数据
-    const collabState = reactive(
-      useCollabCursor({
-        roomId: 'cursor-yjs',
-        currentUser
-      })
-    )
+    // 远程数据
+    const collabState =
+      useCollabCursor({
+        roomId: props.roomId,
+        currentUser
+      })
 
-    const maxVisible = 4
-    const visibleUsers = computed(() => remoteUsers.value.slice(0, maxVisible - 1))
+    const maxVisible = props.maxVisible
+    const visibleUsers = computed(() => remoteUsers.value.slice(0, maxVisible - 1))
     const hiddenUsersCount = computed(() => Math.max(0, remoteUsers.value.length - visibleUsers.value.length))
 
     return {
       currentUser,
       visibleUsers,
       hiddenUsersCount,
       notifications
     }
   }
 }

Also applies to: 65-71, 73-80, 88-91, 176-182

mockServer/src/app.js (1)

67-86: Fix WS path mismatch, robust doc routing, and avoid future listener leaks; gate logs to dev.

Currently the server accepts all WS paths but logs /ws, and req.url.slice(1) will collapse everyone on /ws into a single room 'ws' and breaks ?room=. Also, if the commented logger is re-enabled as-is, it will leak per-connection listeners. Normalize the doc name, pass it to setupWSConnection, pin the WS path, and add dev-only logging with cleanup.

-const wss = new WebSocket.Server({ server })
+const wss = new WebSocket.Server({ server, path: '/ws', perMessageDeflate: false })

-wss.on('connection', (conn, req) => {
-  setupWSConnection(conn, req, { gc: true })
-
-  const docName = req.url.slice(1) // 去掉开头的 '/'
-  const doc = docs.get(docName)
-  // if (doc) {
-  //   doc.on('update', (update, origin, docInstance) => {
-  //     console.log(`
-  //       ==== Yjs Server Stats (Doc Changed) ====
-  //       Time: ${new Date().toLocaleString()}
-  //       DocId: ${docName}
-  //       Update byteLength: ${update.byteLength}
-  //       Current connections: ${docInstance.conns?.size ?? 0}
-  //       ==========================
-  //     `)
-  //   })
-  // }
-})
+wss.on('connection', (conn, req) => {
+  const { pathname, searchParams } = new URL(req.url, `ws://${req.headers.host}`)
+  const docName =
+    searchParams.get('room') ||
+    pathname.replace(/^\/ws\/?/, '') ||
+    'default'
+
+  setupWSConnection(conn, req, { gc: true, docName })
+
+  // dev-only update stats; remove listener on connection close
+  if (env === 'development') {
+    const doc = docs.get(docName)
+    if (doc) {
+      const updateLogger = (update, _origin, docInstance) => {
+        console.log(
+          `\n==== Yjs Server Stats (Doc Changed) ====\n` +
+          `Time: ${new Date().toLocaleString()}\n` +
+          `DocId: ${docName}\n` +
+          `Update byteLength: ${update.byteLength}\n` +
+          `Current connections: ${docInstance.conns?.size ?? 0}\n` +
+          `==========================\n`
+        )
+      }
+      doc.on('update', updateLogger)
+      conn.on('close', () => doc.off('update', updateLogger))
+    }
+  }
+})
packages/multi-person-collaboration/src/operation/operationHandler .ts (8)

167-177: Style update method lacks proper validation

The method doesn't validate if the target node exists before accessing its properties.

   public updatedStyle(operation: UpdateStyleOperation) {
     const { strStyle, nodeId, className } = operation
     // 添加样式
     this.yMap.set('css', strStyle)
 
     // 添加 class 类名
     const targetNode = this.getYNode(nodeId)
-    targetNode?.get('props').set('className', `${className}_${nodeId}`)
+    if (targetNode) {
+      const props = targetNode.get('props')
+      if (props instanceof Y.Map) {
+        props.set('className', `${className}_${nodeId}`)
+      } else {
+        // Create props map if it doesn't exist
+        const newProps = new Y.Map()
+        newProps.set('className', `${className}_${nodeId}`)
+        targetNode.set('props', newProps)
+      }
+    } else {
+      console.warn(`[Style Update] Node with ID ${nodeId} not found.`)
+    }
 
     Object.assign(this.rootSchema, { css: strStyle })
   }

378-385: Property name inconsistency: 'schemaId' vs 'schemeId'

The event payload uses 'schemaId' but SchemaManager may expect 'schemeId'. Verify consistency across the codebase.

#!/bin/bash
# Check for both 'schemaId' and 'schemeId' usage patterns
echo "=== Checking for 'schemaId' usage ==="
rg -n "schemaId" --type ts --type js -C2

echo -e "\n=== Checking for 'schemeId' usage ==="
rg -n "schemeId" --type ts --type js -C2

1-1: File path contains a trailing space before ".ts" and will break imports

Rename the file to remove the space: operationHandler.ts. Update all imports accordingly.

#!/bin/bash
# Verify if any files reference the incorrect path with space
rg -n "operationHandler\s+\.ts" --type-add 'config:*.{json,js,ts,tsx,mjs}' --type config

30-33: Potential Y.Doc mismatch: ensure yMap comes from the same doc

Using a different Doc for transact() than the one owning yMap could cause cross-doc transaction errors.

     // 获得 yDoc 用于执行事务
     const docManager = DocManager.getInstance()
-    this.yDoc = docManager.getOrCreateDoc(docName)
+    const doc = docManager.getOrCreateDoc(docName)
+    // Verify yMap belongs to this doc, otherwise operations will fail
+    const mapDoc = (this.yMap as any).doc as Y.Doc | null
+    if (mapDoc && mapDoc !== doc) {
+      throw new Error(`yMap belongs to a different Y.Doc than '${docName}'`)
+    }
+    this.yDoc = doc

39-41: Insert into root fails when parentId is the root schema id

The code returns empty object when parentNode is missing, which breaks inserts when parentId refers to the root schema.

     if (!parentNode) {
+      // Check if parentId matches root schema ID
+      if (parentId && parentId === this.rootSchema.id) {
+        parentNode = this.yMap
+      } else {
-      return {}
+        return {}
+      }
     }

83-90: Fix 'out' insertion: handle missing index and corrupt children structure

Current code doesn't handle index=-1 and creates invalid children structure.

       case 'out':
         if (referenceNode) {
-          const childrenNode = Array.isArray(referenceNode) ? [...referenceNode] : [referenceNode]
-          yNode.get('newNode').set('children', childrenNode)
-
-          yChildren.get(index).set('_node_deleted', true)
-          yChildren.insert(index, [yNode])
+          if (index === -1) {
+            index = yChildren.length
+          }
+          const original = yChildren.get(index) as Y.Map<any>
+          if (original) {
+            const childArray = new Y.Array<Y.Map<any>>()
+            childArray.insert(0, [original])
+            yNewNode.set('children', childArray)
+            original.set('_node_deleted', true)
+          }
+          yChildren.insert(index, [yNode])
         }
         break

188-217: updatedProps copies Yjs structures incorrectly and lacks transaction

Copying Yjs objects with yNewProps.set(key, val) will fail when val is a Y.Map/Y.Array. Also updates aren't atomic.

   public updatedProps(operation: UpdatePropsOperation) {
     const { newProps, nodeId, overwrite } = operation
     let node = this.getYNode(nodeId)
 
     if (!node) {
       node = this.yMap
     }
 
-    const yNewProps = new Y.Map<any>() // 新的 props
-    const propsMap = node.get('props') as Y.Map<any> // 旧的 props
-
-    if (overwrite) {
-      // 覆盖模式
-      for (const [k, v] of Object.entries(newProps || {})) {
-        yNewProps.set(k, v)
-      }
-    } else {
-      // 先复制旧的
-      if (propsMap) {
-        propsMap.forEach((val, key) => {
-          yNewProps.set(key, val)
-        })
-      }
-
-      // 再合并新的
-      for (const [k, v] of Object.entries(newProps) || {}) {
-        yNewProps.set(k, v)
-      }
-    }
-
-    // 元数据,用于补丁操作
-    const meta = new Y.Map<any>()
-    meta.set('nodeId', nodeId)
-    meta.set('overwrite', overwrite)
-
-    yNewProps.set('meta', meta)
-    node.set('props', yNewProps)
+    this.yDoc.transact(() => {
+      const propsMap = node.get('props') as Y.Map<any> | undefined
+      const baseJson = overwrite ? {} : (propsMap?.toJSON?.() ?? {})
+      const merged = { ...baseJson, ...(newProps || {}) }
+
+      const meta = new Y.Map<any>()
+      meta.set('nodeId', nodeId)
+      meta.set('overwrite', overwrite)
+
+      const yMerged = new Y.Map<any>()
+      toYjs(yMerged, merged)
+      yMerged.set('meta', meta)
+      node.set('props', yMerged)
+    })
   }

220-249: updatedMethods: create props map if absent and wrap in a transaction

The method assumes props map exists, which could cause NPE, and updates aren't atomic.

   public updatedMethods(operation: UpdateMethodsOperation) {
+    this.yDoc.transact(() => {
       if (operation.type === 'root') {
         const methods = operation.methods
         // 根节点直接设置 methods 不需要 id
         this.yMap.set('methods', methods)
       } else if (operation.type === 'node') {
         const { nodeId, methodsName, methods } = operation
         const node = this.getYNode(nodeId)
         if (node) {
-          const nodeProps = node.get('props')
+          let nodeProps = node.get('props') as Y.Map<any> | undefined
+          if (!(nodeProps instanceof Y.Map)) {
+            nodeProps = new Y.Map()
+            node.set('props', nodeProps)
+          }
           nodeProps.set(methodsName, {
             ...methods,
             meta: { nodeId }
           })
         }
       } else if (operation.type === 'delete-method') {
         const { nodeId, methodsName } = operation
         const node = this.getYNode(nodeId)
         if (node) {
           const nodeProps = node.get('props')
-          // 依旧软删除
-          nodeProps.set(methodsName, {
-            _methods_deleted: true,
-            meta: { nodeId }
-          })
-          // 软删除后直接硬删除删除,保证 yMap 数据干净
-          nodeProps.delete(methodsName)
+          if (nodeProps instanceof Y.Map) {
+            // 依旧软删除
+            nodeProps.set(methodsName, {
+              _methods_deleted: true,
+              meta: { nodeId }
+            })
+            // 软删除后直接硬删除删除,保证 yMap 数据干净
+            nodeProps.delete(methodsName)
+          }
         }
       }
+    })
   }
packages/multi-person-collaboration/src/utils/index.ts (3)

3-3: Data corruption: string sentinel collides with real data

Using a plain string sentinel will wrongly decode any genuine "undefined" string into undefined.

-const UNDEFINED_PLACEHOLDER = '__undefined__'
+// Use a structured token that survives encode/decode and can be detected by shape
+const UNDEFINED_PLACEHOLDER = Object.freeze({ __tiny_yjs_undefined__: true }) as const

Also add a helper:

// place near the top of the module
function isUndefinedToken(v: any): v is typeof UNDEFINED_PLACEHOLDER {
  return !!v && typeof v === 'object' && (v as any).__tiny_yjs_undefined__ === true
}

68-71: Decode undefined by shape, not identity or string

Make decoding robust across serialization by checking the structured token.

   } else if (value instanceof Y.Text) {
     return value.toString()
-  } else if (value === UNDEFINED_PLACEHOLDER) {
-    return undefined // 还原 undefined
+  } else if (isUndefinedToken(value)) {
+    return undefined // 还原 undefined
   } else {

81-86: Fix: getValueByPath always returns undefined

The null-check is inverted; it bails when acc is truthy. This breaks all lookups.

 export const getValueByPath = (obj: any, path: (string | number)[]): any => {
   return path.reduce((acc, key) => {
-    if (acc) return undefined // 避免继续取值报错
+    if (acc == null) return undefined // null/undefined: stop safely
     return acc[key]
   }, obj)
 }
packages/canvas/container/src/CanvasContainer.vue (6)

147-152: Don’t hard‑code currentUser in production.

Externalize via config/props or a user service; avoid committing PII/test avatars.


201-217: Throttle remote reflow on scroll via rAF.

Recomputing rects on every scroll event is hot.

-    const syncRemoteNode = () => {
-      syncRemoteStatesSelections.value = syncRemoteStatesSelections.value
+    let syncRaf = 0
+    const syncRemoteNode = () => {
+      cancelAnimationFrame(syncRaf)
+      syncRaf = requestAnimationFrame(() => {
+        syncRemoteStatesSelections.value = syncRemoteStatesSelections.value
           .map((selState) => {
             const element = querySelectById(selState.selection.id)
             if (!element) return null
             const { top, left, width, height } = getRect(element)
             return {
               ...selState,
               top,
               left,
               width,
               height
             }
           })
           .filter(Boolean)
-    }
+      })
+    }

Optionally cancel syncRaf in onUnmounted.


3-3: Fix unstable v-for key for remote selections.

Remote items may not have state.id; use a stable fallback.

-  <div v-for="state in allMultiSelectedStates" :key="state.id">
+  <div
+    v-for="state in allMultiSelectedStates"
+    :key="state.id || (state.selection && state.selection.id) || (state.user && `remote-${state.user.id}`)"
+  >

168-184: Null‑guard selection.id in mapper.

Prevents querySelectById(undefined) and downstream crashes.

-    const mapStateToSelection = (state, selection) => {
-      const element = querySelectById(selection.id)
+    const mapStateToSelection = (state, selection) => {
+      const id = selection && selection.id
+      if (!id) return null
+      const element = querySelectById(id)
       if (!element) return null

351-354: Align with cursor API; pass the event.

Typo in updateCursorPositioin and missing mousedown event param.

-        const { updateCursorPositioin, mouseUpHandler, mouseDownHandler } = useCollabCursor({
+        const { updateCursorPosition, mouseUpHandler, mouseDownHandler } = useCollabCursor({
           roomId: 'cursor-yjs',
           currentUser
         })
@@
-        win.addEventListener('mousedown', (event) => {
-          mouseDownHandler()
+        win.addEventListener('mousedown', (event) => {
+          mouseDownHandler(event)
@@
-            updateCursorPositioin(ev)
+            updateCursorPosition(ev)
@@
-            mouseUpHandler()
+            mouseUpHandler(ev)

Also applies to: 358-358, 391-391, 419-419


488-516: Duplicate identifier ‘remoteStates’ shadows ref and will crash.

You declare remoteStates (ref) and then destructure a property with the same name.

-        const {
+        const {
           insertSharedNode,
           deleteSharedNode,
           updateUserSelection,
           moveDownSharedNode,
           moveUpSharedNode,
           updateStyleNode,
           updatePropsNode,
           updateMethodNode,
-          updateAttributesNode,
-          remoteStates
+          updateAttributesNode,
+          remoteStates: awarenessRemoteStates
         } = useCollabSchema({
           roomId: 'schema-yjs',
           currentUser
         })
@@
         initHook(HOOK_NAME.useRealtimeCollab, {
           insertSharedNode,
           deleteSharedNode,
           updateUserSelection,
           moveDownSharedNode,
           moveUpSharedNode,
           updateStyleNode,
           updatePropsNode,
           updateMethodNode,
-          updateAttributesNode,
-          remoteStates
+          updateAttributesNode,
+          remoteStates: awarenessRemoteStates
         })

Update downstream references if any used the destructured name.

packages/canvas/container/src/components/CanvasAction.vue (2)

29-37: Guard remote user name and color.

Prevent runtime errors for missing user; consider i18n text.

-    <div
+    <div
       v-if="haveRemoteState"
       ref="remoteRef"
       class="corner-mark-left"
-      :style="[remoteStyle, { backgroundColor: selectState.user.color }]"
+      :style="[remoteStyle, { backgroundColor: selectState.user?.color || '#409EFF' }]"
     >
-      <span> {{ selectState.user.name }} 正在编辑 </span>
+      <span> {{ (selectState.user && selectState.user.name) || '其他用户' }} 正在编辑 </span>
     </div>

235-245: Guard collab calls and current node presence.

Race conditions can invoke handlers before collab init or without parent/schema.

-    const moveUp = () => {
+    const moveUp = () => {
       const { parent, schema } = getCurrent()
       moveChild(parent?.children, schema, -1)
-      useRealtimeCollab().moveUpSharedNode(parent.id, schema.id, 'up')
+      const collab = typeof useRealtimeCollab === 'function' ? useRealtimeCollab() : null
+      if (collab?.moveUpSharedNode && parent?.id && schema?.id) {
+        collab.moveUpSharedNode(parent.id, schema.id, 'up')
+      }
     }
@@
-    const moveDown = () => {
+    const moveDown = () => {
       const { parent, schema } = getCurrent()
       moveChild(parent?.children, schema, 1)
-      useRealtimeCollab().moveDownSharedNode(parent.id, schema.id, 'down')
+      const collab = typeof useRealtimeCollab === 'function' ? useRealtimeCollab() : null
+      if (collab?.moveDownSharedNode && parent?.id && schema?.id) {
+        collab.moveDownSharedNode(parent.id, schema.id, 'down')
+      }
     }
🧹 Nitpick comments (15)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)

90-100: Don’t patch MonacoBinding private fields.

Overriding _onDidChangeModelContent is brittle and can break with library updates; it’s unnecessary given the init sequence. Remove this block.

-        // 拦截编辑器事件,防止初始化回写循环
-        const originalListener = (monacoBinding.value as any)['_onDidChangeModelContent'] as (
-          e: any
-        ) => void | undefined
-        if (originalListener) {
-          ;(monacoBinding.value as any)['_onDidChangeModelContent'] = (event: any) => {
-            if (isApplyingRemote) return
-            originalListener(event)
-          }
-        }

9-12: Tighten typing for editorRef.

Prefer a precise type for the Monaco editor to avoid any and catch API misuses early.

Example:

  • Define type Editor = import('monaco-editor').editor.IStandaloneCodeEditor
  • Type editorRef as Ref<Editor | { getEditor: () => Editor }>

Also applies to: 31-31


46-47: Gate console logs behind a debug flag.

Prevent noisy logs in production; keep them under a debug/DEV guard.

Also applies to: 50-51, 65-65, 75-75, 103-103

packages/collab-ui/avatar/src/Main.vue (3)

47-52: Make notifications screen‑reader friendly (aria‑live).

Ensure join/leave messages are announced.

-    <transition-group name="notifications" tag="div" class="notification-area">
+    <transition-group
+      name="notifications"
+      tag="div"
+      class="notification-area"
+      role="status"
+      aria-live="polite"
+      aria-atomic="true">

81-87: Optional: de‑duplicate multi‑session presence by user.id.

If one user opens multiple tabs, avatars duplicate. Collapse by user id.

-    const remoteUsers = computed(() => {
-      return Object.values(collabState.remoteCursors)
-        .filter((state) => state.user)
-        .map((state) => state.user)
-    })
+    const remoteUsers = computed(() => {
+      const seen = new Set()
+      return Object.values(collabState.remoteCursors)
+        .filter((s) => s.user)
+        .map((s) => s.user)
+        .filter((u) => (seen.has(u.id) ? false : (seen.add(u.id), true)))
+    })

187-196: Z‑index is excessively high; gate with a CSS var.

Prevents unintended overlay over modals/tooltips elsewhere.

 .presence-container {
   position: fixed;
   bottom: 10px;
   right: 120px;
-  z-index: 100000;
+  z-index: var(--te-presence-z, 10000);
mockServer/src/app.js (2)

89-91: Align startup log with actual routing.

Clarify base path and room patterns to avoid client confusion.

-server.listen(port, () => {
-  console.log(`HTTP+Yjs server listening at http://localhost:${port} , WebSocket: ws://localhost:${port}/ws`)
-})
+server.listen(port, () => {
+  console.log(
+    `HTTP+Yjs server listening at http://localhost:${port} , ` +
+    `WebSocket base: ws://localhost:${port}/ws (room via /ws/:doc or ?room=doc)`
+  )
+})

17-19: Optional: use WebSocketServer named export (clearer than static .Server).

Pure readability; no behavior change.

-import WebSocket from 'ws'
+import { WebSocketServer } from 'ws'

And adjust creation (if you adopt this style):

-const wss = new WebSocket.Server({ server, path: '/ws', perMessageDeflate: false })
+const wss = new WebSocketServer({ server, path: '/ws', perMessageDeflate: false })
packages/multi-person-collaboration/src/utils/index.ts (1)

103-104: Optimize array filtering with single pass

The current code maps then filters in two passes. Use flatMap or reduce for better performance.

-    return schema.map((item) => sanitizeSchema(item, keysToFilter)).filter((item) => item !== undefined)
+    return schema.reduce((acc: any[], item) => {
+      const sanitized = sanitizeSchema(item, keysToFilter)
+      if (sanitized !== undefined) acc.push(sanitized)
+      return acc
+    }, [])
packages/multi-person-collaboration/src/operation/operationHandler .ts (1)

256-280: Wrap attribute updates in transaction for consistency

Non-clean operations should also be wrapped in transactions for atomicity.

   public updatedAttributes(opertion: UpdateAttributesOperation) {
     const { type, nodeId } = opertion
     const targetNode = this.getYNode(nodeId)
+    if (!targetNode) {
+      console.warn(`[Attributes Update] Node with ID ${nodeId} not found.`)
+      return
+    }
 
-    switch (type) {
-      case 'condition': {
-        targetNode?.set('condition', opertion.value)
-        break
-      }
-      case 'loop': {
-        targetNode?.set('loop', opertion.value)
-        break
-      }
-      case 'loopArgs': {
-        targetNode?.set('loopArgs', opertion.value)
-        break
-      }
-      case 'clean': {
-        this.yDoc.transact(() => {
-          // 合并为一次操作
-          targetNode?.delete('loop')
-          targetNode?.delete('loopArgs')
-        })
-        break
-      }
-      default:
-        break
-    }
+    this.yDoc.transact(() => {
+      switch (type) {
+        case 'condition':
+          targetNode.set('condition', opertion.value)
+          break
+        case 'loop':
+          targetNode.set('loop', opertion.value)
+          break
+        case 'loopArgs':
+          targetNode.set('loopArgs', opertion.value)
+          break
+        case 'clean':
+          targetNode.delete('loop')
+          targetNode.delete('loopArgs')
+          break
+        default:
+          break
+      }
+    })
   }
packages/canvas/container/src/composables/useMultiSelect.ts (2)

2-2: Init collab once and guard availability.

Avoid repeated hook calls and races before init; cache the instance and null‑guard calls.

 export const useMultiSelect = () => {
+  const collab = typeof useRealtimeCollab === 'function' ? useRealtimeCollab() : null

119-121: Normalize payload shape and guard collab.

Keep a consistent API shape for consumers (always an array) and avoid throwing before hook init.

-    // 多人协作
-    useRealtimeCollab().updateUserSelection(selectState)
+    // 多人协作
+    collab?.updateUserSelection([selectState])
packages/canvas/container/src/CanvasContainer.vue (2)

2-2: Guard cursor component presence.

Avoid runtime errors if collab UI isn’t registered.

-  <component :is="cursorComponent.entry"></component>
+  <component v-if="cursorComponent && cursorComponent.entry" :is="cursorComponent.entry"></component>

485-487: Remove scroll listener on unmount.

Prevent leaks and duplicate handlers on remount.

 onUnmounted(() => {
   if (iframe.value?.contentDocument) {
     removeHotkeyEvent(iframe.value.contentDocument)
   }
   window.removeEventListener('message', updateI18n, false)
+  const win = iframe.value?.contentWindow
+  if (win) {
+    win.removeEventListener('scroll', syncNodeScroll, true)
+    win.removeEventListener('scroll', syncRemoteNode, true)
+  }
 })
packages/canvas/container/src/components/CanvasAction.vue (1)

9-11: Null‑safe border color.

Avoid accessing selectState.user.color when user is absent.

-      width: selectState.width + 'px',
-      ...(haveRemoteState ? { border: `2px solid ${selectState.user.color}` } : {})
+      width: selectState.width + 'px',
+      ...(haveRemoteState ? { border: `2px solid ${selectState.user?.color || 'var(--te-canvas-container-bg-color-checked)'}` } : {})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6aa92e and 02a8063.

📒 Files selected for processing (11)
  • mockServer/src/app.js (2 hunks)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (11 hunks)
  • packages/canvas/container/src/composables/useMultiSelect.ts (2 hunks)
  • packages/collab-ui/avatar/src/Main.vue (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1 hunks)
  • packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1 hunks)
  • packages/multi-person-collaboration/src/operation/operationHandler .ts (1 hunks)
  • packages/multi-person-collaboration/src/services/schemaManager.ts (1 hunks)
  • packages/multi-person-collaboration/src/utils/index.ts (1 hunks)
  • packages/plugins/script/src/js/method.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/multi-person-collaboration/src/models/NodeSchemaModel.ts
  • packages/multi-person-collaboration/src/services/schemaManager.ts
  • packages/plugins/script/src/js/method.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/canvas/container/src/CanvasContainer.vue
📚 Learning: 2025-01-15T02:19:06.755Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#940
File: packages/canvas/DesignCanvas/src/DesignCanvas.vue:0-0
Timestamp: 2025-01-15T02:19:06.755Z
Learning: In Vue components using message subscriptions from opentiny/tiny-engine-meta-register, always clean up subscriptions in the onUnmounted hook using useMessage().unsubscribe() to prevent memory leaks.

Applied to files:

  • packages/canvas/container/src/CanvasContainer.vue
📚 Learning: 2024-10-10T02:48:10.881Z
Learnt from: yy-wow
PR: opentiny/tiny-engine#850
File: packages/toolbars/preview/src/Main.vue:0-0
Timestamp: 2024-10-10T02:48:10.881Z
Learning: 在 `packages/toolbars/preview/src/Main.vue` 文件中,使用 `useNotify` 而不是 `console` 来记录错误日志。

Applied to files:

  • packages/canvas/container/src/components/CanvasAction.vue
🧬 Code graph analysis (4)
packages/canvas/container/src/composables/useMultiSelect.ts (1)
packages/register/src/hooks.ts (1)
  • useRealtimeCollab (96-96)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/plugins/script/src/js/method.ts (1)
  • watch (236-270)
packages/multi-person-collaboration/src/operation/operationHandler .ts (3)
packages/multi-person-collaboration/src/type.ts (9)
  • PageSchema (50-50)
  • NodeOperation (81-81)
  • DeleteOperation (83-85)
  • MoveOperation (87-91)
  • UpdateStyleOperation (93-97)
  • UpdatePropsOperation (99-103)
  • UpdateMethodsOperation (105-108)
  • UpdateAttributesOperation (110-114)
  • Node (23-34)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/multi-person-collaboration/src/utils/index.ts (1)
  • toYjs (10-54)
packages/multi-person-collaboration/src/utils/index.ts (1)
packages/vue-generator/src/templates/vue-template/index.js (1)
  • value (30-30)
🔇 Additional comments (7)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)

60-67: Fix Y.Text emptiness check (yText.length is not reliable).

Y.Text doesn’t expose a stable length property; use yText.toString().length. Current logic can skip initialization and wrongly override local content.

-        if (yText.length === 0) {
+        if (yText.toString().length === 0) {

Likely an incorrect or invalid review comment.

packages/collab-ui/avatar/src/Main.vue (1)

1-54: Template structure looks solid.

Avatar stack, tooltips, and +N indicator are clear and composable.

mockServer/src/app.js (2)

63-66: LGTM on HTTP server wiring.

Attaching WS to the shared HTTP server is correct.


69-71: Confirm client URLs use the new WS contract (/ws + room)

  • Server: mockServer/src/app.js (lines 69–71) registers the WS handler — ensure clients connect to /ws/:doc or /ws?room=, not the old implicit endpoints.
  • Automated repo search in the sandbox failed (ripgrep skipped files); manually search client code for new WebSocket, ws://, wss://, /ws and ?room= and update any hardcoded URLs.
packages/canvas/container/src/composables/useMultiSelect.ts (1)

7-18: Exported SelectionState looks good.

packages/canvas/container/src/components/CanvasAction.vue (1)

358-359: Expose refs used in watchPostEffect.

Ensure remoteRef is in setup return (it is); keep as-is. LGTM.

packages/canvas/container/src/CanvasContainer.vue (1)

573-578: remoteStates is reactive — no change required.

useRealtimeCollab().remoteStates comes from useAwareness (packages/multi-person-collaboration/src/composables/useAwareness.ts) where remoteStates is created with reactive<Record<number, TState>>({}), so computed(() => Object.values(remoteStates.value)) will update.

Comment thread packages/canvas/container/src/components/CanvasAction.vue
Comment thread packages/canvas/container/src/composables/useMultiSelect.ts
Comment thread packages/collab-ui/avatar/src/Main.vue
Comment thread packages/collab-ui/avatar/src/Main.vue
Comment thread packages/multi-person-collaboration/src/composables/useCollabMonaco.ts Outdated
Comment thread packages/multi-person-collaboration/src/composables/useCollabMonaco.ts Outdated

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (5)
packages/multi-person-collaboration/src/services/schemaManager.ts (5)

197-197: Typo in property name: Use schemaId instead of schemeId.

The code references payload.schemeId but should use payload.schemaId to maintain consistency with the type definition and the rest of the codebase.

-              schemaId: payload.schemeId,
+              schemaId: payload.schemaId,

103-133: Potential memory leak: Provider sync handler not cleaned up properly.

The handleInitialSync function removes itself from the provider's sync event using provider.off('sync', handleInitialSync), but this handler is never removed if the provider disconnects abnormally or if destroySchema is called before initial sync completes. This can cause memory leaks.

Store the handler reference for cleanup in destroySchema:

 export class SchemaManager {
   private static instance: SchemaManager
   private schemaMap: Map<string, Y.Map<any>> = new Map()
   private nodeSchemaModelMap: Map<string, NodeSchemaModel> = new Map()
   private observerCallbacks: Map<
     string,
     { yRoot: Y.Map<any>; cb: (events: Y.YEvent<any>[], transaction: Y.Transaction) => void }
   > = new Map()
   private initialSyncDone: Map<string, boolean> = new Map()
   private eventListeners = new Map()
+  private syncHandlers = new Map<string, { provider: YjsProvider; handler: (isSynced: boolean) => void }>()

Then in the provider sync setup:

       if (provider) {
         const handleInitialSync = (isSynced: boolean) => {
           if (!isSynced) return
 
           // Ensure this handler only executes once
           provider.off('sync', handleInitialSync)
+          this.syncHandlers.delete(docName)
 
           if (this.initialSyncDone.get(docName)) return
 
           if (yMap!.size === 0) {
             // Remote empty → use local schema to initialize
             console.log(`[${docName}] Remote empty. Using local schema to initialize yMap.`)
             ydoc.transact(() => {
               toYjs(yMap!, pageSchema)
             }, IGNORE_OBSERVER_ORIGIN)
           } else {
             // Remote has data → override UI with remote
             console.log(`[${docName}] Remote has data. Importing remote schema to UI.`)
             const rawRemoteSchema = fromYjs(yMap!)
             const INTERNAL_YJS_KEYS = ['meta', '_methods_deleted', '_node_deleted', 'newNode']
             const cleanSchema = sanitizeSchema(rawRemoteSchema, INTERNAL_YJS_KEYS)
             useCanvas().importSchema(cleanSchema)
           }
 
           // Mark initial sync complete
           this.initialSyncDone.set(docName, true)
         }
 
         provider.on('sync', handleInitialSync)
+        this.syncHandlers.set(docName, { provider, handler: handleInitialSync })
       }

And in destroySchema:

   public destroySchema(docName: string): void {
     this.destroyObserver(docName)
+    
+    // Clean up sync handler if exists
+    const syncHandler = this.syncHandlers.get(docName)
+    if (syncHandler) {
+      syncHandler.provider.off('sync', syncHandler.handler)
+      this.syncHandlers.delete(docName)
+    }
+    
+    // Clean up event listeners
+    if (this.eventListeners.has(docName)) {
+      const { map, cb } = this.eventListeners.get(docName)
+      map.unobserve(cb)
+      this.eventListeners.delete(docName)
+    }
+    
     this.schemaMap.delete(docName)
     this.nodeSchemaModelMap.delete(docName)
+    this.initialSyncDone.delete(docName)
   }

100-133: Missing initialization when provider is absent.

When no provider is present (offline mode), initialSyncDone remains false forever, causing all local changes to be ignored by the observer. The system needs to handle offline-first scenarios.

Add proper handling for offline mode:

       // Mark initial sync as not complete
       this.initialSyncDone.set(docName, false)
 
       if (provider) {
         const handleInitialSync = (isSynced: boolean) => {
           // ... existing handler code ...
         }
 
         provider.on('sync', handleInitialSync)
+      } else {
+        // No provider means we're in offline mode - allow local changes immediately
+        console.log(`[${docName}] No provider. Operating in offline mode.`)
+        this.initialSyncDone.set(docName, true)
       }

158-162: Incomplete cleanup in destroySchema method.

The method doesn't clean up event listeners set up in setupEventListeners, potentially causing memory leaks and stale event handling.

Add proper event listener cleanup:

   public destroySchema(docName: string): void {
     this.destroyObserver(docName)
+    
+    // Clean up event listeners
+    if (this.eventListeners.has(docName)) {
+      const { map, cb } = this.eventListeners.get(docName)
+      map.unobserve(cb)
+      this.eventListeners.delete(docName)
+    }
+    
     this.schemaMap.delete(docName)
     this.nodeSchemaModelMap.delete(docName)
   }

479-484: Incomplete boundary validation in array swap operation.

The condition only validates patch.targetIndex > -1 and patch.swapIndex < childrenArray.length, but doesn't ensure both indices are within valid bounds, potentially causing out-of-bounds access.

-          if (patch.targetIndex > -1 && patch.swapIndex < childrenArray.length) {
+          if (patch.targetIndex >= 0 && patch.targetIndex < childrenArray.length && 
+              patch.swapIndex >= 0 && patch.swapIndex < childrenArray.length) {
🧹 Nitpick comments (3)
packages/multi-person-collaboration/src/services/schemaManager.ts (3)

59-69: Redundant initialization in constructor.

The Maps are already initialized with new Map() in the field declarations (lines 50-57), so re-initializing them in the constructor is unnecessary.

Remove redundant initialization:

   private constructor() {
     // Private constructor to ensure singleton pattern
-    // Initialize internal Maps
-    this.schemaMap = new Map<string, Y.Map<any>>()
-    this.nodeSchemaModelMap = new Map<string, NodeSchemaModel>()
-    this.observerCallbacks = new Map<
-      string,
-      { yRoot: Y.Map<any>; cb: (events: Y.YEvent<any>[], transaction: Y.Transaction) => void }
-    >()
-    this.initialSyncDone = new Map<string, boolean>()
   }

426-428: Remove or guard debug console.log statement.

Debug logging should be removed or wrapped in a development-only check to avoid cluttering production logs.

               if (insertedYjsItems.length > 0) {
-                // eslint-disable-next-line no-console
-                console.log('===insert yjsItem[0].toJSON() (from target)===', insertedYjsItems[0].toJSON())
+                if (process.env.NODE_ENV === 'development') {
+                  // eslint-disable-next-line no-console
+                  console.log('===insert yjsItem[0].toJSON() (from target)===', insertedYjsItems[0].toJSON())
+                }
               }

495-498: Remove commented-out code.

The commented-out code for updateRect() should be removed if it's no longer needed, or uncommented if it's required for functionality.

           useMessage().publish({ topic: 'schemaChange', data: {} })
-
-          // const { updateRect } = useCanvas().canvasApi.value
-          // updateRect()
           break
         }
         case 'style-update': {
           const strStyle = patch.css
 
           useCanvas().updateSchema({ css: strStyle })
-
-          // const { updateRect } = useCanvas().canvasApi.value
-          // updateRect()
           break
         }

Also applies to: 505-507

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02a8063 and e9e0fd0.

📒 Files selected for processing (1)
  • packages/multi-person-collaboration/src/services/schemaManager.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/multi-person-collaboration/src/services/schemaManager.ts (7)
packages/multi-person-collaboration/src/type.ts (2)
  • UpdateAttributesRole (116-116)
  • RootNode (36-48)
packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1)
  • NodeSchemaModel (19-110)
packages/multi-person-collaboration/src/services/providerManager.ts (1)
  • YjsProvider (4-4)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/register/src/hooks.ts (1)
  • useCanvas (79-79)
packages/multi-person-collaboration/src/config/index.ts (2)
  • ROOT_SCHEMA_MAP (5-5)
  • IGNORE_OBSERVER_ORIGIN (8-8)
packages/multi-person-collaboration/src/utils/index.ts (3)
  • toYjs (10-54)
  • fromYjs (57-73)
  • sanitizeSchema (97-130)
🔇 Additional comments (1)
packages/multi-person-collaboration/src/services/schemaManager.ts (1)

11-44: Well-structured discriminated union type for patches.

The DiffPatch type definition uses discriminated unions effectively, making it easy to handle different patch types safely in the switch statement. This is a good TypeScript practice that improves type safety and maintainability.

Comment thread packages/multi-person-collaboration/src/services/schemaManager.ts
Comment thread packages/multi-person-collaboration/src/services/schemaManager.ts

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/plugins/versioncontrol/README.md (1)

82-88: Add “Breaking changes & migration” section.

PR is labeled breaking-change. Document API/behavior changes, migration steps, and fallback/feature flags.

packages/multi-person-collaboration/src/composables/useCollabCursor.ts (1)

1-74: Rename updateCursorPositioin → updateCursorPosition — update API, callers and docs

Typo in the exported API will break callers; rename the function and update every usage.

  • packages/multi-person-collaboration/src/composables/useCollabCursor.ts — rename function (def at line ~34) and return/export key (line ~69).
  • packages/canvas/container/src/CanvasContainer.vue — update destructure and call sites (lines ~351, ~391).
  • packages/multi-person-collaboration/README.md — update example usage and event binding (lines ~73, ~77).
  • Also found hard-coded websocket URL in packages/multi-person-collaboration/README.md (ws://localhost:1234 at line ~53) — confirm/update if intended.
🧹 Nitpick comments (22)
packages/plugins/versioncontrol/README.md (10)

41-48: Import the merge strategy instead of using a magic string later.

Add a typed enum/import for merge strategy to avoid brittle string literals.

 import { BranchAppServiceImpl } from './app/services/BranchAppService'
 import { CommitAppServiceImpl } from './app/services/CommitAppService'
+import { MergeStrategy } from './domain/merge'

63-65: Avoid magic string and verify result field name.

  • Prefer MergeStrategy.ThreeWay (or your enum) instead of 'three-way'.
  • Property looks like conflictReports (not conflictedReports?) — please confirm.
-const mergeResult = await branchAppService.mergeBranch('feature/login', 'develop', 'three-way', '将登录功能合并到 develop 分支')
-console.log(mergeResult.newCommitId, mergeResult.conflictedReports)
+const mergeResult = await branchAppService.mergeBranch('feature/login', 'develop', MergeStrategy.ThreeWay, '将登录功能合并到 develop 分支')
+console.log(mergeResult.newCommitId, mergeResult.conflictReports)

66-74: Prefer an options object for createCommit to reduce arg‑order mistakes.

If the API allows, switch to named params; safer and clearer.

-const commit = await commitAppService.createCommit(
-  newBranch.id,
-  '添加登录页面',
-  { id: 'user-1', username: 'Alice' },
-  { pages: [{ id: 'p1', content: '...' }] },
-  'feature'
-)
+const commit = await commitAppService.createCommit({
+  branchId: newBranch.id,
+  message: '添加登录页面',
+  author: { id: 'user-1', username: 'Alice' },
+  payload: { pages: [{ id: 'p1', content: '...' }] },
+  type: 'feature',
+})

75-76: Disambiguate diff direction with named params.

Readers won’t know whether it’s from, to or left, right.

-const diff = await commitAppService.getCommitDiff(commit.id, 'previous-commit-id')
+const diff = await commitAppService.getCommitDiff({ from: 'previous-commit-id', to: commit.id })

40-80: Top‑level await in example.

As written, this relies on top‑level await support. Consider wrapping in an async IIFE for portability.

-// 分支操作示例
-const newBranch = await branchAppService.createBranch(
+// 分支操作示例
+;(async () => {
+const newBranch = await branchAppService.createBranch(
   'feature/login',
   'upstream-branch-id',
   { id: 'user-1', username: 'Alice' },
   'commit-id-123',
   '登录功能分支'
 )
 
 const mergeResult = await branchAppService.mergeBranch('feature/login', 'develop', MergeStrategy.ThreeWay, '将登录功能合并到 develop 分支')
 console.log(mergeResult.newCommitId, mergeResult.conflictReports)
 
 // 提交操作示例
-const commit = await commitAppService.createCommit({
+const commit = await commitAppService.createCommit({
   branchId: newBranch.id,
   message: '添加登录页面',
   author: { id: 'user-1', username: 'Alice' },
   payload: { pages: [{ id: 'p1', content: '...' }] },
   type: 'feature',
 })
 
-const diff = await commitAppService.getCommitDiff({ from: 'previous-commit-id', to: commit.id })
+const diff = await commitAppService.getCommitDiff({ from: 'previous-commit-id', to: commit.id })
 console.log(diff)
 
 const snapshot = await commitAppService.generateSnapshot(commit.id)
 console.log(snapshot)
+})().catch(console.error)

12-23: Destructive ops need permission/guardrails note.

For delete/force‑delete/rename/restore/archive, add a brief note on required permissions, confirmations, and audit logging to prevent accidental data loss.


15-15: Flag WIP capability.

“更新分支 (UI 尚未实现)” — mark as Beta/behind feature flag in README to set expectations.


84-86: Specify the actual memoization mechanism.

@Memoize is ambiguous. Name the package/decorator (e.g., core-decorators, lodash-decorators, or custom) and note cache invalidation policy/TTL to avoid stale reads.


38-80: Add basic error handling in examples.

Show try/catch (or .catch) around merge/diff/snapshot to model expected error surfaces (conflicts, validation errors).


1-5: Add a short “Scope & prerequisites” block.

List prerequisites (e.g., storage backend, Yjs provider, auth requirements) and scope boundaries to help integrators wire the services correctly.

packages/multi-person-collaboration/src/utils/index.ts (3)

24-27: Only treat plain objects as maps; avoid Date/Map/Set being mis-serialized

Limit the object branch to plain records; others should be stored as-is or stringified upstream.

+function isPlainObject(v: unknown): v is Record<string, any> {
+  return Object.prototype.toString.call(v) === '[object Object]'
+}
@@
-      } else if (typeof item === 'object') {
+      } else if (isPlainObject(item)) {
         const childMap = new Y.Map()
         toYjs(childMap, item)
         target.push([childMap])
@@
-      } else if (typeof val === 'object') {
+      } else if (isPlainObject(val)) {
         const yMap = new Y.Map()
         target.set(key, yMap) // 先 set 到父节点
         toYjs(yMap, val)

Please confirm that non-plain objects don’t appear in inputs; if they do, decide on a canonical encoding.

Also applies to: 45-49


97-136: Avoid O(n²) key lookups in sanitizeSchema; precompute a Set once

Using includes() in every recursion is costly; capture a Set once and reuse.

-export function sanitizeSchema(schema: any, keysToFilter?: string[]): any {
-  if (typeof schema !== 'object' || schema === null) {
-    return schema
-  }
-  // 如果对象被标记为软删除,则直接将整个对象过滤掉
-  // 这是最优先的检查,因为如果节点被删除,就无需再处理它的子节点或属性
-  if (schema._node_deleted === true) {
-    return undefined // 返回 undefined,让上层调用者 (Array.filter) 将其移除
-  }
-  if (Array.isArray(schema)) {
-    return schema.map((item) => sanitizeSchema(item, keysToFilter)).filter((item) => item !== undefined)
-  }
-  const sanitizedObject: { [key: string]: any } = {}
-  const originalKeys = Object.keys(schema) // 保留原对象的键顺序
-  for (const key of originalKeys) {
-    if (keysToFilter?.includes(key)) continue
-    const child = sanitizeSchema(schema[key], keysToFilter)
-    if (child !== undefined) {
-      sanitizedObject[key] = child
-    }
-  }
-  if (
-    Object.keys(schema).length === 1 && // 原始对象只有一个键
-    'id' in schema && // 这个键是 id
-    !('id' in sanitizedObject) // 过滤后 id 不在了
-  ) {
-    return undefined
-  }
-  // 如果过滤后对象是空的,也直接返回 undefined(保证不会出现 {})
-  if (Object.keys(sanitizedObject).length === 0) {
-    return undefined
-  }
-  return sanitizedObject
-}
+export function sanitizeSchema(schema: any, keysToFilter?: string[]): any {
+  const filterSet = keysToFilter ? new Set(keysToFilter) : undefined
+  const _sanitize = (node: any): any => {
+    if (typeof node !== 'object' || node === null) return node
+    if (node._node_deleted === true) return undefined
+    if (Array.isArray(node)) {
+      return node.map((item) => _sanitize(item)).filter((item) => item !== undefined)
+    }
+    const sanitizedObject: Record<string, any> = {}
+    const originalKeys = Object.keys(node) // 保留原对象的键顺序
+    for (const key of originalKeys) {
+      if (filterSet?.has(key)) continue
+      const child = _sanitize(node[key])
+      if (child !== undefined) sanitizedObject[key] = child
+    }
+    if (Object.keys(node).length === 1 && 'id' in node && !('id' in sanitizedObject)) {
+      return undefined
+    }
+    if (Object.keys(sanitizedObject).length === 0) return undefined
+    return sanitizedObject
+  }
+  return _sanitize(schema)
+}

10-54: Optional: guard toYjs against cycles/shared references

Circular structures or shared references can loop or duplicate. Add a WeakSet to detect and fail fast.

-export function toYjs(target: Y.Map<any> | Y.Array<any>, obj: any) {
+export function toYjs(target: Y.Map<any> | Y.Array<any>, obj: any, visited: WeakSet<object> = new WeakSet()) {
   if (Array.isArray(obj)) {
@@
-    obj.forEach((item) => {
+    obj.forEach((item) => {
+      if (item && typeof item === 'object') {
+        if (visited.has(item)) throw new Error('Cyclic structure not supported')
+        visited.add(item)
+      }
@@
-        toYjs(childArr, item)
+        toYjs(childArr, item, visited)
@@
-        toYjs(childMap, item)
+        toYjs(childMap, item, visited)
@@
   } else if (obj && typeof obj === 'object') {
@@
-    Object.entries(obj).forEach(([key, val]) => {
+    Object.entries(obj).forEach(([key, val]) => {
+      if (val && typeof val === 'object') {
+        if (visited.has(val)) throw new Error('Cyclic structure not supported')
+        visited.add(val)
+      }
@@
-        toYjs(yArr, val) // 再递归写入
+        toYjs(yArr, val, visited) // 再递归写入
@@
-        toYjs(yMap, val)
+        toYjs(yMap, val, visited)

If cycles are impossible by design, feel free to ignore.

packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)

47-55: Guard binding with try/catch to avoid stuck “bound=true” on failure.

-    awareness.value.setLocalStateField('user', currentUser)
-    binding = new MonacoBinding(yText, model, new Set([editorRef]), awareness.value)
-    bound = true
-    // eslint-disable-next-line no-console
-    console.log('[useCollabMonaco] Binding successful.')
+    try {
+      awareness.value.setLocalStateField('user', currentUser)
+      binding = new MonacoBinding(yText, model, new Set([editorRef]), awareness.value)
+      bound = true
+      // eslint-disable-next-line no-console
+      console.log('[useCollabMonaco] Binding successful.')
+    } catch (err) {
+      bound = false
+      // eslint-disable-next-line no-console
+      console.error('[useCollabMonaco] Binding failed.', err)
+      return
+    }

98-99: Expose an explicit unbind() in return for consumers needing manual teardown.

-  return { binding, yText, ydoc, provider }
+  const unbind = () => { binding?.destroy(); cleanupListeners() }
+  return { binding, yText, ydoc, provider, unbind }

1-18: Add types for editor to improve DX (optional).

Consider typing editor as Monaco’s IStandaloneCodeEditor to surface API in IDEs.

packages/collab-ui/cursor/src/Main.vue (1)

53-57: Unused prop.

iframe is declared but not referenced; remove or wire it into useViewport.

packages/multi-person-collaboration/src/composables/useCollabCursor.ts (2)

16-20: API polish: rename options/interface to align with hook name.

-interface UserCollabCursorOptions {
+interface UseCollabCursorOptions {
   roomId: string
   currentUser: UserAwareness
+  websocketUrl?: string
 }
-
-export function useCollabCursor(options: UserCollabCursorOptions) {
+export function useCollabCursor(options: UseCollabCursorOptions) {

Also applies to: 28-28


59-65: Initialize local state — OK. Consider clearing on unmount (optional).

Add onUnmounted(() => updateLocalStateField('cursor', undefined as any)) so peers stop rendering your cursor immediately on leave.

packages/multi-person-collaboration/src/operation/operationHandler .ts (3)

90-97: "bottom" should append to end regardless of reference index.
Avoid inserting at 0 when index === -1.

-        case 'bottom': {
-          yChildren.insert(index + 1, [yNewNode])
-          break
-        }
+        case 'bottom': {
+          yChildren.insert(yChildren.length, [yNewNode])
+          break
+        }

345-349: Keep rootSchema in sync when rebuilding the map.
Assign the passed schema to the instance to avoid drift.

   public rebuildYNodeMap(rootSchema: PageSchema) {
     this.yNodeMap.clear()
+    this.rootSchema = rootSchema
     const yChildren = this.yMap.get('children') as Y.Array<Y.Map<any>>
     this.setYNode(rootSchema.children, yChildren)
   }

430-458: Swap clones entire subtrees; heavy and disrupts references. Move a single element instead.
Reduce churn by rehydrating only the moved node.

-    this.yDoc.transact(() => {
-      const i = Math.max(index1, index2)
-      const j = Math.min(index1, index2)
-      const elementI = yarray.get(i)
-      const elementJ = yarray.get(j)
-      // 类型安全的深拷贝函数
-      const cloneYMap = (el: Y.Map<any>): Y.Map<any> => {
-        const newMap = new Y.Map<any>()
-        toYjs(newMap, el.toJSON())
-        return newMap
-      }
-      const cloneI = cloneYMap(elementI)
-      const cloneJ = cloneYMap(elementJ)
-      yarray.delete(i, 1)
-      yarray.delete(j, 1)
-      yarray.insert(j, [cloneI])
-      yarray.insert(i, [cloneJ])
-      this.setYNode(this.rootSchema.children, this.yMap.get('children'))
-      // 构建唯一 ID
-      const eventId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
-      eventsMap.set(eventId, eventPayload)
-    })
+    this.yDoc.transact(() => {
+      const from = index1
+      const to = index2
+      const el = yarray.get(from)
+      const json = el.toJSON()
+      const rehydrated = new Y.Map<any>()
+      toYjs(rehydrated, json)
+      yarray.delete(from, 1)
+      const toIdx = from < to ? to - 1 : to
+      yarray.insert(toIdx, [rehydrated])
+      // optional: update yNodeMap minimally or rebuild if necessary
+      this.setYNode(this.rootSchema.children, this.yMap.get('children'))
+      const eventId = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
+      eventsMap.set(eventId, eventPayload)
+    })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9e0fd0 and 9d1f968.

📒 Files selected for processing (10)
  • packages/collab-ui/cursor/src/Main.vue (1 hunks)
  • packages/multi-person-collaboration/README.md (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabCursor.ts (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1 hunks)
  • packages/multi-person-collaboration/src/config/index.ts (1 hunks)
  • packages/multi-person-collaboration/src/operation/operationHandler .ts (1 hunks)
  • packages/multi-person-collaboration/src/services/schemaManager.ts (1 hunks)
  • packages/multi-person-collaboration/src/utils/index.ts (1 hunks)
  • packages/plugins/script/src/js/method.ts (4 hunks)
  • packages/plugins/versioncontrol/README.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/multi-person-collaboration/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/multi-person-collaboration/src/config/index.ts
  • packages/plugins/script/src/js/method.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/multi-person-collaboration/src/composables/useCollabCursor.ts
🧬 Code graph analysis (5)
packages/multi-person-collaboration/src/utils/index.ts (1)
packages/vue-generator/src/templates/vue-template/index.js (1)
  • value (30-30)
packages/multi-person-collaboration/src/composables/useCollabCursor.ts (4)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-58)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • updateLocalStateField (31-33)
packages/multi-person-collaboration/src/operation/operationHandler .ts (3)
packages/multi-person-collaboration/src/type.ts (9)
  • PageSchema (50-50)
  • NodeOperation (81-81)
  • Node (23-34)
  • DeleteOperation (83-85)
  • MoveOperation (87-91)
  • UpdateStyleOperation (93-97)
  • UpdatePropsOperation (99-103)
  • UpdateMethodsOperation (105-108)
  • UpdateAttributesOperation (110-114)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/multi-person-collaboration/src/utils/index.ts (1)
  • toYjs (10-54)
packages/multi-person-collaboration/src/services/schemaManager.ts (7)
packages/multi-person-collaboration/src/type.ts (2)
  • UpdateAttributesRole (116-116)
  • RootNode (36-48)
packages/multi-person-collaboration/src/models/NodeSchemaModel.ts (1)
  • NodeSchemaModel (19-110)
packages/multi-person-collaboration/src/services/providerManager.ts (1)
  • YjsProvider (4-4)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/register/src/hooks.ts (1)
  • useCanvas (79-79)
packages/multi-person-collaboration/src/config/index.ts (3)
  • ROOT_SCHEMA_MAP (5-5)
  • IGNORE_OBSERVER_ORIGIN (8-8)
  • INTERNAL_YJS_KEYS (11-11)
packages/multi-person-collaboration/src/utils/index.ts (3)
  • toYjs (10-54)
  • fromYjs (57-73)
  • sanitizeSchema (97-136)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/plugins/script/src/js/method.ts (1)
  • watch (235-270)
🔇 Additional comments (30)
packages/multi-person-collaboration/src/services/schemaManager.ts (8)

52-73: LGTM!

The singleton pattern implementation is correct with proper private constructor and static getInstance method. The Map initialization is appropriately handled both in field declarations and constructor for clarity.


160-165: Event listeners cleanup is incomplete.

Based on the past review comments, destroySchema should also clean up event listeners stored in this.eventListeners. Currently only observer callbacks are cleaned up.

Apply this fix to properly clean up event listeners:

   public destroySchema(docName: string): void {
     this.destroyObserver(docName)
+    
+    // Clean up event listeners
+    if (this.eventListeners.has(docName)) {
+      const { map, cb } = this.eventListeners.get(docName)
+      map.unobserve(cb)
+      this.eventListeners.delete(docName)
+    }
+    
     this.schemaMap.delete(docName)
     this.nodeSchemaModelMap.delete(docName)
+    this.initialSyncDone.delete(docName)
   }

178-232: LGTM!

The event listeners setup correctly handles cleanup of existing listeners before binding new ones, preventing duplicate listeners. The event processing logic properly handles move, insert, and delete operations from the event bus.


308-319: Potential null reference when accessing props.

The code calls yMapNode.get('props').toJSON() without checking if props exists, which could throw an error if props is null or not a Yjs type.

             } else if (key === 'props') {
               // Props 属性更新同步逻辑
               if (change.action === 'add' || change.action === 'update') {
-                const newProps = yMapNode.get('props').toJSON()
-                const { meta, ...cleanProps } = newProps
-                patches.push({
-                  type: 'props-update',
-                  path: event.path,
-                  props: cleanProps,
-                  meta
-                })
+                const propsValue = yMapNode.get('props')
+                if (propsValue && propsValue.toJSON) {
+                  const newProps = propsValue.toJSON()
+                  const { meta, ...cleanProps } = newProps
+                  patches.push({
+                    type: 'props-update',
+                    path: event.path,
+                    props: cleanProps,
+                    meta: meta || {}
+                  })
+                }
               }

353-374: Check for undefined property access on methods deletion.

When accessing newObj['_methods_deleted'] and newObj.meta.nodeId, there's no validation that newObj has these properties, which could cause runtime errors.

               if (change.action === 'add' || change.action === 'update') {
                 const newObj = yMapNode.get(key)
-                if (newObj['_methods_deleted']) {
+                if (newObj && typeof newObj === 'object') {
+                  if (newObj['_methods_deleted']) {
+                    if (newObj.meta?.nodeId) {
                   patches.push({
                     type: 'methods-delete',
                     path: event.path,
                     nodeId: newObj.meta.nodeId,
                     methodsName: key
                   })
+                    }
                 } else {
                   const { meta, ...methods } = newObj
+                    if (meta?.nodeId) {
                   patches.push({
                     type: 'methods-add-node',
                     path: event.path,
                     methods,
                     methodsName: key,
                     nodeId: meta.nodeId
                   })
+                    }
+                  }
                 }

415-433: Boundary check inconsistency in array swap.

The condition at line 424 checks patch.targetIndex > -1 && patch.swapIndex < childrenArray.length, but it should also verify that patch.swapIndex > -1 to ensure both indices are valid.

-          if (targetIndex > -1 && swapIndex < childrenArray.length) {
+          if (targetIndex > -1 && targetIndex < childrenArray.length && 
+              swapIndex > -1 && swapIndex < childrenArray.length) {

250-262: Consider fallback for offline provider scenarios.

The initial sync gate prevents all patches when initialSyncDone is false. If provider is undefined or fails to sync, patches will be blocked indefinitely.

Based on the search results, I can see that the 'sync' event is fired when the client receives content from the server and there can be intermittent issues where the sync event fires but documents aren't fully loaded. However, when changes happen and the provider is not connected or offline, the changes can be lost as messages are only sent when the websocket is available.

Consider adding a fallback mechanism for offline scenarios:

       // 标记初始同步未完成
       this.initialSyncDone.set(docName, false)

       if (provider) {
         const handleInitialSync = (isSynced: boolean) => {
           // ... existing sync logic ...
         }
         provider.on('sync', handleInitialSync)
+      } else {
+        // No provider means offline mode; allow local changes to be processed
+        console.log(`[${docName}] No provider available. Operating in offline mode.`)
+        this.initialSyncDone.set(docName, true)
       }

394-510: LGTM!

The patch application logic comprehensively handles all patch types from the DiffPatch union. The error handling around patch application ensures resilience against malformed data. Each patch type correctly maps to the corresponding useCanvas operations.

packages/multi-person-collaboration/src/utils/index.ts (5)

108-110: Confirm: array compaction changes indices

Filtering out undefined entries compacts arrays and shifts indices. Ensure callers don’t rely on stable positions.


3-3: Blocker: string sentinel can corrupt real data; switch to a structured token

The plain string "undefined" collides with legitimate user data and will be decoded as undefined.

Apply this diff to introduce a structured token and a shape guard:

-const UNDEFINED_PLACEHOLDER = '__undefined__'
+// Structured token to represent JS undefined without colliding with real strings
+const UNDEFINED_PLACEHOLDER = Object.freeze({ __tiny_yjs_undefined__: true } as const)
+
+function isUndefinedToken(v: unknown): v is typeof UNDEFINED_PLACEHOLDER {
+  return !!v && typeof v === 'object' && (v as any).__tiny_yjs_undefined__ === true
+}

16-18: Write the structured undefined token in toYjs (arrays and maps)

Update the encoder to persist the new structured token.

-      if (item === undefined) {
-        target.push([UNDEFINED_PLACEHOLDER])
+      if (item === undefined) {
+        target.push([UNDEFINED_PLACEHOLDER])
@@
-      if (val === undefined) {
-        target.set(key, UNDEFINED_PLACEHOLDER)
+      if (val === undefined) {
+        target.set(key, UNDEFINED_PLACEHOLDER)

Also applies to: 36-39


56-73: Decode undefined by shape, not string equality

Make decoding robust and non-colliding.

   } else if (value instanceof Y.Text) {
     return value.toString()
-  } else if (value === UNDEFINED_PLACEHOLDER) {
-    return undefined // 还原 undefined
+  } else if (isUndefinedToken(value)) {
+    return undefined // 还原 undefined
   } else {
     return value
   }

81-86: Fix getValueByPath: inverted null-check returns undefined always

Guard only when accumulator is null/undefined; allow falsy values like 0 or ''.

-export const getValueByPath = (obj: any, path: (string | number)[]): any => {
-  return path.reduce((acc, key) => {
-    if (acc) return undefined // 避免继续取值报错
-    return acc[key]
-  }, obj)
-}
+export const getValueByPath = (obj: any, path: (string | number)[]): any => {
+  return path.reduce((acc: any, key) => {
+    if (acc == null) return undefined // null/undefined: stop safely
+    return acc[key as any]
+  }, obj)
+}
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (2)

16-18: Make WebSocket URL configurable; derive ws/wss from location by default.

Avoid hard-coding localhost so this works in non-local and HTTPS contexts.

 interface UseCollabMonacoOptions {
   currentUser: UserAwareness
   editorRef: any
   roomId: string
   fieldName: string
+  websocketUrl?: string
 }

 export function useCollabMonaco(options: UseCollabMonacoOptions) {
-  const { currentUser, editorRef, roomId, fieldName } = options
-  const { ydoc, awareness, provider } = useYjs(roomId, {
-    websocketUrl: `ws://localhost:${PORT}`
-  })
+  const { currentUser, editorRef, roomId, fieldName, websocketUrl } = options
+  const proto = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss' : 'ws'
+  const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
+  const ws = websocketUrl ?? `${proto}://${host}:${PORT}`
+  const { ydoc, awareness, provider } = useYjs(roomId, { websocketUrl: ws })

25-30: Detach 'sync' listener properly; current once/off mismatch can leak after unmount.

Using once() then off(bind) won’t remove the internal wrapper if unmounted pre-sync; keep a stable handler and clean up.

-// 定义一个清理函数,用来移除所有监听器
-function cleanupListeners(bindFn: () => void) {
-  if (provider.value) {
-    provider.value.off('sync', bindFn)
-  }
-}
+let syncHandler: ((isSynced: boolean) => void) | null = null
+function cleanupListeners() {
+  if (provider.value && syncHandler) {
+    provider.value.off('sync', syncHandler)
+    syncHandler = null
+  }
+}
@@
-      // 使用 once 监听 sync 事件
-      prov.once('sync', bind)
+      // attach a stable handler so we can detach on unmount
+      if (syncHandler) prov.off('sync', syncHandler)
+      syncHandler = (isSynced: boolean) => { if (isSynced) bind() }
+      prov.on('sync', syncHandler)
@@
-      // provider 出现后即可停止对 provider 自身的 watch
-      stopProviderWatch?.()
+      // stop provider watch only after successful bind
+      if (bound) stopProviderWatch?.()
@@
-  onUnmounted(() => {
+  onUnmounted(() => {
     binding?.destroy()
-    cleanupListeners(bind) // 确保所有监听器都被移除
+    cleanupListeners() // 确保所有监听器都被移除

Also applies to: 63-65, 71-73, 91-96

packages/collab-ui/cursor/src/Main.vue (3)

50-70: Don’t hard-code currentUser/roomId; accept as props and thread through.

 export default {
   name: 'Cursor',
-  props: {
-    iframe: {
-      type: Object,
-      default: () => {}
-    }
-  },
-  setup() {
-    const currentUser = {
-      id: 'user-2',
-      name: 'Bob',
-      color: '#1296db'
-    }
+  props: {
+    iframe: { type: Object, default: () => {} },
+    currentUser: { type: Object, required: true },
+    roomId: { type: String, default: 'cursor-yjs' }
+  },
+  setup(props) {
@@
-    const collabState = reactive(
-      useCollabCursor({
-        roomId: 'cursor-yjs',
-        currentUser
-      })
-    )
+    const collabState = reactive(
+      useCollabCursor({
+        roomId: props.roomId,
+        currentUser: props.currentUser
+      })
+    )

118-124: pressedMap defaults are fine; keep after filter.


74-77: Filter out entries without valid cursor coords to avoid NaN/template crashes.

-    const processedCursors = computed(() => {
-      return Object.entries(collabState.remoteCursors).map(([clientId, state]) => {
-        if (!state.cursor) return { clientId, state, position: {} }
+    const processedCursors = computed(() => {
+      return Object.entries(collabState.remoteCursors)
+        .filter(([, state]) => state?.cursor && Number.isFinite(state.cursor.x) && Number.isFinite(state.cursor.y))
+        .map(([clientId, state]) => {
@@
-        return { clientId, state, position }
-      })
+        return { clientId, state, position }
+      })
     })

Also applies to: 114-116

packages/multi-person-collaboration/src/composables/useCollabCursor.ts (4)

29-31: Make WebSocket URL configurable; derive ws/wss by default.

-export function useCollabCursor(options: UserCollabCursorOptions) {
-  const { roomId, currentUser } = options
-  const { awareness } = useYjs(roomId, { websocketUrl: `ws://localhost:${PORT}` })
+export function useCollabCursor(options: UseCollabCursorOptions) {
+  const { roomId, currentUser, websocketUrl } = options
+  const proto = typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'wss' : 'ws'
+  const host = typeof window !== 'undefined' ? window.location.hostname : 'localhost'
+  const ws = websocketUrl ?? `${proto}://${host}:${PORT}`
+  const { awareness } = useYjs(roomId, { websocketUrl: ws })

38-39: Primary-button check should be bitwise, not strict equality.


42-57: Avoid jumping to (0,0) on mousedown before first move.

Use last known coords or event coords; accept MouseEvent in handlers.

-  const updateCursorPressedState = (pressed: boolean) => {
+  const updateCursorPressedState = (pressed: boolean, ev?: MouseEvent) => {
     const localState = awareness.value?.getLocalState() as CursorAwarenessState | undefined
 
-    const currentX = localState?.cursor?.x || 0
-    const currentY = localState?.cursor?.y || 0
+    const currentX = localState?.cursor?.x ?? ev?.pageX
+    const currentY = localState?.cursor?.y ?? ev?.pageY
+    if (currentX == null || currentY == null) return
 
     updateLocalStateField('cursor', {
       x: currentX,
       y: currentY,
       pressed
     })
   }
 
-  const mouseDownHandler = () => updateCursorPressedState(true)
+  const mouseDownHandler = (ev: MouseEvent) => updateCursorPressedState(true, ev)
   const mouseUpHandler = () => updateCursorPressedState(false)

34-40: Fix typo in exported API: updateCursorPositioin → updateCursorPosition.

-  const updateCursorPositioin = (event: MouseEvent) => {
+  const updateCursorPosition = (event: MouseEvent) => {
     updateLocalStateField('cursor', {
       x: event.pageX,
       y: event.pageY,
-      pressed: event.buttons === 1
+      pressed: (event.buttons & 1) === 1
     })
   }
@@
   return {
     remoteCursors: remoteStates,
-    updateCursorPositioin,
+    updateCursorPosition,
     mouseDownHandler,
     mouseUpHandler
   }

Also applies to: 67-72

packages/multi-person-collaboration/src/operation/operationHandler .ts (8)

1-1: Rename file: trailing space before ".ts" breaks imports and tooling.
Use operationHandler.ts and update all references.

#!/bin/bash
# Find stray references to the wrong filename (with a space)
rg -nP $'operationHandler\\s+\\.ts|operationHandler \\.ts'

38-44: Insert into root when parentId equals root schema id.
Currently returns early; handle root fallback.

-    const parentNode = parentId ? this.getYNode(parentId) : this.yMap
-
-    if (!parentNode) {
+    let parentNode = parentId ? this.getYNode(parentId) : (this.yMap as Y.Map<any>)
+    if (!parentNode && parentId && parentId === (this.rootSchema.id || '')) {
+      parentNode = this.yMap as Y.Map<any>
+    }
+    if (!parentNode) {
       // eslint-disable-next-line no-console
       console.warn(`[Insert Operation] Parent node with ID "${parentId}" not found in schema. Aborting.`)
       return { current: newNodeData }
     }

68-81: "out" wrap is incorrect and risks Yjs schema violations.
Wrap the reference node itself as the child of the new node (children must be a Y.Array), not just clone its children.

-        case 'out': {
-          const referenceNode = this.getYNode(referTargetNodeId)
-          if (referenceNode) {
-            const childrenOfReference = referenceNode.get('children')
-            // 如果被包裹的节点有子节点,需要将其子节点也一并带过来
-            if (childrenOfReference instanceof Y.Array) {
-              yNewNode.set('children', childrenOfReference.clone())
-            }
-            // 将被包裹的就节点标记为删除
-            yChildren.get(index)?.set('_node_deleted', true)
-            yChildren.insert(index, [yNewNode])
-          }
-          break
-        }
+        case 'out': {
+          const referenceNode = this.getYNode(referTargetNodeId)
+          if (referenceNode) {
+            const idx = index === -1 ? yChildren.length : index
+            const original = yChildren.get(idx) as Y.Map<any> | undefined
+            if (!original) break
+            const childArray = new Y.Array<Y.Map<any>>()
+            childArray.insert(0, [original])
+            yNewNode.set('children', childArray)
+            original.set('_node_deleted', true)
+            yChildren.insert(idx, [yNewNode])
+          }
+          break
+        }

281-311: updatedMethods: ensure props map exists and wrap in a transaction.
Prevents NPE and guarantees atomic updates.

-    if (operation.type === 'root') {
-      const methods = operation.methods
-      // 根节点直接设置 methods 不需要 id
-      this.yMap.set('methods', methods)
-    } else if (operation.type === 'node') {
-      const { nodeId, methodsName, methods } = operation
-      const node = this.getYNode(nodeId)
-      if (node) {
-        const nodeProps = node.get('props')
-        nodeProps.set(methodsName, {
-          ...methods,
-          meta: { nodeId }
-        })
-      }
-    } else if (operation.type === 'delete-method') {
-      const { nodeId, methodsName } = operation
-      const node = this.getYNode(nodeId)
-      if (node) {
-        const nodeProps = node.get('props')
-        // 依旧软删除
-        nodeProps.set(methodsName, {
-          _methods_deleted: true,
-          meta: { nodeId }
-        })
-        // 软删除后直接硬删除删除,保证 yMap 数据干净
-        nodeProps.delete(methodsName)
-      }
-    }
+    this.yDoc.transact(() => {
+      if (operation.type === 'root') {
+        this.yMap.set('methods', operation.methods)
+        return
+      }
+      if (operation.type === 'node') {
+        const { nodeId, methodsName, methods } = operation
+        const node = this.getYNode(nodeId)
+        if (!node) return
+        let nodeProps = node.get('props') as Y.Map<any> | undefined
+        if (!(nodeProps instanceof Y.Map)) {
+          nodeProps = new Y.Map()
+          node.set('props', nodeProps)
+        }
+        nodeProps.set(methodsName, { methods, meta: { nodeId } })
+        return
+      }
+      // delete-method
+      const { nodeId, methodsName } = operation
+      const node = this.getYNode(nodeId)
+      if (!node) return
+      const nodeProps = node.get('props') as Y.Map<any> | undefined
+      nodeProps?.delete(methodsName)
+    })

399-428: Event payload key mismatch: use 'schemeId' (not 'schemaId') to match consumers.
Aligns with SchemaManager expectations.

-    schemaId: string,
+    schemeId: string,
@@
-      schemaId,
+      schemeId,
#!/bin/bash
# Verify which key downstream expects
rg -nP "\bschemeId\b|\bschemaId\b" packages | sed -n '1,200p'

30-33: Transact on the Doc that actually owns yMap to avoid cross‑Doc ops.
Bind this.yDoc to this.yMap.doc when available.

-    const docManager = DocManager.getInstance()
-    this.yDoc = docManager.getOrCreateDoc(docName)
+    const existingDoc = (this.yMap as any).doc as Y.Doc | undefined
+    this.yDoc = existingDoc ?? DocManager.getInstance().getOrCreateDoc(docName)

228-239: Possible runtime error when props is missing; also group changes in a transaction.
targetNode?.get('props').set(...) throws if props is undefined.

-    // 添加样式
-    this.yMap.set('css', strStyle)
-
-    // 添加 class 类名
-    const targetNode = this.getYNode(nodeId)
-    targetNode?.get('props').set('className', `${className}_${nodeId}`)
-
-    Object.assign(this.rootSchema, { css: strStyle })
+    this.yDoc.transact(() => {
+      this.yMap.set('css', strStyle)
+      const targetNode = this.getYNode(nodeId)
+      const props = targetNode?.get('props') as Y.Map<any> | undefined
+      props?.set('className', `${className}_${nodeId}`)
+    })
+    Object.assign(this.rootSchema, { css: strStyle })

242-279: updatedProps: unsafe Yjs struct reuse, missing transaction, and a crash when newProps is undefined.

  • Copying Yjs types into a new parent (yNewProps.set(k, val)) breaks Yjs invariants.
  • Object.entries(newProps) will throw if newProps is undefined.
  • Not atomic.
-    const yNewProps = new Y.Map<any>() // 新的 props
-    const propsMap = node.get('props') as Y.Map<any> // 旧的 props
-    if (overwrite) {
-      // 覆盖模式
-      for (const [k, v] of Object.entries(newProps || {})) {
-        yNewProps.set(k, v)
-      }
-    } else {
-      // 先复制旧的
-      if (propsMap) {
-        propsMap.forEach((val, key) => {
-          yNewProps.set(key, val)
-        })
-      }
-      // 再合并新的
-      for (const [k, v] of Object.entries(newProps) || {}) {
-        yNewProps.set(k, v)
-      }
-    }
-    // 元数据,用于补丁操作
-    const meta = new Y.Map<any>()
-    meta.set('nodeId', nodeId)
-    meta.set('overwrite', overwrite)
-    yNewProps.set('meta', meta)
-    node.set('props', yNewProps)
+    this.yDoc.transact(() => {
+      const propsMap = node.get('props') as Y.Map<any> | undefined
+      const baseJson = overwrite ? {} : (propsMap?.toJSON?.() ?? {})
+      const merged = { ...baseJson, ...(newProps ?? {}) }
+      const meta = new Y.Map<any>()
+      meta.set('nodeId', nodeId)
+      meta.set('overwrite', overwrite)
+      const yMerged = new Y.Map<any>()
+      toYjs(yMerged, merged)
+      yMerged.set('meta', meta)
+      node.set('props', yMerged)
+    })

Comment thread packages/multi-person-collaboration/src/composables/useCollabMonaco.ts Outdated
Comment thread packages/multi-person-collaboration/src/services/schemaManager.ts
Comment thread packages/plugins/versioncontrol/README.md

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (4)

88-93: Avoid randomness in diff stats (hurts determinism and tests).

Use deterministic placeholders or real stats instead of Math.random.

-          additions: Math.floor(Math.random() * 20),
-          deletions: Math.floor(Math.random() * 10)
+          additions: 0,
+          deletions: 0

149-153: Prefill branch target commit for branch creation.

Set a sensible default like the selected commit or latest commit.

   const createBranch = () => {
     branchDialogVisible.value = true
     newBranchName.value = ''
-    // branchTargetCommit.value = selectedCommit.value ? selectedCommit.value.hash : commits.value[0].hash // 默认当前选中或最新提交
+    const fallback = commits.value?.[0]?.hash
+    if (selectedCommit.value?.hash || fallback) {
+      branchTargetCommit.value = selectedCommit.value?.hash ?? fallback!
+    }
   }

6-29: Strongly type state refs instead of any to prevent future bugs.

Typing would have caught the .value check issue earlier.

 import { versionManager } from '../js'
 import type { DisplayCommit } from './uesVersionControlData'
 import { useUtils } from './useUtils'
 import { useCanvas, useModal, useNotify } from '@opentiny/tiny-engine-meta-register'
+import type { Ref } from 'vue'
 
+interface VersionControlStateRefs {
+  commits: Ref<DisplayCommit[]>
+  branches: Ref<string[]>
+  currentBranch: Ref<string>
+  selectedCommit: Ref<DisplayCommit | null>
+  dialogVisible: Ref<boolean>
+  compareDialogVisible: Ref<boolean>
+  isLoading: Ref<boolean>
+  compareData: Ref<{
+    base: DisplayCommit | null
+    target: DisplayCommit | null
+    filesChanged: number
+    additions: number
+    deletions: number
+    changedFiles: Array<{ name: string; additions: number; deletions: number }>
+  }>
+  tagDialogVisible: Ref<boolean>
+  tagTargetCommit: Ref<string>
+  currentPage: Ref<number>
+  searchQuery: Ref<string>
+  authorFilter: Ref<string>
+  timeFilter: Ref<string>
+  branchDialogVisible: Ref<boolean>
+  branchTargetCommit: Ref<string | undefined | null>
+  newBranchName: Ref<string>
+  commitDialogVisible: Ref<boolean>
+  close: () => void
+}
+
 export function useVersionControlActions(
   emit: (event: string, ...args: any[]) => void,
   {
     commits,
     branches,
     currentBranch,
     selectedCommit,
     dialogVisible,
     compareDialogVisible,
     isLoading,
     compareData,
     tagDialogVisible,
     tagTargetCommit,
     currentPage,
     searchQuery,
     authorFilter,
     timeFilter,
     branchDialogVisible,
     branchTargetCommit,
     newBranchName,
     commitDialogVisible,
     close
-  }: any
+  }: VersionControlStateRefs
 ) {

2-2: Typo in module name: uesVersionControlData.

If unintentional, consider renaming the file/module to useVersionControlData for consistency. Defer if widely referenced.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d1f968 and 1fc48cf.

📒 Files selected for processing (2)
  • packages/plugins/versioncontrol/src/Main.vue (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/plugins/versioncontrol/src/Main.vue
🧰 Additional context used
🧬 Code graph analysis (1)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (4)
packages/plugins/versioncontrol/src/composable/useUtils.ts (1)
  • useUtils (5-51)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)
  • DisplayCommit (6-20)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/register/src/hooks.ts (1)
  • useNotify (94-94)
🔇 Additional comments (4)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (4)

44-50: Clear filters logic looks good.


52-54: Sorting resets pagination appropriately.


161-169: Fix ref checks and replace alert with notify + validations.

The if condition checks refs, not their values; also avoid alert and validate empty/duplicate names.

-  const confirmCreateBranch = () => {
-    if (branchTargetCommit && newBranchName) {
-      alert(`已基于提交 ${branchTargetCommit.value} 创建新分支:${newBranchName.value} (模拟操作)`)
-      branches.value.push(newBranchName.value)
-      currentBranch.value = newBranchName.value
-      // 实际操作中会调用后端API创建分支
-    }
-    branchDialogVisible.value = false
-  }
+  const confirmCreateBranch = () => {
+    const name = newBranchName.value?.trim()
+    const target = branchTargetCommit.value
+    if (!name || !target) {
+      useNotify({ type: 'warning', message: '请选择提交并输入分支名' })
+      return
+    }
+    if (branches.value.includes(name)) {
+      useNotify({ type: 'error', message: `分支 "${name}" 已存在` })
+      return
+    }
+    branches.value.push(name)
+    currentBranch.value = name
+    // 实际操作中会调用后端API创建分支
+    useNotify({ type: 'success', message: `已基于提交 ${target} 创建新分支:${name} (模拟)` })
+    branchDialogVisible.value = false
+  }

70-96: Fix null-safety in compareCommit and correct variable typo (can crash on missing branch).

Guard against missing commit.branches[0], missing baseCommitId, and null raw commit; also rename brnachId → branchName.

Apply this diff:

   const compareCommit = async (commit: DisplayCommit) => {
     // 比较 commit
-    const brnachId = commit.branches[0]
-    const baseBranch = await versionManager.branchRepository.findByName(brnachId)
-
-    // 找到基准 commit
-    const baseCommitId = baseBranch?.baseCommitId as string
-    const rawBaseCommit = await versionManager.commitRepository.findById(baseCommitId)
-
-    const baseCommit = await transformCommit(rawBaseCommit)
+    const branchName = commit.branches?.[0]
+    let baseCommit: DisplayCommit | null = null
+    if (branchName) {
+      const baseBranch = await versionManager.branchRepository.findByName(branchName)
+      const baseCommitId = baseBranch?.baseCommitId as string | undefined
+      if (baseCommitId) {
+        const rawBaseCommit = await versionManager.commitRepository.findById(baseCommitId)
+        if (rawBaseCommit) {
+          baseCommit = await transformCommit(rawBaseCommit)
+        }
+      }
+    }
 
     // 对比数据
     compareData.value = {
-      base: baseCommit || null,
+      base: baseCommit,
       target: commit,
       filesChanged: commit.filesChanged || 0,
       additions: commit.additions || 0,
       deletions: commit.deletions || 0,
       changedFiles:
         commit.changedFiles?.map((name) => ({
           name,
           additions: Math.floor(Math.random() * 20),
           deletions: Math.floor(Math.random() * 10)
         })) || []
     }
     compareDialogVisible.value = true
   }

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/plugins/versioncontrol/src/Main.vue (1)

109-123: Consider organizing imports for better readability.

The imports could be better organized by grouping Vue components separately from composables.

 import VersionHeader from './components/VersionHeader.vue'
 import VersionTagCreate from './components/VersionTagCreate.vue'
 import VersionCommitInfo from './components/VersionCommitInfo.vue'
 import VersionDiffDialog from './components/VersionDiffDialog.vue'
 import VersionControlFilters from './components/VersionControlFilters.vue'
 import TimelineContainer from './components/TimelineContainer.vue'
 import CommitsContainer from './components/CommitsContainer.vue'
 import VersionBranchCreate from './components/VersionBranchCreate.vue'
 import VersionCommitCreate from './components/VersionCommitCreate.vue'

 import { useVersionControlData } from './composable/useVersionControlData'
 import { useVersionControlActions } from './composable/useVersionControlAction'
 import { useVersionControlUtils } from './composable/useVersionControlUtils'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc48cf and 2b8293f.

📒 Files selected for processing (3)
  • packages/plugins/versioncontrol/index.ts (1 hunks)
  • packages/plugins/versioncontrol/src/Main.vue (1 hunks)
  • packages/plugins/versioncontrol/src/styles/vars.less (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/plugins/versioncontrol/src/styles/vars.less
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/plugins/versioncontrol/index.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-01-14T08:37:01.393Z
Learnt from: gene9831
PR: opentiny/tiny-engine#1011
File: packages/plugins/page/src/PageTree.vue:340-345
Timestamp: 2025-01-14T08:37:01.393Z
Learning: The code in PageTree.vue is based on template code copied from elsewhere and will be refactored later, so suggestions for improvements should be deferred until that refactoring occurs.

Applied to files:

  • packages/plugins/versioncontrol/src/Main.vue
🔇 Additional comments (2)
packages/plugins/versioncontrol/src/Main.vue (2)

1-207: Well-architected component using Vue 3 best practices.

The component correctly uses the <script setup> syntax with the Composition API, properly leverages composables to encapsulate and reuse stateful logic, and follows Vue SFC conventions with proper separation of template, logic and styling. The modular design with dedicated composables for data management, actions, and utilities promotes maintainability and reusability.


209-290: Review responsive design breakpoints for accessibility.

The responsive breakpoints look comprehensive, but consider testing the mobile view (768px and below) where the layout switches to column direction to ensure all functionality remains accessible.

Please verify that the mobile layout change from flex row to column direction doesn't impact the usability of the version control interface, particularly the timeline and commits containers when stacked vertically.

Comment thread packages/plugins/versioncontrol/src/Main.vue
Comment thread packages/plugins/versioncontrol/src/Main.vue

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (15)
packages/multi-person-collaboration/test/services/docManager.spec.ts (1)

9-14: Avoid bypassing DocManager’s private singleton state

Reaching into the private static instance with @ts-ignore makes the test brittle—if the implementation ever switches to a #private field (or even just renames the member), these tests break silently. You can keep test isolation by clearing the shared docs map instead of resetting the singleton reference.

beforeEach(() => {
-  // 重置单例,保证每个测试用例独立
-  // @ts-ignore
-  DocManager.instance = undefined
-  manager = DocManager.getInstance()
+  manager = DocManager.getInstance()
+  manager.destroyAllDocs()
})

This keeps the tests independent without depending on internal class details.

packages/multi-person-collaboration/test/services/providerManager.spec.ts (2)

23-29: Reset mocks between test cases

The module-level mock keeps its call history across specs, which can make future assertions brittle. Clearing the mocks in each setup keeps the suite isolated.

 beforeEach(() => {
+    vi.clearAllMocks()
     // 重置单例
     // @ts-ignore
     ProviderManager.instance = undefined

46-50: Assert resource cleanup when forcing a new provider

forceNew should both destroy the old provider and replace the cached instance. Adding explicit expectations here will catch any regressions in that control flow.

   it('forceNew 为 true 时应该重新创建', () => {
     const oldProvider = manager.createProvider('room2', ydoc, { websocketUrl: 'ws://localhost:1234' })
+    const destroySpy = vi.spyOn(oldProvider, 'destroy')
     const newProvider = manager.createProvider('room2', ydoc, { websocketUrl: 'ws://localhost:1234' }, true)
     expect(newProvider).not.toBe(oldProvider)
+    expect(manager.getProvider('room2')).toBe(newProvider)
+    expect(destroySpy).toHaveBeenCalled()
   })
packages/multi-person-collaboration/test/composables/useAwareness.spec.ts (3)

38-45: Type the ref explicitly and avoid ref re‑assignment

Use a typed ref and assign to .value in beforeEach. This prevents implicit any and avoids re-creating the ref instance across tests.

-  let awarenessRef: ReturnType<typeof ref>
+  let awarenessRef = ref<Awareness | null>(null)
@@
-    awareness = new FakeAwareness()
-    awarenessRef = ref(awareness as unknown as Awareness)
+    awareness = new FakeAwareness()
+    awarenessRef.value = awareness as unknown as Awareness

6-34: Optional: Add getLocalState to FakeAwareness for parity

Not required for current tests, but adding getLocalState helps mirror Yjs Awareness API and can reduce future mocking churn.

 class FakeAwareness {
   clientID = 1
   states = new Map()
   listeners: Record<string, Function[]> = {}
@@
   setLocalStateField(field: string, value: any) {
     const state = this.states.get(this.clientID) || {}
     state[field] = value
     this.states.set(this.clientID, state)
   }
+
+  getLocalState() {
+    return this.states.get(this.clientID)
+  }

1-1: Ensure cleanup between tests to prevent leaked listeners

Destroy the model by clearing the awareness reference after each test; the composable watcher will call destroy() on change.

-import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
@@
   beforeEach(() => {
     awareness = new FakeAwareness()
     awarenessRef.value = awareness as unknown as Awareness
   })
+
+  afterEach(() => {
+    // Triggers model cleanup in useAwareness watcher
+    awarenessRef.value = null
+  })

Also applies to: 42-45

packages/multi-person-collaboration/test/models/AwarenessStateModel.spec.ts (1)

119-124: Strengthen destroy assertions

You’re checking that off is called and the emitter is cleared. Consider asserting the exact event and handler signature to catch regressions.

-    const spyOff = vi.spyOn(awareness, 'off')
+    const spyOff = vi.spyOn(awareness, 'off')
     model.destroy()
-    expect(spyOff).toHaveBeenCalled()
+    expect(spyOff).toHaveBeenCalledWith('update', expect.any(Function))
     expect(model.emitter.all.size).toBe(0)
packages/multi-person-collaboration/test/composables/useCollabCursor.spec.ts (2)

27-35: Return value is never cleaned up; stop the effectScope to avoid leaked watchers

Not stopping the scope can accumulate watchers across tests. Use the returned stop in each test.

-  it('初始化时应设置 cursor 为 (-1, -1, false)', async () => {
-    runComposable()
+  it('初始化时应设置 cursor 为 (-1, -1, false)', async () => {
+    const ctx = runComposable()
     await nextTick()
     expect(mockAwareness.setLocalStateField).toHaveBeenCalledWith('cursor', {
       x: -1,
       y: -1,
       pressed: false
     })
+    ctx.stop()
   })
@@
-  it('updateCursorPositioin 应更新光标位置', () => {
-    const { updateCursorPositioin } = runComposable()
+  it('updateCursorPositioin 应更新光标位置', () => {
+    const { updateCursorPositioin, stop } = runComposable()
@@
-    updateCursorPositioin(fakeEvent)
+    updateCursorPositioin(fakeEvent)
@@
-    })
+    })
+    stop()
   })
@@
-  it('mouseDownHandler / mouseUpHandler 应切换 pressed 状态', () => {
-    const { mouseDownHandler, mouseUpHandler } = runComposable()
+  it('mouseDownHandler / mouseUpHandler 应切换 pressed 状态', () => {
+    const { mouseDownHandler, mouseUpHandler, stop } = runComposable()
@@
-    )
+    )
+    stop()
   })

47-57: API typo: updateCursorPositioin

The method name has a typo (“Positioin”). Consider renaming to updateCursorPosition in the implementation and tests, and exporting a deprecated alias to avoid breakage.

Would you like me to generate a codemod and a follow-up PR note to perform a two-step rename (add alias, migrate usages, then remove alias)?

packages/multi-person-collaboration/test/composables/useCollabMonaco.spec.ts (2)

81-92: onUnmounted cleanup is not exercised

Calling binding.destroy() directly doesn’t prove the composable’s cleanup. If feasible, wrap in a component and unmount, or at least assert provider.off('sync', ...) is called via the registered cleanup.

Example approach:

  • Mount a minimal component that calls useCollabMonaco, then unmount and assert provider.off was called with 'sync' and a function.

21-37: Hoist and reuse awareness mock for clearer assertions

Consider exposing the awareness ref from the mock to the test scope (as in the previous diff) to simplify expectations and avoid reaching through provider internals.

packages/multi-person-collaboration/test/utils/index.spec.ts (2)

1-3: Optional: import path without .ts extension

Vite/Vitest typically resolves TS extensions; dropping “.ts” improves portability.

-import { toYjs, fromYjs, sanitizeSchema } from '../../src/utils/index.ts'
+import { toYjs, fromYjs, sanitizeSchema } from '../../src/utils'

35-57: Round-trip tests look solid; add a Y.Text case for completeness

Consider adding a case that covers Y.Text round-tripping to exercise the instanceof Y.Text branch in fromYjs.

Example snippet:

const ydoc = new Y.Doc()
const ytext = ydoc.getText('t')
ytext.insert(0, 'hello')
expect(fromYjs(ytext)).toBe('hello')
packages/multi-person-collaboration/test/composables/useCollabSchema.spec.ts (2)

11-14: Mock declarations after imports can be brittle

Vitest hoists vi.mock, but keeping mocks above imports avoids edge cases. Consider moving these vi.mock calls above the imports of the same modules.


124-134: Strengthen sync callback assertion

Good check for rebuild on sync. Optionally assert the event name as well, i.e., that the first provider.on call was for 'sync'.

-  const syncCallback = providerMock.value.on.mock.calls[0][1]
+  const [event, syncCallback] = providerMock.value.on.mock.calls[0]
+  expect(event).toBe('sync')
   syncCallback(true)
   expect(schemaModelMock.operationHandler.rebuildYNodeMap)
     .toHaveBeenCalledWith({ id: 'root', children: [] })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b8293f and 6d94f8e.

📒 Files selected for processing (28)
  • packages/canvas/container/src/container.ts (6 hunks)
  • packages/multi-person-collaboration/package.json (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabCursor.ts (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1 hunks)
  • packages/multi-person-collaboration/src/composables/useYjs.ts (1 hunks)
  • packages/multi-person-collaboration/src/utils/index.ts (1 hunks)
  • packages/multi-person-collaboration/test/composables/useAwareness.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/composables/useCollabCursor.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/composables/useCollabMonaco.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/composables/useCollabSchema.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/composables/useYjs.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/models/AwarenessStateModel.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/services/docManager.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/services/providerManager.spec.ts (1 hunks)
  • packages/multi-person-collaboration/test/utils/index.spec.ts (1 hunks)
  • packages/multi-person-collaboration/vite.config.js (1 hunks)
  • packages/plugins/versioncontrol/package.json (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionCommitCreate.vue (1 hunks)
  • packages/plugins/versioncontrol/test/component/CommitCategorySelect.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/CommitsContainer.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/TimelineContainer.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionBranchCreate.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionCommitCreate.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionCommitInfo.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionControlFilters.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionDiffDialog.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionTagCreate.spec.js (1 hunks)
  • packages/plugins/versioncontrol/vite.config.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/multi-person-collaboration/vite.config.js
  • packages/multi-person-collaboration/package.json
  • packages/plugins/versioncontrol/vite.config.js
  • packages/plugins/versioncontrol/src/components/VersionCommitCreate.vue
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-01-14T06:55:14.457Z
Learnt from: rhlin
PR: opentiny/tiny-engine#1011
File: packages/canvas/render/src/canvas-function/design-mode.ts:6-13
Timestamp: 2025-01-14T06:55:14.457Z
Learning: The code in `packages/canvas/render/src/canvas-function/design-mode.ts` is migrated code that should be preserved in its current form during the migration process. Refactoring suggestions for type safety and state management improvements should be considered in future PRs.

Applied to files:

  • packages/multi-person-collaboration/src/composables/useCollabCursor.ts
📚 Learning: 2024-09-30T07:51:10.036Z
Learnt from: chilingling
PR: opentiny/tiny-engine#837
File: packages/vue-generator/src/plugins/genDependenciesPlugin.js:66-66
Timestamp: 2024-09-30T07:51:10.036Z
Learning: In the `tiny-engine` project, `opentiny/tiny-engine-dsl-vue` refers to the current package itself, and importing types from it may cause circular dependencies.

Applied to files:

  • packages/plugins/versioncontrol/package.json
📚 Learning: 2024-12-14T05:53:28.501Z
Learnt from: gene9831
PR: opentiny/tiny-engine#917
File: docs/开始/快速上手.md:31-31
Timestamp: 2024-12-14T05:53:28.501Z
Learning: The latest stable version of `opentiny/tiny-engine-cli` is `2.0.0`, and documentation should reference this version instead of any release candidates.

Applied to files:

  • packages/plugins/versioncontrol/package.json
🧬 Code graph analysis (16)
packages/multi-person-collaboration/test/models/AwarenessStateModel.spec.ts (1)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • AwarenessStateModel (18-66)
packages/multi-person-collaboration/test/composables/useAwareness.spec.ts (2)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-58)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • updateLocalStateField (31-33)
packages/multi-person-collaboration/test/services/providerManager.spec.ts (3)
packages/multi-person-collaboration/src/services/providerManager.ts (1)
  • ProviderManager (16-83)
packages/multi-person-collaboration/test/composables/useAwareness.spec.ts (1)
  • off (27-29)
packages/multi-person-collaboration/test/models/AwarenessStateModel.spec.ts (1)
  • off (21-28)
packages/multi-person-collaboration/test/composables/useCollabCursor.spec.ts (1)
packages/multi-person-collaboration/src/composables/useCollabCursor.ts (1)
  • useCollabCursor (28-74)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (3)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)
  • useYjs (25-92)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/multi-person-collaboration/src/composables/useCollabCursor.ts (5)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)
  • useYjs (25-92)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-58)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • updateLocalStateField (31-33)
packages/multi-person-collaboration/test/composables/useCollabSchema.spec.ts (4)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)
  • useYjs (25-92)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-58)
packages/multi-person-collaboration/src/services/schemaManager.ts (1)
  • SchemaManager (52-511)
packages/canvas/container/src/container.ts (1)
  • dragState (101-103)
packages/multi-person-collaboration/test/utils/index.spec.ts (1)
packages/multi-person-collaboration/src/utils/index.ts (3)
  • toYjs (12-59)
  • fromYjs (62-78)
  • sanitizeSchema (89-128)
packages/multi-person-collaboration/src/composables/useYjs.ts (3)
packages/multi-person-collaboration/src/services/providerManager.ts (2)
  • YjsProvider (4-4)
  • ProviderManager (16-83)
packages/multi-person-collaboration/src/index.ts (1)
  • useYjs (5-5)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/plugins/versioncontrol/test/component/VersionTagCreate.spec.js (2)
packages/plugins/versioncontrol/test/component/CommitsContainer.spec.js (1)
  • commitsMock (9-36)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/multi-person-collaboration/test/composables/useCollabMonaco.spec.ts (2)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)
  • useCollabMonaco (14-80)
packages/multi-person-collaboration/test/composables/useYjs.spec.ts (1)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)
  • useYjs (25-92)
packages/plugins/versioncontrol/test/component/VersionBranchCreate.spec.js (1)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/multi-person-collaboration/test/services/docManager.spec.ts (1)
packages/multi-person-collaboration/src/services/docManager.ts (1)
  • DocManager (8-52)
packages/canvas/container/src/container.ts (2)
packages/register/src/hooks.ts (1)
  • useRealtimeCollab (96-96)
packages/multi-person-collaboration/src/type.ts (1)
  • POSITION (62-70)
packages/multi-person-collaboration/src/utils/index.ts (1)
packages/vue-generator/src/templates/vue-template/index.js (1)
  • value (30-30)
🔇 Additional comments (10)
packages/canvas/container/src/container.ts (1)

366-371: Still map inner insert position before broadcasting.

When insertNode is called with POSITION.IN (the common path when dropping into an existing container), we invoke insertInner(node) without a second argument. That means position defaults to '', and we now broadcast '' to insertSharedNode. Remote peers can’t determine whether to prepend or append the child, so their local ordering drifts. The earlier review you received flagged this exact issue—please reuse the same mapping logic you pass into operateNode.

-  useRealtimeCollab().insertSharedNode({ node, parent: node, data }, position)
+  const collabPosition = ([POSITION.TOP, POSITION.LEFT] as string[]).includes(position)
+    ? POSITION.TOP
+    : POSITION.BOTTOM
+  useRealtimeCollab().insertSharedNode?.({ node, parent: node, data }, collabPosition)
packages/multi-person-collaboration/src/utils/index.ts (1)

1-77: Use a non-colliding sentinel for undefined.
Line 3’s string sentinel clashes with legitimate "__undefined__" values, so fromYjs silently corrupts data. Switch to an object token and detect it by shape (Line 74) to keep real strings intact.

-const UNDEFINED_PLACEHOLDER = '__undefined__'
+const UNDEFINED_PLACEHOLDER = Object.freeze({ __tiny_yjs_undefined__: true }) as const
+
+function isUndefinedToken(value: unknown): value is typeof UNDEFINED_PLACEHOLDER {
+  return Boolean(value && typeof value === 'object' && (value as any).__tiny_yjs_undefined__ === true)
+}
@@
-  } else if (value === UNDEFINED_PLACEHOLDER) {
+  } else if (isUndefinedToken(value)) {
     return undefined // 还原 undefined
packages/multi-person-collaboration/src/composables/useCollabMonaco.ts (1)

15-18: Stop hard-coding the collaboration websocket to localhost

With the URL fixed to ws://localhost:${PORT}, any deployment that isn’t running the frontend and Yjs server on the same developer workstation will silently fail to connect. Please thread an optional websocketUrl through the options (or at least derive a sane default from window.location, upgrading to wss when needed) so production/staging environments work.

 interface UseCollabMonacoOptions {
   currentUser: UserAwareness
   editorRef: any
   roomId: string
   fieldName: string
+  websocketUrl?: string
 }
 
 export function useCollabMonaco(options: UseCollabMonacoOptions) {
-  const { currentUser, editorRef, roomId, fieldName } = options
-  const { ydoc, awareness, provider } = useYjs(roomId, {
-    websocketUrl: `ws://localhost:${PORT}`
-  })
+  const { currentUser, editorRef, roomId, fieldName, websocketUrl } = options
+  const isBrowser = typeof window !== 'undefined'
+  const protocol = isBrowser && window.location.protocol === 'https:' ? 'wss' : 'ws'
+  const host = isBrowser ? window.location.host : `localhost:${PORT}`
+  const { ydoc, awareness, provider } = useYjs(roomId, {
+    websocketUrl: websocketUrl ?? `${protocol}://${host}`
+  })
packages/multi-person-collaboration/src/composables/useCollabCursor.ts (2)

52-66: Preserve last-known cursor coords when toggling pressed state

Defaulting x/y to 0 means the shared cursor jumps to the top-left whenever mouseDownHandler fires before the first move (or when the cursor state was cleared). Pass the event through and reuse the last known coordinates instead of clobbering them.

-  const updateCursorPressedState = (pressed: boolean) => {
+  const updateCursorPressedState = (pressed: boolean, event?: MouseEvent) => {
     const localState = awareness.value?.getLocalState() as CursorAwarenessState | undefined
 
-    const currentX = localState?.cursor?.x || 0
-    const currentY = localState?.cursor?.y || 0
+    const currentX = localState?.cursor?.x ?? event?.pageX
+    const currentY = localState?.cursor?.y ?? event?.pageY
+    if (currentX == null || currentY == null) return
 
     updateLocalStateField('cursor', {
       x: currentX,
       y: currentY,
       pressed
     })
   }
 
-  const mouseDownHandler = () => updateCursorPressedState(true)
+  const mouseDownHandler = (event: MouseEvent) => updateCursorPressedState(true, event)
   const mouseUpHandler = () => updateCursorPressedState(false)

43-49: Fix primary-button detection for cursor awareness

MouseEvent.buttons is a bitmask. Equality to 1 fails whenever another button is depressed (e.g. trackpads, stylus side-buttons), so remote clients never see “pressed”. Use a bitwise mask instead.

   const updateCursorPositioin = (event: MouseEvent) => {
     updateLocalStateField('cursor', {
       x: event.pageX,
       y: event.pageY,
-      pressed: event.buttons === 1
+      pressed: (event.buttons & 1) === 1
     })
   }
packages/plugins/versioncontrol/test/component/TimelineContainer.spec.js (1)

1-63: Looks good

The timeline interactions and emitted events are covered thoroughly, and the expectations line up with the component API. 👍

packages/plugins/versioncontrol/test/component/CommitsContainer.spec.js (1)

1-115: Solid coverage

This suite exercises the major user flows (selection, actions, sorting, pagination, empty state) and validates the emitted events. Nicely done.

packages/multi-person-collaboration/test/models/AwarenessStateModel.spec.ts (1)

72-76: LGTM: local state update behavior

The test captures the “ignore local updated” path through AwarenessStateModel and ensures only local state is mutated with no change event emitted.

packages/multi-person-collaboration/test/utils/index.spec.ts (1)

104-120: Great negative-path coverage

Type-mismatch error expectations align exactly with the implementation’s error messages.

packages/multi-person-collaboration/test/composables/useCollabSchema.spec.ts (1)

71-83: LGTM: initialization and SchemaManager integration

The test asserts returned API shape and verifies createSchema(roomId, provider) wiring as expected.

Comment thread packages/multi-person-collaboration/src/composables/useYjs.ts
Comment thread packages/multi-person-collaboration/test/composables/useYjs.spec.ts
Comment thread packages/plugins/versioncontrol/package.json
Comment thread packages/plugins/versioncontrol/test/component/VersionCommitCreate.spec.js Outdated

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (26)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)

37-37: Filename typo: rename to useVersionControlData.ts.

The file is named uesVersionControlData.ts (missing the second "s"), but the exported composable is useVersionControlData. Rename the file to match the export for consistency and to avoid import confusion.

Rename:

packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts
→
packages/plugins/versioncontrol/src/composable/useVersionControlData.ts

Update the import in Main.vue around line 151 accordingly.

packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (1)

176-731: CSS duplication already flagged in previous review.

The extensive style duplication with VersionCommitInfo.vue has already been identified in previous reviews and should be addressed by extracting shared styles to a common LESS file.

packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (5)

70-96: Null safety and typo issues already flagged in previous review.

The compareCommit function's issues with the brnachId typo, missing null checks for commit.branches[0], and cascading null reference errors have been comprehensively covered in previous review comments.


110-129: Error handling issues already flagged in previous review.

The missing async error handling in revertToCommit and the optimistic success notification have been comprehensively addressed in previous review comments.


131-134: Non-null assertion issue already flagged in previous review.

The unsafe non-null assertion on selectedCommit.value! has been addressed in previous review comments.


136-139: Empty array access issue already flagged in previous review.

The unsafe access to commits.value[0].hash without checking for an empty array has been addressed in previous review comments.


161-169: Ref value check and alert() usage already flagged in previous review.

The incorrect ref check on line 162 and the use of alert() for user notifications have been comprehensively addressed in previous review comments.

packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (2)

44-53: Type mismatch: changedFiles initialization doesn't match CommitStats interface.

Line 49 initializes changedFiles as { path: string; oldValue: any; newValue: any }[], but the CommitStats interface in type.ts declares it as any[]. This was flagged in the review of type.ts (lines 58-62).

Once the CommitStats interface is updated in type.ts, this code will be correctly typed.


71-123: Verify jsondiffpatch opcode handling and metadata key filtering.

Past review comments identified two critical issues in this delta traversal logic that need to be addressed:

  1. Missing metadata key filtering: The _t marker and array-deletion keys (prefixed with _) are not being filtered, polluting the changedFiles paths.

  2. Inverted opcode interpretation: The handling of value[2] opcodes is incorrect:

    • [diff, 0, 2] should be text diff (currently treated as deletion)
    • [oldValue, 0, 0] should be deletion (currently treated as text diff)

Please refer to the previous review comments for detailed fixes to:

  • Skip key === '_t' and normalize keys starting with _
  • Swap the conditional checks for value[2] === 0 and value[2] === 2
packages/canvas/container/src/CanvasContainer.vue (6)

3-3: Fix unstable VDOM key for remote selections.

Remote selections may not have a stable state.id, leading to duplicate or undefined keys and poor VDOM diffing performance.

Consider using a composite key that includes the selection ID or user ID:

-  <div v-for="state in allMultiSelectedStates" :key="state.id">
+  <div v-for="state in allMultiSelectedStates" :key="state.selection?.id || `remote-${state.user?.id}`">

155-161: Externalize hard-coded user configuration.

The currentUser object contains hard-coded user data including PII (email) and test avatars that should not be committed to production code.

Extract this into a prop, config service, or environment variable:

-    const currentUser = {
-      id: 'user-2',
-      name: 'Bob',
-      color: '#1296db',
-      email: 'opentiny@tiny-engine',
-      avatarUrl: 'https://avatars.githubusercontent.com/u/3?v=4'
-    }
+    const currentUser = computed(() => props.currentUser || useCollabConfig().getCurrentUser())

222-238: Throttle scroll-triggered rect recalculation.

The syncRemoteNode function recalculates DOM rects on every scroll event, which can be expensive and cause performance issues.

Throttle using requestAnimationFrame:

+    let syncRaf = 0
     const syncRemoteNode = () => {
+      cancelAnimationFrame(syncRaf)
+      syncRaf = requestAnimationFrame(() => {
         syncRemoteStatesSelections.value = syncRemoteStatesSelections.value
           .map((selState) => {
             const element = querySelectById(selState.selection.id)
             if (!element) return null
             const { top, left, width, height } = getRect(element)
             return {
               ...selState,
               top,
               left,
               width,
               height
             }
           })
           .filter(Boolean)
+      })
     }

372-375: Fix typo in cursor API and pass event to mouseDownHandler.

The method name updateCursorPositioin contains a typo (should be updateCursorPosition), and mouseDownHandler is called without passing the event parameter.

Apply these fixes:

-        const { updateCursorPositioin, mouseUpHandler, mouseDownHandler, updateCursorPageId } = useCollabCursor({
+        const { updateCursorPosition, mouseUpHandler, mouseDownHandler, updateCursorPageId } = useCollabCursor({
           roomId: 'cursor-yjs',
           currentUser
         })

Also update the calls on lines 379 and 412:

-          mouseDownHandler()
+          mouseDownHandler(event)
-            updateCursorPositioin(ev)
+            updateCursorPosition(ev)

509-524: Fix duplicate identifier: remoteStates declared twice.

The variable remoteStates is declared as ref({}) on line 167 and then redeclared via destructuring on line 520, causing a compile-time error.

Rename the destructured property:

         const {
           insertSharedNode,
           deleteSharedNode,
           updateUserSelection,
           updateUserPage,
           moveDownSharedNode,
           moveUpSharedNode,
           updateStyleNode,
           updatePropsNode,
           updateMethodNode,
           updateAttributesNode,
-          remoteStates
+          remoteStates: awarenessRemoteStates
         } = useCollabSchema({
           roomId: 'schema-yjs',
           currentUser
         })

Then update line 538:

-          remoteStates
+          remoteStates: awarenessRemoteStates

177-193: Add null-safety guard for selection parameter.

While the function signature now accepts both state and selection parameters, there's no null-check before accessing selection.id on line 179.

Apply this guard:

 const mapStateToSelection = (state, selection) => {
+  if (!selection?.id) return null
   const element = querySelectById(selection.id)
   if (!element) return null
packages/collab-ui/avatar/src/Main.vue (3)

72-78: Hard-coded currentUser must be fixed before production.

This past review comment remains unaddressed. Hard-coding "Bob" will leak demo data in production and prevent integration with real authentication.

As noted in the previous review, accept currentUser as a prop with a fallback to the demo object. The component should receive the authenticated user from parent context.


124-124: Clear notificationTimer on unmount to prevent post‑unmount writes.

notificationTimer is never cleared in onUnmounted, so if the component unmounts while a timer is pending, processNotificationBuffer will mutate notifications.value after the component is destroyed, causing memory leaks and potential errors.

This issue was flagged in a past review but remains unaddressed. Apply this diff:

     onUnmounted(() => {
       window.removeEventListener('popstate', updateSearch)
+      if (notificationTimer) {
+        clearTimeout(notificationTimer)
+        notificationTimer = null
+      }
     })

Also applies to: 204-206, 232-234


182-209: Watch configuration is contradictory; remove { deep: true }.

The spread operator { ...collabState.remoteCursors } creates a shallow copy that produces a new object reference when keys (client IDs) are added/removed, correctly triggering the watch. However, { deep: true } at line 208 still enables deep watching of nested cursor properties (position, etc.), causing the watcher to fire on every cursor movement—exactly what the past review warned against.

Apply this diff to properly implement shallow watching:

     watch(
       () => ({ ...collabState.remoteCursors }),
       (newStates, oldStates) => {
         // ... existing logic ...
       },
-      { deep: true }
+      { deep: false }
     )

The shallow copy already handles membership changes; deep watching is unnecessary and harmful.

packages/multi-person-collaboration/src/composables/useCollabSchema.ts (3)

58-61: Guard provider initialization before use.

provider.value is asserted (!) but can be null on failed init; fail fast with a clear error. Previously noted.

-  const schemaModel = schemaManager.createSchema(roomId, provider.value!)
+  if (!provider.value) {
+    throw new Error(`[useCollabSchema] Failed to initialize provider for room ${roomId}`)
+  }
+  const schemaModel = schemaManager.createSchema(roomId, provider.value!)

17-20: Stop hard-coding ws://localhost; add override and derive ws/wss from location.

Make websocket configurable and production-safe (HTTPS/mixed-content). Reuse defaults when window is unavailable. This was flagged earlier.

 interface UseCollabSchemaOptions {
   roomId: string // 协同文档的房间 ID
   currentUser: UserAwareness // 当前用户信息,用于 Awareness
+  websocketUrl?: string // 可选覆盖 WebSocket 地址
 }
@@
-  const { awareness, provider } = useYjs(roomId, { websocketUrl: `ws://localhost:${PORT}` })
+  const isBrowser = typeof window !== 'undefined'
+  const protocol = isBrowser && window.location.protocol === 'https:' ? 'wss' : 'ws'
+  const host = isBrowser ? window.location.host : `localhost:${PORT}`
+  const wsUrl = options.websocketUrl ?? `${protocol}://${host}`
+  const { awareness, provider } = useYjs(roomId, { websocketUrl: wsUrl })

Also applies to: 55-56


119-127: Fix event listener leak: off() uses a different function reference.

Define a stable onSync handler; remove the same reference on unmount. Previously flagged.

-  provider.value!.on('sync', (isSynced: boolean) => {
-    if (isSynced) {
-      // eslint-disable-next-line no-console
-      console.log(`[schema-yjs] Yjs 同步完成,重建映射`)
-      const pageSchema = toRaw(useCanvas().getPageSchema())
-      schemaModel.operationHandler.rebuildYNodeMap(pageSchema as RootNode)
-    }
-  })
+  const onSync = (isSynced: boolean) => {
+    if (!isSynced) return
+    // eslint-disable-next-line no-console
+    console.log(`[schema-yjs] Yjs 同步完成,重建映射`)
+    const pageSchema = toRaw(useCanvas().getPageSchema())
+    schemaModel.operationHandler.rebuildYNodeMap(pageSchema as RootNode)
+  }
+  provider.value!.on('sync', onSync)
@@
   onUnmounted(() => {
     schemaManager.destroyObserver(roomId)
-    provider.value?.off('sync', () => {})
+    provider.value?.off('sync', onSync)
     // awareness.value?.destroy()
   })

Also applies to: 130-134

packages/collab-ui/cursor/src/Main.vue (2)

51-71: Don’t hard‑code currentUser/roomId; accept as props and pass through.

Improves deployability and testability. Previously suggested.

 export default {
   name: 'Cursor',
-  props: {
-    iframe: {
-      type: Object,
-      default: () => {}
-    }
-  },
-  setup() {
-    const currentUser = {
-      id: 'user-2',
-      name: 'Bob',
-      color: '#1296db'
-    }
+  props: {
+    iframe: { type: Object, default: () => ({}) },
+    currentUser: { type: Object, required: true },
+    roomId: { type: String, default: 'cursor-yjs' }
+  },
+  setup(props) {
@@
-    const collabState = reactive(
-      useCollabCursor({
-        roomId: 'cursor-yjs',
-        currentUser
-      })
-    )
+    const collabState = reactive(
+      useCollabCursor({
+        roomId: props.roomId,
+        currentUser: props.currentUser
+      })
+    )

78-121: Guard missing/invalid cursor state to prevent NaN/undefined transforms.

Filter out entries without valid numeric x/y before mapping; avoids runtime style errors.

-    const processedCursors = computed(() => {
-      return Object.entries(collabState.remoteCursors).map(([clientId, state]) => {
-        if (!state.cursor) return { clientId, state, position: {} }
+    const processedCursors = computed(() => {
+      return Object.entries(collabState.remoteCursors)
+        .filter(([, state]) => state?.cursor && Number.isFinite(state.cursor.x) && Number.isFinite(state.cursor.y))
+        .map(([clientId, state]) => {
           const { x: pageX, y: pageY } = state.cursor
@@
-        return { clientId, state, position }
-      })
+        return { clientId, state, position }
+      })
     })
packages/multi-person-collaboration/src/composables/useCollabCursor.ts (3)

43-50: Fix API typo and primary-button detection.

Rename updateCursorPositioin → updateCursorPosition; use bitwise check for MouseEvent.buttons.

-  // 更新光标位置
-  const updateCursorPositioin = (event: MouseEvent) => {
+  // 更新光标位置
+  const updateCursorPosition = (event: MouseEvent) => {
     updateLocalStateField('cursor', {
       x: event.pageX,
       y: event.pageY,
-      pressed: event.buttons === 1
+      pressed: (event.buttons & 1) === 1
     })
   }

Also update the returned API name below.


52-71: Avoid jumping to (0,0) on mousedown; pass event to preserve coords.

Use last known coords or the event position; update handler signature to accept MouseEvent.

-  // 更新本地光标按下状态的方法
-  const updateCursorPressedState = (pressed: boolean) => {
+  // 更新本地光标按下状态的方法
+  const updateCursorPressedState = (pressed: boolean, ev?: MouseEvent) => {
     const localState = awareness.value?.getLocalState() as CursorAwarenessState | undefined
-
-    const currentX = localState?.cursor?.x || 0
-    const currentY = localState?.cursor?.y || 0
+    const currentX = localState?.cursor?.x ?? ev?.pageX
+    const currentY = localState?.cursor?.y ?? ev?.pageY
+    if (currentX == null || currentY == null) {
+      return // no position yet; wait for a move event
+    }
 
     updateLocalStateField('cursor', {
       x: currentX,
       y: currentY,
       pressed
     })
   }
 
-  const mouseDownHandler = () => updateCursorPressedState(true)
+  const mouseDownHandler = (ev: MouseEvent) => updateCursorPressedState(true, ev)
   const mouseUpHandler = () => updateCursorPressedState(false)
@@
   return {
     remoteCursors: remoteStates,
-    updateCursorPositioin,
+    updateCursorPosition,
     mouseDownHandler,
     mouseUpHandler,
     updateCursorPageId
   }

17-20: Make websocket URL configurable and HTTPS‑aware (no localhost pin).

Add websocketUrl to options; derive ws/wss from window.location with SSR guard. Previously noted.

 interface UserCollabCursorOptions {
   roomId: string
   currentUser: UserAwareness
+  websocketUrl?: string
 }
@@
-export function useCollabCursor(options: UserCollabCursorOptions) {
-  const { roomId, currentUser } = options
-  const { awareness } = useYjs(roomId, { websocketUrl: `ws://localhost:${PORT}` })
+export function useCollabCursor(options: UserCollabCursorOptions) {
+  const { roomId, currentUser, websocketUrl } = options
+  const isBrowser = typeof window !== 'undefined'
+  const protocol = isBrowser && window.location.protocol === 'https:' ? 'wss' : 'ws'
+  const host = isBrowser ? window.location.host : `localhost:${PORT}`
+  const { awareness } = useYjs(roomId, { websocketUrl: websocketUrl ?? `${protocol}://${host}` })

Also applies to: 30-33

🧹 Nitpick comments (15)
packages/toolbars/collaboration/src/Main.vue (1)

93-95: Remove unused dead code.

The isSingle function always returns true and is never invoked in the template or elsewhere in the component.

Apply this diff to remove the unused function:

    })

-   const isSingle = () => {
-     return true
-   }
-
    const openVersionControl = () => {
      const { PLUGIN_NAME, activePlugin } = useLayout()
      activePlugin(PLUGIN_NAME.VersionControl).then(() => {})
    }

    return {
      state,
-     isSingle,
      openVersionControl
    }
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (3)

22-26: Consider unknown instead of any for diff values.

The newValue and oldValue fields are typed as any, which bypasses type safety. Since diff values can represent various types, consider using unknown to enforce explicit type checking at consumption sites.

 export interface DiffData {
   path: string
-  newValue: any
-  oldValue: any
+  newValue: unknown
+  oldValue: unknown
 }

42-42: Provide proper typing for availableBranches.

The availableBranches ref is typed as any, which removes type safety. Define an explicit type (e.g., Ref<string[]> or a proper branch interface) based on the actual branch data structure.

Example:

-const availableBranches: Ref<any> = ref([])
+const availableBranches: Ref<string[]> = ref([])

174-182: Consider user-facing error feedback.

Currently, errors during commit fetching are only logged to the console. Consider exposing an error state (e.g., error: Ref<Error | null>) that parent components can use to display user feedback.

Example:

const error: Ref<Error | null> = ref(null)

onMounted(async () => {
  try {
    const rawCommits = await versionManager.commitRepository.findAll()
    commits.value = await Promise.all(rawCommits.map(transformCommit))
  } catch (err) {
    error.value = err instanceof Error ? err : new Error(String(err))
    console.error('转换 commits 出错:', err)
  }
})

return {
  // ... existing returns
  error
}
packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (1)

107-164: Extract formatDiffValue logic into smaller, testable functions.

The formatDiffValue function handles multiple responsibilities (null/empty checks, URL decoding, JSON parsing, diff format detection) in a single 57-line function with nested try-catch blocks. This increases cognitive complexity and makes testing difficult.

Consider decomposing into helper functions:

const isEmptyValue = (data: any): boolean => {
  return data === null || data === undefined || data === '' || 
    (typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length === 0)
}

const tryDecodeURIComponent = (str: string): string => {
  try {
    return decodeURIComponent(str)
  } catch {
    return str
  }
}

const tryParseJSON = (str: string): string | null => {
  try {
    const parsed = JSON.parse(str)
    return JSON.stringify(parsed, null, 2)
  } catch {
    return null
  }
}

const formatDiffValue = (data: any, type: 'add' | 'del'): string => {
  // Handle empty values
  if (isEmptyValue(data)) {
    return type === 'add' ? '空值(null)' : '空对象(null)'
  }
  
  // Handle zero
  if (data === 0) {
    return type === 'add' ? '新增空对象' : '删除标记 (0)'
  }
  
  // Handle strings
  if (typeof data === 'string') {
    const str = data.trim()
    
    // Diff format
    if (str.startsWith('@@')) {
      return tryDecodeURIComponent(str)
    }
    
    // JSON
    const firstChar = str[0]
    if (firstChar === '{' || firstChar === '[') {
      const parsed = tryParseJSON(str)
      if (parsed) return parsed
    }
    
    // URL encoded
    if (/%[0-9A-Fa-f]{2}/.test(str)) {
      return tryDecodeURIComponent(str)
    }
    
    return str
  }
  
  // Default: stringify objects
  return JSON.stringify(data, null, 2)
}

This approach improves testability, readability, and maintainability.

packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1)

171-179: Consider documenting mock implementation or replacing with real data fetching.

The loadMore function uses setTimeout to simulate loading delay without fetching actual data. While this is acceptable for prototyping, consider:

  1. Adding a comment indicating this is mock/placeholder code
  2. Planning the real implementation that fetches additional commits from the backend
  3. Handling potential errors during data fetching

Example with documentation:

const loadMore = () => {
  if (!isLoading.value) {
    isLoading.value = true
    // TODO: Replace with actual backend call
    // Example: await versionManager.commitRepository.findByPage(currentPage.value + 1)
    setTimeout(() => {
      currentPage.value++
      isLoading.value = false
    }, 500) // Mock delay
  }
}
packages/plugins/versioncontrol/src/js/shared/type.ts (4)

45-53: Consider making diff optional to match the documented behavior.

The comment states "如果无差异则为 undefined" (undefined if no difference), but the diff field is not marked as optional.

Apply this diff if the field should be optional:

 export interface DiffResult {
-  diff: jsonDiffPatch.Delta // 差异对象,如果无差异则为 undefined
+  diff?: jsonDiffPatch.Delta // 差异对象,如果无差异则为 undefined
 }

384-395: Consider making the props type more flexible.

The props field has a highly specific type constraint Record<string, any> & { columns?: { slots?: Record<string, any> }[] } that hardcodes the columns structure. This may be too restrictive for a generic node interface.

Consider simplifying to:

 export interface Node {
   id: string
   componentName: string
-  props: Record<string, any> & { columns?: { slots?: Record<string, any> }[] }
+  props: Record<string, any>
   children?: Node[]
   componentType?: 'Block' | 'PageStart' | 'PageSection'
   slots?: string | Record<string, any>
   params?: string[]
   loop?: Record<string, any>
   loopArgs?: string[]
   conditions?: boolean | Record<string, any>
 }

400-412: Replace any type for schema field with a more specific type.

The schema field is typed as any, which loses type safety. Based on the comment "递归引用自身或其他Schema类型", this should reference PageSchema or RootNode.

Apply this diff:

 export type RootNode = Omit<Node, 'id'> & {
   id?: string
   css?: string
   fileName?: string
   methods?: Record<string, any>
   state?: Record<string, any>
   lifeCycles?: Record<string, any>
   dataSource?: any
   bridge?: any
   inputs?: any[]
   outputs?: any[]
-  schema?: any // 递归引用自身或其他Schema类型
+  schema?: PageSchema // 递归引用自身或其他Schema类型
 }

431-448: Inconsistent use of any vs unknown types.

The interface mixes unknown (lines 432, 434, 441) and any (lines 433, 435, 446) types inconsistently. Use unknown for better type safety when the type is truly unknown, and reserve any only when necessary for compatibility.

Consider standardizing on unknown for type-safe handling:

 export interface PageState {
   currentVm?: unknown
-  currentSchema?: { [x: string]: any; id: string }
+  currentSchema?: { [x: string]: unknown; id: string }
   currentType?: unknown
-  currentPage?: { [x: string]: any; id: string; name: string } | null
+  currentPage?: { [x: string]: unknown; id: string; name: string } | null
   currentPageId?: string
   currentPageName?: string
   hoverVm?: unknown
   pageSchema: RootNode | null
   properties?: unknown
   dataSource?: unknown
   dataSourceMap?: unknown
   isSaved: boolean
   isLock: boolean
   isBlock: boolean
-  nodesStatus: Record<string, any>
+  nodesStatus: Record<string, unknown>
   loading: boolean
 }
packages/canvas/container/src/CanvasContainer.vue (1)

507-507: Consider combining scroll handlers for efficiency.

Two separate scroll listeners are registered (lines 506-507). While both are necessary, consider combining them into a single throttled handler to reduce event overhead.

Example approach:

const handleScroll = () => {
  syncNodeScroll()
  syncRemoteNode()
}
win.addEventListener('scroll', handleScroll, true)

This should be combined with the rAF throttling suggested earlier for syncRemoteNode.

packages/collab-ui/avatar/src/Main.vue (2)

94-116: Clarify the inverted length comparison logic.

The ternary at line 115 returns the shorter list when lengths differ, which seems counterintuitive. The comment "don't change the judgment direction" suggests this was previously buggy and reversed, but lacks explanation.

Add a comment explaining why the shorter list is preferred, or refactor to make the intent explicit:

-      // 逻辑保持一致(不要改动判断方向)
-      return remoteUserAndSelection.length > remoteUser.length ? remoteUser : remoteUserAndSelection
+      // Prefer remoteUser (local cursor data) unless remoteUserAndSelection (with page filter) is smaller/more specific
+      // This handles cases where realtime collab state may include stale entries
+      const preferLocal = remoteUserAndSelection.length > remoteUser.length
+      return preferLocal ? remoteUser : remoteUserAndSelection

If the logic is indeed correct, document the reasoning so future maintainers don't "fix" it.


248-251: Extremely high z-index may cause layering conflicts.

z-index: 100000 is very high and may conflict with other overlays (modals, dropdowns, toasts). Consider using a CSS custom property or design-system z-index scale to manage layering consistently.

packages/multi-person-collaboration/src/composables/useCollabSchema.ts (1)

105-108: Type the selection update API.

Narrow any to SchemaAwarenessState['selection'] for safety and DX.

-  const updateUserSelection = (selectedNode: any) => {
-    updateLocalStateField('selection', selectedNode)
+  const updateUserSelection = (selected: SchemaAwarenessState['selection']) => {
+    updateLocalStateField('selection', selected)
   }
packages/collab-ui/cursor/src/Main.vue (1)

54-58: iframe prop appears unused.

Either wire it (e.g., coordinate transforms for iframe) or remove to reduce API surface.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d94f8e and ccb2ff1.

⛔ Files ignored due to path filters (3)
  • mockServer/src/database/branch.db is excluded by !**/*.db
  • mockServer/src/database/commit.db is excluded by !**/*.db
  • mockServer/src/database/pages.db is excluded by !**/*.db
📒 Files selected for processing (12)
  • packages/canvas/container/src/CanvasContainer.vue (11 hunks)
  • packages/collab-ui/avatar/src/Main.vue (1 hunks)
  • packages/collab-ui/cursor/src/Main.vue (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabCursor.ts (1 hunks)
  • packages/multi-person-collaboration/src/composables/useCollabSchema.ts (1 hunks)
  • packages/plugins/page/src/composable/usePage.ts (2 hunks)
  • packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (1 hunks)
  • packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (1 hunks)
  • packages/plugins/versioncontrol/src/js/shared/type.ts (1 hunks)
  • packages/toolbars/collaboration/src/Main.vue (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
packages/plugins/page/src/composable/usePage.ts (1)
packages/register/src/hooks.ts (1)
  • useRealtimeCollab (96-96)
packages/multi-person-collaboration/src/composables/useCollabSchema.ts (6)
packages/multi-person-collaboration/src/type.ts (7)
  • UserAwareness (118-123)
  • Node (23-34)
  • RootNode (36-48)
  • PositionType (72-72)
  • POSITION (62-70)
  • UpdateMethodsOperation (105-108)
  • UpdateAttributesOperation (110-114)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)
  • useYjs (25-92)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-58)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • updateLocalStateField (31-33)
packages/register/src/hooks.ts (1)
  • useCanvas (79-79)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (2)
packages/plugins/versioncontrol/src/composable/useUtils.ts (1)
  • useUtils (5-51)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (1)
packages/plugins/versioncontrol/src/js/shared/type.ts (2)
  • PageSchema (417-417)
  • CommitStats (58-62)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (4)
packages/plugins/versioncontrol/src/composable/useUtils.ts (1)
  • useUtils (5-51)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)
  • DisplayCommit (6-20)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/register/src/hooks.ts (1)
  • useNotify (94-94)
packages/multi-person-collaboration/src/composables/useCollabCursor.ts (5)
packages/multi-person-collaboration/src/type.ts (1)
  • UserAwareness (118-123)
packages/multi-person-collaboration/src/composables/useYjs.ts (1)
  • useYjs (25-92)
packages/multi-person-collaboration/src/config/index.ts (1)
  • PORT (2-2)
packages/multi-person-collaboration/src/composables/useAwareness.ts (1)
  • useAwareness (10-58)
packages/multi-person-collaboration/src/models/AwarenessStateModel.ts (1)
  • updateLocalStateField (31-33)
packages/plugins/versioncontrol/src/js/shared/type.ts (2)
packages/plugins/versioncontrol/src/js/domain/models/Commit.ts (1)
  • Commit (7-179)
packages/plugins/versioncontrol/src/js/domain/models/Branch.ts (1)
  • Branch (7-325)
🔇 Additional comments (12)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (3)

86-162: Filtering and sorting logic looks good.

The computed uniqueAuthors and filteredCommits implementations are sound. Date filtering and sorting use Date.parse() and new Date() which work correctly with the ISO-formatted date strings produced by transformCommit in useUtils.ts (line 30: new Date(json.timestamp).toISOString()).


164-172: Pagination logic correctly implements infinite scroll.

The paginatedCommits and hasMore computed properties properly implement a "load more" pattern where currentPage increments to show progressively more items from the start.


184-212: Return statement is comprehensive.

All reactive state, computed properties, and dialog controls are properly exposed, providing a complete interface for consuming components.

packages/plugins/versioncontrol/src/js/domain/strategies/SchemaStatsCalculator.ts (1)

12-36: Well-configured jsondiffpatch instance.

The DiffPatcher configuration is appropriate for schema comparison:

  • Smart objectHash using id/componentName fallback
  • Array move detection enabled
  • Text diff with reasonable minLength threshold
  • Property filter to ignore internal fields
  • Performance optimization via cloneDiffValues: false
packages/canvas/container/src/CanvasContainer.vue (6)

64-108: LGTM - Clean import additions.

The collaboration imports are well-organized and necessary for the multiplayer features.


205-220: LGTM - Robust remote selection filtering.

The implementation correctly filters remote states by page ID and safely handles missing selections with filter(Boolean).


526-539: Hook initialization looks correct pending remoteStates fix.

The initHook call properly registers all collaboration handlers. However, ensure the remoteStates duplicate identifier issue (line 520) is resolved first.


586-600: LGTM - Comprehensive URL change tracking.

The implementation correctly handles both popstate events and programmatic history API calls to track route changes.


612-638: LGTM - Well-structured reactive collaboration state.

The watchers correctly initialize collaboration state after isReady, sync remote selections, and handle route changes to update the current page ID.


640-671: LGTM - Clean public API exposure.

The return statement properly exposes all collaboration-related state and components for external access.

packages/plugins/page/src/composable/usePage.ts (1)

25-26: LGTM!

The import addition is necessary for the real-time collaboration functionality added below.

packages/multi-person-collaboration/src/composables/useCollabCursor.ts (1)

73-80: Only the typo needs correction; mouseDownHandler guidance is incorrect.

The updateCursorPositioin typo must be fixed, but the review comment's guidance about mouseDownHandler is incorrect. The handler implementation accepts no parameters (const mouseDownHandler = () => updateCursorPressedState(true)) and is correctly called without arguments throughout the codebase. No changes are needed to how it's invoked.

Likely an incorrect or invalid review comment.

Comment thread packages/collab-ui/avatar/src/Main.vue
Comment thread packages/collab-ui/avatar/src/Main.vue
Comment thread packages/collab-ui/avatar/src/Main.vue
Comment thread packages/collab-ui/cursor/src/Main.vue
Comment thread packages/plugins/page/src/composable/usePage.ts
Comment thread packages/plugins/versioncontrol/src/js/shared/type.ts
Comment thread packages/toolbars/collaboration/src/Main.vue
Comment thread packages/toolbars/collaboration/src/Main.vue

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (6)
packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (2)

176-731: Extract shared dialog styles to a common LESS to reduce duplication.

Follow the earlier suggestion to move repeated overlay/content/header/form styles into a shared file (e.g., styles/dialog-shared.less) and import it here and in VersionCommitInfo.vue.


23-23: Guard branches access with optional chaining to avoid runtime errors.

Accessing branches[0] without guards will throw when base/target or branches is missing.

Apply:

-              <span class="version-info"> 分支 - {{ modelCompareData.base.branches[0] || '未知' }} </span>
+              <span class="version-info"> 分支 - {{ modelCompareData.base?.branches?.[0] || '未知' }} </span>
-              <span class="version-info"> 分支 - {{ modelCompareData.target.branches[0] || '未知' }} </span>
+              <span class="version-info"> 分支 - {{ modelCompareData.target?.branches?.[0] || '未知' }} </span>

Also applies to: 30-30

packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (4)

70-80: Fix typo and guard branch/base resolution to prevent undefined lookups.

brnachId typo and unchecked branches[0] cause runtime errors; baseCommitId may be undefined.

-  const compareCommit = async (commit: DisplayCommit) => {
+  const compareCommit = async (commit: DisplayCommit) => {
     // 比较 commit
-    const brnachId = commit.branches[0]
-    const baseBranch = await versionManager.branchRepository.findByName(brnachId)
+    const branchName = commit.branches?.[0]
+    const baseBranch = branchName
+      ? await versionManager.branchRepository.findByName(branchName)
+      : null
@@
-    const baseCommitId = baseBranch?.baseCommitId as string
-    const rawBaseCommit = await versionManager.commitRepository.findById(baseCommitId)
+    const baseCommitId = baseBranch?.baseCommitId as string | undefined
+    const rawBaseCommit = baseCommitId
+      ? await versionManager.commitRepository.findById(baseCommitId)
+      : null
@@
-    const baseCommit = await transformCommit(rawBaseCommit)
+    const baseCommit = rawBaseCommit ? await transformCommit(rawBaseCommit) : null

110-128: Add async error handling and avoid optimistic success in revertToCommit.

-      exec() {
-        versionManager.commitRepository.findById(commit.id).then((val) => {
-          useCanvas().importSchema(val.schema)
-
-          close()
-
-          useNotify({
-            type: 'success',
-            message: '版本回退成功!'
-          })
-        })
-      },
+      async exec() {
+        try {
+          const val = await versionManager.commitRepository.findById(commit.id)
+          if (!val?.schema) {
+            useNotify({ type: 'error', message: '未找到提交的 schema,回退失败。' })
+            return
+          }
+          await useCanvas().importSchema(val.schema)
+          close()
+          useNotify({ type: 'success', message: '版本回退成功!' })
+        } catch (e) {
+          useNotify({ type: 'error', message: '回退失败,请稍后重试。' })
+        }
+      },

140-143: Guard empty commit list when opening tag dialog.

-  const createTag = () => {
-    tagDialogVisible.value = true
-    tagTargetCommit.value = selectedCommit.value ? selectedCommit.value.hash : commits.value[0].hash // 默认当前选中或最新提交
-  }
+  const createTag = () => {
+    const fallback = commits.value?.[0]?.hash
+    if (selectedCommit.value?.hash || fallback) {
+      tagDialogVisible.value = true
+      tagTargetCommit.value = selectedCommit.value?.hash ?? fallback!
+    } else {
+      useNotify({ type: 'warning', message: '暂无可用提交用于创建标签' })
+    }
+  }

165-173: Check ref .value and replace alert with non-blocking notify in confirmCreateBranch.

-  const confirmCreateBranch = () => {
-    if (branchTargetCommit && newBranchName) {
-      alert(`已基于提交 ${branchTargetCommit.value} 创建新分支:${newBranchName.value} (模拟操作)`)
-      branches.value.push(newBranchName.value)
-      currentBranch.value = newBranchName.value
-      // 实际操作中会调用后端API创建分支
-    }
-    branchDialogVisible.value = false
-  }
+  const confirmCreateBranch = () => {
+    const name = newBranchName.value?.trim()
+    const target = branchTargetCommit.value
+    if (!name) {
+      useNotify({ type: 'warning', message: '分支名称不能为空' })
+      return
+    }
+    if (!target) {
+      useNotify({ type: 'warning', message: '缺少基准提交' })
+      return
+    }
+    // 实际操作中会调用后端API创建分支
+    branches.value.push(name)
+    currentBranch.value = name
+    branchDialogVisible.value = false
+    useNotify({ type: 'success', message: `已基于提交 ${target} 创建新分支:${name}` })
+  }
🧹 Nitpick comments (3)
packages/plugins/versioncontrol/test/component/VersionCommitCreate.spec.js (2)

56-64: Expand test coverage for submit flow.

The test suite only covers the cancel flow. According to the AI summary, the component should also test the submit flow (calling versionManager.commitAppService.createCommit and subsequent UI updates). Additionally, the comprehensive mocks set up at lines 6-22 are never verified in any test.

Consider adding test cases for:

  • Submit button flow that verifies createCommit is called with correct parameters
  • How availableBranches and commits props are used
  • Schema export functionality via useCanvas
  • Verification that mocked repository methods are called as expected

60-60: The cancel button selector is valid and exists in the component.

Verification confirms the button.cancel-button selector at line 60 matches the actual implementation in VersionCommitCreate.vue (line 27). The test will function correctly as written.

Optional: Consider using data-testid attributes for more resilient test selectors in future refactoring.

packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (1)

7-12: Add a11y and button semantics to the close control.

-        <button @click="closeCompareDialog" class="close-btn">
+        <button type="button" aria-label="关闭" @click="closeCompareDialog" class="close-btn">
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ccb2ff1 and 6eb3629.

📒 Files selected for processing (4)
  • packages/collab-ui/avatar/src/Main.vue (1 hunks)
  • packages/plugins/versioncontrol/src/components/VersionDiffDialog.vue (1 hunks)
  • packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionCommitCreate.spec.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/collab-ui/avatar/src/Main.vue
🧰 Additional context used
🧬 Code graph analysis (1)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (4)
packages/plugins/versioncontrol/src/composable/useUtils.ts (1)
  • useUtils (5-51)
packages/plugins/versioncontrol/src/composable/uesVersionControlData.ts (1)
  • DisplayCommit (6-20)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
packages/register/src/hooks.ts (1)
  • useNotify (94-94)
🔇 Additional comments (1)
packages/plugins/versioncontrol/src/composable/useVersionControlAction.ts (1)

131-138: Nice guard on null selectedCommit.

The check prevents non-null assertion risks and improves UX with a warning.

Comment thread packages/plugins/versioncontrol/test/component/VersionCommitCreate.spec.js Outdated

ghost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
packages/canvas/container/src/components/CanvasAction.vue (3)

616-616: Fix incorrect destructuring fallback logic.

The current destructuring props.selectState || props.selectState.selection will crash if selectState is falsy, because it attempts to access .selection on a falsy value. The fallback order is also incorrect—it should prefer selection over selectState.

Apply this diff:

-      const { left, top, width, height, doc } = props.selectState || props.selectState.selection
+      const base = props.selectState?.selection || props.selectState || {}
+      const { left, top, width, height, doc } = base

235-245: Guard realtime collaboration calls against undefined values.

Both moveUp and moveDown call realtime collab methods without verifying that parent and schema exist or that the collab API is available. This can throw exceptions during race conditions.

Apply this diff:

 const moveUp = () => {
   const { parent, schema } = getCurrent()
   moveChild(parent?.children, schema, -1)
-  useRealtimeCollab().moveUpSharedNode(parent.id, schema.id, 'up')
+  const collab = useRealtimeCollab?.()
+  if (parent?.id && schema?.id && collab?.moveUpSharedNode) {
+    collab.moveUpSharedNode(parent.id, schema.id, 'up')
+  }
 }

 const moveDown = () => {
   const { parent, schema } = getCurrent()
   moveChild(parent?.children, schema, 1)
-  useRealtimeCollab().moveDownSharedNode(parent.id, schema.id, 'down')
+  const collab = useRealtimeCollab?.()
+  if (parent?.id && schema?.id && collab?.moveDownSharedNode) {
+    collab.moveDownSharedNode(parent.id, schema.id, 'down')
+  }
 }

29-37: Add null-safe fallbacks for user properties.

While haveRemoteState ensures user exists, it doesn't guarantee user.color or user.name are defined. Access these properties safely with fallbacks.

Apply this diff:

     <div
       v-if="haveRemoteState"
       ref="remoteRef"
       class="corner-mark-left"
-      :style="[remoteStyle, { backgroundColor: selectState.user.color }]"
+      :style="[remoteStyle, { backgroundColor: selectState.user?.color || '#1890ff' }]"
     >
-      <span> {{ selectState.user.name }} 正在编辑 </span>
+      <span> {{ selectState.user?.name || '其他用户' }} 正在编辑 </span>
     </div>
packages/design-core/package.json (1)

94-94: Update @vue/repl to latest stable version.

A previous review flagged that version 4.6.1 is outdated (4.6.2 was latest as of July 2025). Since the PR was created in July 2025 and we are now in October 2025, please verify if a newer stable release is available and update to align with Vue's security best practices.

The web search results show that version 4.6.1 was published by antfu, but don't provide clear information about the absolute latest version as of October 2025. Let me search more specifically for the latest versions.Perfect! The latest version of @vue/repl is 4.6.3, last published 10 days ago. The code is using 4.6.1, which is two patch versions behind.

-    "@vue/repl": "4.6.1",
+    "@vue/repl": "4.6.3",
🧹 Nitpick comments (2)
packages/plugins/versioncontrol/test/component/VersionTagCreate.spec.js (1)

78-78: Consider importing versionManager at the top level.

The dynamic import pattern works but is unconventional. For better clarity and consistency with standard testing practices, consider importing versionManager at the module level and using vi.mocked() to access the mock.

Apply this diff to refactor the import:

+import { versionManager } from '../../src/js'
+
 import { mount } from '@vue/test-utils'
 import { ref } from 'vue'
 import TagDialog from '../../src/components/VersionTagCreate.vue'
 import { vi } from 'vitest'

Then update the test:

   await button.trigger('click')

   // 确认 versionManager.commitAppService.addTagToCommit 被调用
-  const { versionManager } = await import('../../src/js')
   expect(versionManager.commitAppService.addTagToCommit).toHaveBeenCalledWith('', 'v1.0.0')
packages/canvas/container/src/components/CanvasAction.vue (1)

275-278: Consider simplifying the user existence check.

The current logic is safe but verbose. If any truthy user object should trigger remote state, the Object.keys check may be unnecessary.

Optionally simplify to:

 const haveRemoteState = computed(() => {
   const user = props.selectState?.user
-  return user !== null && user !== undefined && Object.keys(user).length > 0
+  return Boolean(user && typeof user === 'object')
 })

This assumes any non-empty user object is valid. Keep the current implementation if you specifically need to verify the object has enumerable keys.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb3629 and a60f1a2.

📒 Files selected for processing (10)
  • mockServer/package.json (1 hunks)
  • packages/canvas/container/src/components/CanvasAction.vue (11 hunks)
  • packages/design-core/package.json (3 hunks)
  • packages/design-core/re-export.js (1 hunks)
  • packages/design-core/registry.js (3 hunks)
  • packages/plugins/page/src/composable/usePage.ts (2 hunks)
  • packages/plugins/script/package.json (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionCommitCreate.spec.js (1 hunks)
  • packages/plugins/versioncontrol/test/component/VersionTagCreate.spec.js (1 hunks)
  • packages/register/src/constants.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/plugins/script/package.json
  • packages/register/src/constants.ts
  • packages/design-core/re-export.js
  • packages/plugins/page/src/composable/usePage.ts
  • packages/plugins/versioncontrol/test/component/VersionCommitCreate.spec.js
  • packages/design-core/registry.js
  • mockServer/package.json
🧰 Additional context used
🧬 Code graph analysis (1)
packages/plugins/versioncontrol/test/component/VersionTagCreate.spec.js (1)
packages/plugins/versioncontrol/src/js/index.ts (1)
  • versionManager (37-37)
🔇 Additional comments (13)
packages/plugins/versioncontrol/test/component/VersionTagCreate.spec.js (7)

1-5: LGTM!

Imports are correct and necessary for the test suite.


13-25: LGTM!

The versionManager mock is well-structured with appropriate test data and method stubs.


27-43: LGTM!

Test setup follows best practices with a clean mount for each test case.


45-51: LGTM!

Rendering test appropriately verifies the presence of all form elements and the correct number of options.


53-56: LGTM!

Dialog close event is properly tested.


58-62: LGTM!

Input v-model binding is correctly verified.


64-68: LGTM!

Select v-model binding is correctly verified.

packages/canvas/container/src/components/CanvasAction.vue (3)

610-612: LGTM: Remote style reuses label alignment logic.

The remote indicator appropriately reuses the label positioning logic, ensuring consistent placement.


642-651: LGTM: Consistent style value handling.

The remoteStyleValue follows the same pattern as labelStyleValue and optionStyleValue, ensuring consistent reactive updates.


665-674: LGTM: Proper exposure of remote state properties.

All remote collaboration properties are correctly exposed from setup and used in the template.

packages/design-core/package.json (3)

66-66: Version-control plugin dependency looks good.

The new @opentiny/tiny-engine-plugin-version-control entry follows the project's workspace:* convention and is appropriately placed. Related updates in re-export.js, registry.js, and defaultLayout.js confirm proper integration.


92-93: Collab UI dependencies (Cursor, Avatar) correctly added.

Both new dependencies follow the workspace:* convention and are properly positioned. Integration across re-export.js, registry.js, and defaultLayout.js is confirmed by the AI summary.


50-51: Inconsistent workspace version specifiers on lines 50–51: workspace:~ deviates from project convention.

Lines 50–51 use workspace:~ (tilde), while all other workspace dependencies throughout the file use workspace:* (caret). This is a confirmed inconsistency affecting only these two packages.

No duplicate entries were found for @opentiny/tiny-engine-multi-person-collaboration—it appears only once at line 51.

Verify whether workspace:~ is intentional for these two packages or should align with the workspace:* convention used throughout the rest of the file. If alignment is needed:

-    "@opentiny/tiny-engine-layout": "workspace:~",
-    "@opentiny/tiny-engine-multi-person-collaboration": "workspace:~",
+    "@opentiny/tiny-engine-layout": "workspace:*",
+    "@opentiny/tiny-engine-multi-person-collaboration": "workspace:*",

Comment thread packages/canvas/container/src/components/CanvasAction.vue
@hexqi hexqi changed the title Ospp 2025/multiplayer collaboration feat: multiplayer collaboration(Ospp 2025) Oct 29, 2025
@github-actions github-actions Bot added the enhancement New feature or request label Oct 29, 2025
@hexqi
hexqi merged commit 59650a2 into opentiny:ospp-2025/multiplayer-collaboration Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change enhancement New feature or request ospp ospp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants