Skip to content

Main#82

Merged
Braydenccc merged 2 commits into
devfrom
main
Apr 30, 2026
Merged

Main#82
Braydenccc merged 2 commits into
devfrom
main

Conversation

@Braydenccc
Copy link
Copy Markdown
Owner

@Braydenccc Braydenccc commented Apr 30, 2026

Summary by Sourcery

Improve seat chart UI controls, tag display options, undo behavior, and workspace permission compatibility while refining theming and dialogs across the app.

New Features:

  • Add global settings for seat chart tag display mode and per-seat element visibility (name, student number, tags) with optional large text modes.
  • Allow selecting candidate students in the list and sync seat/candidate rendering with new visibility and large-text settings.

Bug Fixes:

  • Restore access to legacy cloud workspaces by merging old per-user file lists and auto-migrating missing permission records based on file authorship.
  • Fix permission checks for reading and deleting workspaces so legacy files owned by a user remain accessible.
  • Ensure undo wrappers only highlight seats whose state actually changes between snapshots.

Enhancements:

  • Redesign the resize divider and related controls for better visual clarity and touch behavior.
  • Move tag display mode configuration from the tag dialog into the unified settings dialog with a redirect entry point.
  • Unify dialog overlays and key UI colors with theme variables, improving dark mode readability and consistency.
  • Refine undo/redo integration for seat operations to use snapshot-based batch recording and more accurate affected-seat highlighting.
  • Improve auto-save to avoid redundant writes, track last-saved state, and surface failures via the logger.
  • Allow opening and controlling the unified settings dialog via a shared composable instead of local state.
  • Tighten WebDAV export preview styling and button theming to use shared color variables.
  • Improve visual feedback for undo highlights, candidate selection, logs, tags, and various badges across the UI.

实现权限系统的向后兼容,当权限记录不存在时:
1. 在文件列表操作中合并旧用户文件数据
2. 在文件读写操作中检查文件作者并自动创建权限记录
refactor: 重构撤销/重做功能以支持批量操作

fix(undo): 修复撤销高亮效果并优化性能

feat(settings): 新增座位表元素显示控制选项

style: 调整分隔条样式和交互效果

perf(autosave): 优化自动保存机制减少冗余操作

fix(drag): 修复拖拽操作与撤销系统的同步问题

chore: 清理无用代码和重复样式

docs: 更新部分注释和提示文本
Copilot AI review requested due to automatic review settings April 30, 2026 13:22
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 30, 2026

Reviewer's Guide

Refines seat chart UI personalization, tag display settings, undo behavior, and dark theme colors; introduces a centralized settings dialog controller; improves autosave and save-sync flow; restructures seat operations to batch-based undo; and updates various overlays and components for consistent theming and better interaction patterns.

Sequence diagram for tag display mode settings redirection

sequenceDiagram
  actor User
  participant TagSettingsDialog
  participant SettingsDialogController as useSettingsDialog
  participant UnifiedSettingsDialog
  participant UIPanel
  participant TagData as useTagData

  User->>TagSettingsDialog: click goToDisplayModeSettings
  TagSettingsDialog->>TagSettingsDialog: close()
  TagSettingsDialog->>SettingsDialogController: openSettings('global','ui')
  SettingsDialogController->>UnifiedSettingsDialog: visible true, initialTab global, initialCategory ui
  UnifiedSettingsDialog->>TagData: read tagDisplayMode, showTagsInSeatChart
  UnifiedSettingsDialog->>UIPanel: render UIPanel with settings.ui
  UIPanel->>TagData: bind localTagDisplayMode via computed
  User->>UIPanel: change tag display mode
  UIPanel->>TagData: setTagDisplayMode(mode)
  TagData-->>UIPanel: tagDisplayMode updated
  UIPanel-->>UnifiedSettingsDialog: emit update:settings
  UnifiedSettingsDialog-->>SettingsDialogController: closeSettings()
Loading

Sequence diagram for batch seat operations with undo snapshots

sequenceDiagram
  actor User
  participant SeatChart
  participant SeatChartStore as useSeatChart
  participant Undo as useUndo

  User->>SeatChart: multi_select_seats
  User->>SeatChart: click_clear_selection
  SeatChart->>Undo: createSnapshot() as beforeSnapshot
  SeatChart->>SeatChartStore: clearSeat(seatId,false) loop selectedSeats
  SeatChart->>Undo: createSnapshot() as afterSnapshot
  SeatChart->>Undo: recordBatch(beforeSnapshot,afterSnapshot)
  Note right of Undo: pushes command type batch

  User->>SeatChart: press_ctrl_z
  SeatChart->>Undo: canUndo.value?
  Undo-->>SeatChart: true
  SeatChart->>Undo: undo()
  Undo->>SeatChartStore: apply_snapshot(previous_state)
  Undo->>SeatChartStore: compute_affected_seats_from_snapshot
  SeatChartStore-->>SeatChart: updated seats with undo_highlight

  User->>SeatChart: press_ctrl_y
  SeatChart->>Undo: canRedo.value?
  Undo-->>SeatChart: true
  SeatChart->>Undo: redo()
  Undo->>SeatChartStore: apply_snapshot(next_state)
  SeatChartStore-->>SeatChart: updated seats with undo_highlight
Loading

Sequence diagram for legacy workspace permission migration

sequenceDiagram
  actor User
  participant Client
  participant WorkspaceAPI as workspace.php
  participant DbUsers
  participant DbFiles
  participant DbPermissions

  User->>Client: open_cloud_workspace_list
  Client->>WorkspaceAPI: action=list, username

  WorkspaceAPI->>DbPermissions: getUserAccessibleFiles(username)
  WorkspaceAPI->>DbUsers: get_array(username_files) as legacyFileIds
  WorkspaceAPI->>WorkspaceAPI: merge fileIds and legacyFileIds

  loop each fileId
    WorkspaceAPI->>DbFiles: get(fileId)
    DbFiles-->>WorkspaceAPI: fileRaw
    WorkspaceAPI->>WorkspaceAPI: parse fileRaw to fileData
    WorkspaceAPI->>DbPermissions: get(perm_fileId_username)
    alt no_permission_record
      WorkspaceAPI->>WorkspaceAPI: check fileData.metadata.author
      alt author_is_username
        WorkspaceAPI->>DbPermissions: grantFilePermission(owner)
      end
    end
    WorkspaceAPI-->>Client: include file metadata in list
  end

  User->>Client: open_workspace(fileId)
  Client->>WorkspaceAPI: action=open, fileId, username
  WorkspaceAPI->>DbFiles: get(fileId)
  WorkspaceAPI->>WorkspaceAPI: parse fileData
  WorkspaceAPI->>DbPermissions: get(perm_fileId_username)
  alt no_permission_record
    WorkspaceAPI->>WorkspaceAPI: check fileData.metadata.author
    alt author_is_username
      WorkspaceAPI->>DbPermissions: grantFilePermission(owner)
      WorkspaceAPI-->>Client: respond success with data
    else
      WorkspaceAPI-->>Client: respond 403
    end
  else has_permission_record
    WorkspaceAPI->>WorkspaceAPI: hasFilePermission(read)?
    alt has_read
      WorkspaceAPI-->>Client: respond success with data
    else
      WorkspaceAPI-->>Client: respond 403
    end
  end
Loading

Updated class diagram for settings, autosave, undo, and seat display

classDiagram
  class useGlobalSettings {
    +Ref settings
    +defaultSettings
    +saveToLocalStorage()
    +resetSettings()
    +applyThemeColor(color)
    +applyColorScheme(mode)
  }

  class UISettings {
    +bool enableAnimations
    +number defaultZoom
    +bool showStudentName
    +bool showStudentNumber
    +bool largeNameMode
    +bool largeNumberMode
  }

  class useSettingsDialog {
    +Ref visible
    +Ref initialTab
    +Ref initialCategory
    +openSettings(tab,category)
    +closeSettings()
  }

  class UnifiedSettingsDialog {
    +Ref activeTab
    +Ref activeCategory
    +handleSave()
    +handleCancel()
    +handleReset()
  }

  class TagSettingsDialog {
    +Ref localShowTagsInSeatChart
    +goToDisplayModeSettings()
    +close()
  }

  class UIPanel {
    +Computed localSettings
    +Computed localTagDisplayMode
    +Computed localShowTags
    +Computed hasHiddenElement
    +Computed canEnableLargeName
    +Computed canEnableLargeNumber
  }

  class useTagData {
    +Ref tags
    +Ref showTagsInSeatChart
    +Ref tagDisplayMode
    +setShowTagsInSeatChart(value)
    +setTagDisplayMode(mode)
  }

  class SeatItem {
    +Computed showStudentName
    +Computed showStudentNumber
    +Computed hasHiddenElement
    +Computed largeNameMode
    +Computed largeNumberMode
  }

  class CandidateItem {
    +Computed isSelected
    +Computed showStudentName
    +Computed showStudentNumber
    +Computed hasHiddenElement
    +Computed largeNameMode
    +Computed largeNumberMode
    +handleClick()
  }

  class useStudentData {
    +Ref selectedStudentId
    +selectStudent(id)
    +clearSelection()
  }

  class useEditMode {
    +Ref currentMode
    +Enum EditMode
    +setMode(mode)
    +setFirstSelectedSeat(seatId)
    +clearFirstSelectedSeat()
  }

  class useAutoSave {
    +Ref isAutoSaveEnabled
    +Ref lastSaveTime
    +startAutoSave()
    +stopAutoSave()
    +performAutoSave()
    +markSaved()
    +getAutoSaveBackup()
    +clearAutoSaveBackup()
  }

  class useUndo {
    +Ref canUndo
    +Ref canRedo
    +undo()
    +redo()
    +createSnapshot()
    +recordAssign(seatId,studentId,previousSeatId)
    +recordBatch(beforeSnapshot,afterSnapshot)
  }

  class SeatChart {
    +Computed selectedSeatsArray
    +handleSelClear()
    +handleSelShuffle()
    +handleSelAssign()
    +handleAssignStudent(seatId,studentId)
  }

  class StudentList {
    +handleDrop(event)
  }

  class GlobalDropZone {
    +handleDrop(event)
  }

  class CloudWorkspaceDialog {
    +handleSave()
  }

  class SidebarPanel {
    +handleSaveWorkspace()
  }

  useGlobalSettings --> UISettings : has ui
  UnifiedSettingsDialog --> useGlobalSettings : uses
  UnifiedSettingsDialog --> useSettingsDialog : uses
  TagSettingsDialog --> useTagData : uses
  TagSettingsDialog --> useSettingsDialog : goToDisplayModeSettings
  UIPanel --> useTagData : binds displayMode
  UIPanel --> useGlobalSettings : uses ui flags
  SeatItem --> useGlobalSettings : reads settings.ui
  CandidateItem --> useGlobalSettings : reads settings.ui
  CandidateItem --> useStudentData : selection
  CandidateItem --> useEditMode : mode_control
  SeatChart --> useUndo : batch_operations
  SeatChart --> useGlobalSettings : uses
  StudentList --> useUndo : batch_clear
  GlobalDropZone --> useSeatChart : clearSeat
  CloudWorkspaceDialog --> useAutoSave : markSaved
  SidebarPanel --> useAutoSave : markSaved
  AppHeader --> useSettingsDialog : openSettings
  UnifiedSettingsDialog <.. AppHeader : rendered_by
Loading

Flow diagram for autosave and explicit save coordination

flowchart LR
  subgraph AutoSaveComposable
    AS_useAutoSave[useAutoSave]
    lastSavedJson[(lastSavedJson)]
  end

  App[App_root]
  Sidebar[SidebarPanel_handleSaveWorkspace]
  CloudDialog[CloudWorkspaceDialog_handleSave]

  App --> AS_useAutoSave

  AS_useAutoSave -->|startAutoSave_interval| AutoTimer[Auto_save_timer]
  AutoTimer -->|timeout| PerformAutoSave[performAutoSave]
  PerformAutoSave --> GetJson[getWorkspaceJson]
  GetJson -->|json| CheckDiff{json == lastSavedJson?}
  CheckDiff -->|yes| StopNoChange[skip_save]
  CheckDiff -->|no| SaveLocal[save_backup_to_localStorage]
  SaveLocal --> UpdateLast[update_lastSavedJson_and_lastSaveTime]

  Sidebar -->|user_click_save_to_local| SaveWorkspace[saveWorkspace]
  SaveWorkspace -->|success| SidebarMark[markSaved]
  SidebarMark --> AS_useAutoSave

  CloudDialog -->|user_click_save_to_cloud| CloudSave[save_workspace_to_cloud]
  CloudSave -->|success| CloudMark[markSaved]
  CloudMark --> AS_useAutoSave
Loading

File-Level Changes

Change Details Files
Add UI controls for seat chart element visibility and tag display mode, and wire them into global/tag settings.
  • Introduce tag display mode selector and element visibility toggles (name, student number, tags, large font modes) in the UI settings panel with validation logic via computed flags and watchers.
  • Bind tag display mode and show-tags-in-seat-chart flags to the tag data composable, exposing local computed proxies for two-way updates.
  • Ensure large font toggles are only enabled when at least one other element is hidden, automatically resetting invalid large-name/large-number states.
src/components/settings/panels/UIPanel.vue
src/composables/useGlobalSettings.js
src/components/seat/SeatItem.vue
src/components/student/CandidateItem.vue
Move tag display mode configuration from the tag dialog into global UI settings and add a redirect affordance.
  • Remove inline tag display mode radio controls from the tag settings dialog and replace them with a hint plus a button that opens the unified settings dialog at the UI tab.
  • Stop using tagDisplayMode in the tag dialog composable, keeping only showTagsInSeatChart there.
  • Style the redirect hint and button to match the updated design language.
src/components/student/TagSettingsDialog.vue
src/composables/useSettingsDialog.ts
src/components/settings/UnifiedSettingsDialog.vue
src/components/layout/AppHeader.vue
Unify name/number visibility and large-text behavior across seat items and candidate list items.
  • Derive showStudentName/showStudentNumber and largeNameMode/largeNumberMode from global UI settings in seat and candidate components.
  • Guard name and number rendering with v-if checks and adjust CSS classes to center content or scale font sizes depending on visibility and large-text flags.
  • Add responsive font-size adjustments for large-name/large-number across different seat size breakpoints.
src/components/seat/SeatItem.vue
src/components/student/CandidateItem.vue
Redesign the resize divider and collapse toggle for the candidate panel.
  • Replace the icon-based handle with a centered pill-shaped grip containing three dots, sitting atop a 1px divider line.
  • Adjust hover, dragging, and collapsed states (backgrounds, borders, scaling) for the divider line, grip, and toggle button, including touch-specific sizing tweaks.
  • Simplify the collapse toggle icon sizing and styling to match the new handle visuals.
src/components/ui/ResizeDivider.vue
src/components/layout/EditorPanel.vue
Introduce a centralized settings dialog composable and refactor UnifiedSettingsDialog/AppHeader to use it.
  • Create useSettingsDialog composable to hold visibility and initial tab/category state and provide open/close helpers.
  • Convert UnifiedSettingsDialog to use the composable’s visible/initialTab/initialCategory instead of props and v-model, updating open/close flows accordingly.
  • Update AppHeader and TagSettingsDialog to open/close settings through the composable, and ensure cleanup on unmount.
  • Normalize primary button text colors to use the inverse text variable.
src/composables/useSettingsDialog.ts
src/components/settings/UnifiedSettingsDialog.vue
src/components/layout/AppHeader.vue
src/components/student/TagSettingsDialog.vue
Improve autosave behavior and align manual/local/cloud saves with autosave snapshots.
  • Track lastSavedJson in the autosave composable to avoid redundant writes when nothing has changed.
  • Expose a markSaved helper and call it after local and cloud workspace saves so autosave doesn’t immediately re-save identical content.
  • Switch autosave logging to user-facing error messages instead of console.error and keep lastSaveTime updated on successful saves.
src/composables/useAutoSave.js
src/components/layout/SidebarPanel.vue
src/components/workspace/CloudWorkspaceDialog.vue
Add backward-compatible permission handling for workspaces when listing, loading, and deleting files.
  • When listing workspaces, merge file IDs from legacy per-user file lists with the permission table, de-duplicating IDs.
  • On list/load/delete, if no permission record exists, check if the current user is the file’s author and, if so, automatically grant owner permission before continuing.
  • If neither permission nor ownership applies, respond with a 403 error while maintaining compatibility with older data layouts.
public/api/workspace.php
Refactor undo/redo recording to favor snapshot-based batch operations and make highlighting more accurate.
  • Remove granular recordSwap/recordClear/recordToggleEmpty usage from seat chart, student list, and global drop zone flows, instead using createSnapshot + recordBatch for multi-seat operations and swap/assign helpers that can skip recording.
  • Update seat clearing, swapping, and assigning calls to accept a flag controlling undo recording, using false when a surrounding batch handles history.
  • Change undo wrapper highlighting to only mark seats whose current state actually differs from the stored snapshot, reducing unnecessary highlight noise.
src/components/seat/SeatChart.vue
src/components/student/StudentList.vue
src/components/ui/GlobalDropZone.vue
src/components/student/StudentListHeader.vue
src/composables/useUndo.js
Polish UI theming, overlays, and small interaction details across dialogs and components.
  • Replace hard-coded overlay colors with the shared --color-bg-overlay variable in multiple dialogs and modals, and use theme variables for dialog backgrounds and borders.
  • Adjust dark theme text and border color tokens to improve contrast (e.g., muted/disabled text and border-dark).
  • Standardize tag and primary-button text color to var(--color-text-inverse) instead of literal white.
  • Tweak various component styles such as export preview buttons, tag list borders, loading spinner overlay background, and log list severity backgrounds.
src/assets/main.css
src/components/relation/SeatRuleEditor.vue
src/components/layout/ExportPreview.vue
src/components/layout/SidebarPanel.vue
src/components/student/BatchEditDialog.vue
src/components/auth/LoginDialog.vue
src/components/auth/SyncSettingsDialog.vue
src/components/docs/RuleUsageGuide.vue
src/components/layout/SeatConfigDialog.vue
src/components/rule/RuleList.vue
src/components/settings/panels/AboutPanel.vue
src/components/settings/panels/SyncPanel.vue
src/components/student/StudentEditDialog.vue
src/components/student/StudentItem.vue
src/components/student/StudentRosterDialog.vue
src/components/student/TagManager.vue
src/components/student/TagStudentSelector.vue
src/components/ui/LoadingSpinner.vue

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Braydenccc Braydenccc merged commit f145e86 into dev Apr 30, 2026
8 of 12 checks passed
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • The large-name/large-number enablement logic in UIPanel (hasHiddenElement includes localShowTags) is inconsistent with SeatItem/CandidateItem (where hasHiddenElement only considers name/number), so hiding only tags can allow enabling large text in settings but it will never apply in seat/candidate display—consider aligning the conditions or updating the hint/UX to match actual behavior.
  • In useSettingsDialog, initialCategory is initialized to 'ui' but openSettings() without arguments resets it to 'sync'; this means opening the settings from the header will default to a different category than the initial value—clarify the intended default and remove the mismatch.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The large-name/large-number enablement logic in UIPanel (`hasHiddenElement` includes `localShowTags`) is inconsistent with SeatItem/CandidateItem (where `hasHiddenElement` only considers name/number), so hiding only tags can allow enabling large text in settings but it will never apply in seat/candidate display—consider aligning the conditions or updating the hint/UX to match actual behavior.
- In `useSettingsDialog`, `initialCategory` is initialized to `'ui'` but `openSettings()` without arguments resets it to `'sync'`; this means opening the settings from the header will default to a different category than the initial value—clarify the intended default and remove the mismatch.

## Individual Comments

### Comment 1
<location path="src/components/settings/panels/UIPanel.vue" line_range="748-749" />
<code_context>
 const { defaultSettings, applyThemeColor, applyColorScheme } = useGlobalSettings()
+const { tagDisplayMode, setTagDisplayMode, showTagsInSeatChart, setShowTagsInSeatChart } = useTagData()

 const localSettings = computed({
-  get: () => props.settings,
+  get: () => props.settings || {},
   set: (value) => emit('update:settings', value)
 })
</code_context>
<issue_to_address>
**issue (bug_risk):** Defaulting `localSettings` to `{}` can drop existing UI settings and cause inconsistent state.

When `props.settings` is `null`/`undefined`, this getter will now emit and persist changes against an empty object, so fields like `showStudentName` / `showStudentNumber` may become `undefined` or overwritten with partial data. Consider defaulting to a fully-populated settings object (e.g. `defaultSettings.ui`) or enforcing that the parent always provides a non-null settings instance and keeping the original getter, to avoid saving incomplete or corrupted UI settings in early-init cases (e.g. dialog opened before settings are loaded).
</issue_to_address>

### Comment 2
<location path="src/components/seat/SeatItem.vue" line_range="109" />
<code_context>
+  set: (val) => setShowTagsInSeatChart(val)
+})
+
+const hasHiddenElement = computed(() => {
+  return !localSettings.value.showStudentName || !localSettings.value.showStudentNumber || !localShowTags.value
+})
</code_context>
<issue_to_address>
**issue (bug_risk):** Large-font mode logic in `SeatItem` ignores tag visibility, which can desync with the settings constraints.

In `UIPanel.vue`, `hasHiddenElement` correctly includes tag visibility via `localShowTags` / `showTagsInSeatChart`, but in `SeatItem.vue` and `CandidateItem.vue` it only checks `showStudentName` and `showStudentNumber`. This means hiding only tags keeps `hasHiddenElement` false in those components, so large-font modes never activate even though the settings allow them. Please either reuse the same `hasHiddenElement` rule (including tag visibility) in these components, or update the settings constraints so they align with the rendering logic.
</issue_to_address>

### Comment 3
<location path="src/composables/useSettingsDialog.ts" line_range="4-11" />
<code_context>
+import { ref } from 'vue'
+
+const visible = ref(false)
+const initialTab = ref('global')
+const initialCategory = ref('ui')
+
+export function useSettingsDialog() {
+  const openSettings = (tab?: string, category?: string) => {
+    if (tab) initialTab.value = tab
+    else initialTab.value = 'global'
+    if (category) initialCategory.value = category
+    else initialCategory.value = 'sync'
+    visible.value = true
</code_context>
<issue_to_address>
**issue (bug_risk):** The default `initialCategory` differs between the ref and `openSettings`, which can cause inconsistent initial tab state.

`initialCategory` is initialized as `'ui'`, but `openSettings` falls back to `'sync'` when `category` is undefined. Since `AppHeader` calls `openSettings()` with no args while `TagSettingsDialog` uses `'ui'` explicitly, the first open will show `'sync'` and direct navigation `'ui'`. If `'ui'` is the intended default, update the `openSettings` fallback to `'ui'` to keep behavior consistent.
</issue_to_address>

### Comment 4
<location path="src/components/ui/GlobalDropZone.vue" line_range="19-25" />
<code_context>
 import { useAuth } from '@/composables/useAuth'
 import { useUndo } from '@/composables/useUndo'
 import { useCloudWorkspaceDialog } from '@/composables/useCloudWorkspaceDialog'
+import { useAutoSave } from '@/composables/useAutoSave'
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clearing a seat via the global drop zone is no longer undoable, which may be a regression versus other clear operations.

Previously, dropping a seat into the global drop zone called `recordClear` before `clearSeat`, so the action was added to the undo history. With this change, the handler only calls `clearSeat`:
```js
if (data.type === 'seat' && data.seatId) {
  clearSeat(data.seatId)
  clearSelection()
  success('已将学生移出座位')
}
```
Other clear operations now use `createSnapshot` + `recordBatch` to remain undoable. To keep behavior consistent, this path should likely use the same snapshot-based undo flow, unless there’s a deliberate reason for this removal to be non-undoable.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 748 to +749
const localSettings = computed({
get: () => props.settings,
get: () => props.settings || {},
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.

issue (bug_risk): Defaulting localSettings to {} can drop existing UI settings and cause inconsistent state.

When props.settings is null/undefined, this getter will now emit and persist changes against an empty object, so fields like showStudentName / showStudentNumber may become undefined or overwritten with partial data. Consider defaulting to a fully-populated settings object (e.g. defaultSettings.ui) or enforcing that the parent always provides a non-null settings instance and keeping the original getter, to avoid saving incomplete or corrupted UI settings in early-init cases (e.g. dialog opened before settings are loaded).

const showStudentNumber = computed(() => settings.value.ui.showStudentNumber !== false)
const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value)
const largeNameMode = computed(() => showStudentName.value && hasHiddenElement.value && settings.value.ui.largeNameMode)
const largeNumberMode = computed(() => showStudentNumber.value && hasHiddenElement.value && settings.value.ui.largeNumberMode)
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.

issue (bug_risk): Large-font mode logic in SeatItem ignores tag visibility, which can desync with the settings constraints.

In UIPanel.vue, hasHiddenElement correctly includes tag visibility via localShowTags / showTagsInSeatChart, but in SeatItem.vue and CandidateItem.vue it only checks showStudentName and showStudentNumber. This means hiding only tags keeps hasHiddenElement false in those components, so large-font modes never activate even though the settings allow them. Please either reuse the same hasHiddenElement rule (including tag visibility) in these components, or update the settings constraints so they align with the rendering logic.

Comment on lines +4 to +11
const initialTab = ref('global')
const initialCategory = ref('ui')

export function useSettingsDialog() {
const openSettings = (tab?: string, category?: string) => {
if (tab) initialTab.value = tab
else initialTab.value = 'global'
if (category) initialCategory.value = category
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.

issue (bug_risk): The default initialCategory differs between the ref and openSettings, which can cause inconsistent initial tab state.

initialCategory is initialized as 'ui', but openSettings falls back to 'sync' when category is undefined. Since AppHeader calls openSettings() with no args while TagSettingsDialog uses 'ui' explicitly, the first open will show 'sync' and direct navigation 'ui'. If 'ui' is the intended default, update the openSettings fallback to 'ui' to keep behavior consistent.

Comment on lines -19 to -25
import { useUndo } from '@/composables/useUndo'
import { useLogger } from '@/composables/useLogger'

const { isDraggingFromSeat } = useDragState()
const { clearSeat, getStudentAtSeat } = useSeatChart()
const { clearSeat } = useSeatChart()
const { clearSelection, students } = useStudentData()
const { recordClear } = useUndo()
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.

suggestion (bug_risk): Clearing a seat via the global drop zone is no longer undoable, which may be a regression versus other clear operations.

Previously, dropping a seat into the global drop zone called recordClear before clearSeat, so the action was added to the undo history. With this change, the handler only calls clearSeat:

if (data.type === 'seat' && data.seatId) {
  clearSeat(data.seatId)
  clearSelection()
  success('已将学生移出座位')
}

Other clear operations now use createSnapshot + recordBatch to remain undoable. To keep behavior consistent, this path should likely use the same snapshot-based undo flow, unless there’s a deliberate reason for this removal to be non-undoable.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the settings/seat-chart UX by adding new UI preferences (name/number visibility + large-font modes), centralizing settings dialog state via a composable, improving autosave behavior, and refining undo batching/highlighting. It also includes broad styling updates to use theme variables more consistently, plus legacy-permission migration logic in the workspace API.

Changes:

  • Add UI settings for tag display mode and seat-chart element visibility (name/number/tags + large-font modes), and wire them into seat/candidate rendering.
  • Introduce a global useSettingsDialog() state to open/close the unified settings dialog (including deep-linking to a specific tab/category).
  • Improve autosave to skip redundant writes and mark autosave state on successful manual saves; adjust undo batching/highlighting; update various overlays/buttons to theme variables; add legacy permission migration in workspace.php.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/composables/useUndo.js Adjusts affected-seat highlighting logic for wrapper snapshot commands.
src/composables/useSettingsDialog.ts Adds shared settings-dialog visibility + initial tab/category state.
src/composables/useGlobalSettings.js Extends default UI settings (name/number visibility + large-font modes).
src/composables/useAutoSave.js Skips autosave when workspace JSON unchanged; adds markSaved().
src/components/workspace/CloudWorkspaceDialog.vue Calls markSaved() after successful cloud save; theme variable styling tweaks.
src/components/ui/ResizeDivider.vue Redesigns divider visuals and adjusts sizing/hover/drag styles.
src/components/ui/LoadingSpinner.vue Uses theme-based overlay background via color-mix().
src/components/ui/GlobalDropZone.vue Simplifies drop handling by relying on seat-chart undo recording.
src/components/student/TagStudentSelector.vue Replaces hard-coded border color with theme variable.
src/components/student/TagSettingsDialog.vue Removes inline tag-mode controls and redirects to unified UI settings.
src/components/student/TagManager.vue Updates modal overlay background to theme variable.
src/components/student/StudentRosterDialog.vue Updates modal overlay background to theme variable.
src/components/student/StudentListHeader.vue Random-assign now records a single batch undo snapshot.
src/components/student/StudentList.vue Batch seat-clears now use snapshot-based undo batching.
src/components/student/StudentItem.vue Replaces white text with inverse theme variable for tag text.
src/components/student/StudentEditDialog.vue Updates modal overlay background to theme variable.
src/components/student/CandidateItem.vue Adds selection + name/number visibility + large-font rendering options.
src/components/student/BatchEditDialog.vue Updates overlay + badge text color to theme variables.
src/components/settings/panels/UIPanel.vue Adds tag display mode + element visibility toggles (and large-font enable rules).
src/components/settings/panels/SyncPanel.vue Replaces white with inverse text variable for primary button.
src/components/settings/panels/AboutPanel.vue Replaces white with inverse text variable for tech tags.
src/components/settings/UnifiedSettingsDialog.vue Switches to useSettingsDialog() for visibility and initial tab/category.
src/components/seat/SeatItem.vue Adds name/number visibility + large-font rendering; tweaks undo highlight animation.
src/components/seat/SeatChart.vue Moves several multi-seat operations to snapshot-based batch undo recording.
src/components/rule/RuleList.vue Replaces white with inverse text variable for delete-confirm state.
src/components/relation/SeatRuleEditor.vue Theme variable updates for dialog surfaces/overlays and active text colors.
src/components/layout/SidebarPanel.vue Calls markSaved() on successful local save; log styling + overlay theme updates.
src/components/layout/SeatConfigDialog.vue Updates overlay background to theme variable.
src/components/layout/ExportPreview.vue Replaces inline button colors with classes + theme variables; other theme tweaks.
src/components/layout/EditorPanel.vue Removes bottom border styling (visual/layout adjustment).
src/components/layout/AppHeader.vue Migrates unified settings open/close to useSettingsDialog() state.
src/components/docs/RuleUsageGuide.vue Replaces hard-coded border color with theme variable.
src/components/auth/SyncSettingsDialog.vue Updates overlay background to theme variable.
src/components/auth/LoginDialog.vue Updates overlay background to theme variable.
src/assets/main.css Tweaks dark-mode text/border variables and comment wording.
src/App.vue Updates undo/redo hotkeys to use computed refs (canUndo.value / canRedo.value).
public/api/workspace.php Merges legacy file lists + migrates permissions; updates read/write permission logic.
Comments suppressed due to low confidence (1)

public/api/workspace.php:255

  • In the list action, legacy $legacyFileIds are merged into $fileIds, but each listed file is added to $list without verifying the current user has read permission for that $fileId. If the legacy list contains stale/incorrect ids, this can leak file metadata (name/time/size/author). Add a per-file permission check in the loop (similar to load), and only include legacy ids when the user is the author or when hasFilePermission(..., 'read') passes (optionally creating a migrated permission record when author matches).
        // 从权限表获取文件
        $fileIds = getUserAccessibleFiles($dbPermissions, $username);
        
        // 兼容旧数据:同时从用户文件列表获取文件
        $userFilesKey = sanitizeDbKey($username . '_files');
        $legacyFileIds = $dbUsers->get_array($userFilesKey);
        if ($legacyFileIds && is_array($legacyFileIds)) {
            $fileIds = array_unique(array_merge($fileIds ?: [], $legacyFileIds));
        }

        $list = [];
        if ($fileIds && is_array($fileIds)) {
            foreach ($fileIds as $fileId) {
                if (!isValidFileId($fileId)) continue;

                $sanitizedFileId = sanitizeDbKey($fileId);
                $fileRaw = $dbFiles->get($sanitizedFileId);
                if (!$fileRaw) continue;

                $fileData = json_decode($fileRaw, true);
                if (!$fileData || !isset($fileData['metadata'])) continue;

                // 兼容旧数据:自动迁移权限记录
                $permKey = sanitizeDbKey("perm_{$fileId}_{$username}");
                if ($dbPermissions->get($permKey) === null) {
                    // 检查文件作者是否是当前用户
                    if (isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) {
                        grantFilePermission($dbPermissions, $fileId, $username, 'owner');
                    }
                }

                $list[] = [
                    'fileId' => $fileId,
                    'metadata' => $fileData['metadata']
                ];

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4 to +13
const initialTab = ref('global')
const initialCategory = ref('ui')

export function useSettingsDialog() {
const openSettings = (tab?: string, category?: string) => {
if (tab) initialTab.value = tab
else initialTab.value = 'global'
if (category) initialCategory.value = category
else initialCategory.value = 'sync'
visible.value = true
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

initialCategory is initialized to 'ui' (line 5) but openSettings() falls back to 'sync' when no category is provided (line 12). This inconsistency makes the default dialog state dependent on call path and is easy to misread/extend incorrectly. Align the defaults by picking one source of truth (e.g., make both default to 'sync'), and consider narrowing tab/category to known ids to prevent invalid values being stored.

Copilot uses AI. Check for mistakes.
Comment on lines 77 to 86
case 'redo_wrapper':
case 'undo_wrapper':
if (command.snapshot) {
command.snapshot.forEach(s => affected.push(s.id))
command.snapshot.forEach(s => {
const current = seats.value.find(seat => seat.id === s.id)
if (current && (current.studentId !== s.studentId || current.isEmpty !== s.isEmpty)) {
affected.push(s.id)
}
})
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

undo() pushes { type: 'redo_wrapper', snapshot: beforeSnapshot } onto redoStack, but executeRedo() has no redo_wrapper case (it only handles undo_wrapper). As a result, calling redo() after undo() will not restore the snapshot and effectively becomes a no-op beyond stack bookkeeping. Fix by making the wrapper command types symmetrical (either handle redo_wrapper in executeRedo, or push an undo_wrapper from undo() / adjust the naming consistently).

Copilot uses AI. Check for mistakes.

const showStudentName = computed(() => settings.value.ui.showStudentName !== false)
const showStudentNumber = computed(() => settings.value.ui.showStudentNumber !== false)
const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

hasHiddenElement only considers name/number visibility, but the UI settings hint says large-font modes can be enabled when any element is hidden (including tags). If a user hides tags but keeps both name & number visible, largeNameMode/largeNumberMode will never activate even if enabled. Include tag visibility (e.g., showTagsInSeatChart) in hasHiddenElement so the runtime behavior matches the settings logic.

Suggested change
const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value)
const hasHiddenElement = computed(() =>
!showStudentName.value ||
!showStudentNumber.value ||
!showTagsInSeatChart.value
)

Copilot uses AI. Check for mistakes.

const showStudentName = computed(() => settings.value.ui.showStudentName !== false)
const showStudentNumber = computed(() => settings.value.ui.showStudentNumber !== false)
const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

hasHiddenElement only checks name/number visibility, but the settings panel enables large-font modes when any element is hidden (name/number/tags). If tags are hidden while name & number remain visible, largeNameMode/largeNumberMode won’t activate even if enabled. Include showTagsInSeatChart in hasHiddenElement so seat rendering matches the settings behavior.

Suggested change
const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value)
const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value || showTagsInSeatChart.value === false)

Copilot uses AI. Check for mistakes.
Comment thread public/api/workspace.php
Comment on lines +328 to +334
// 没有权限记录,检查文件作者
$fileData = $fileRaw ? json_decode($fileRaw, true) : null;
if ($fileData && isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) {
// 自动迁移:创建权限记录
grantFilePermission($dbPermissions, $fileId, $username, 'owner');
} else {
respond(['success' => false, 'message' => '无权删除该文件'], 403);
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

In delete, when there is no permission record and $fileRaw is null (file already deleted), $fileData becomes null and the code returns 403 (lines 327-335). This prevents users from removing stale legacy entries, even though the later else branch is designed to handle the “file already removed” case. Consider short-circuiting: if $fileRaw === null, skip the author-based permission migration and proceed to remove the id from the user list / revoke permissions and return success.

Suggested change
// 没有权限记录,检查文件作者
$fileData = $fileRaw ? json_decode($fileRaw, true) : null;
if ($fileData && isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) {
// 自动迁移:创建权限记录
grantFilePermission($dbPermissions, $fileId, $username, 'owner');
} else {
respond(['success' => false, 'message' => '无权删除该文件'], 403);
// 没有权限记录时,仅在文件仍存在时回退检查文件作者;
// 如果文件已不存在,则继续走后续清理逻辑,允许移除陈旧引用。
if ($fileRaw !== null) {
$fileData = json_decode($fileRaw, true);
if ($fileData && isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) {
// 自动迁移:创建权限记录
grantFilePermission($dbPermissions, $fileId, $username, 'owner');
} else {
respond(['success' => false, 'message' => '无权删除该文件'], 403);
}

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants