# OneOS V2 Global Operation Actions Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make OneOS V2 list action columns expose “查看/详情” first and one workflow action second, while moving management, history, and dangerous actions into the shared “更多” menu; migrate the vehicle asset list to that shared behavior. **Architecture:** Keep `OperationActions` as the only renderer and menu owner. Split detail-entry recognition from history recognition in the flat-item adapter, then consume the public `view`, `edit`, and `more` inputs from the vehicle list. Update every normative document that currently describes the old “details in more” behavior. **Tech Stack:** React 18, TypeScript 5.9, Vitest 4, Lucide React, Vite, CSS. --- ## File map - Create `src/common/OperationActions.test.tsx`: focused regression tests for action ordering, detail/history classification, hidden actions, and empty menus. - Modify `src/common/OperationActions.tsx`: render detail first, allow one workflow primary second, and keep history items in `more`. - Modify `src/common/operation-actions-spec.md`: public component contract and migration guidance. - Modify `src/resources/design-system/DESIGN.md`: global normative summary. - Modify `src/resources/design-system/chapters/03-components.md`: component chapter summary and widths. - Modify `src/prototypes/vm-shared/DESIGN.md`: full shared pattern, examples, and forbidden patterns. - Modify `src/prototypes/vehicle-management/components/ListView.tsx`: remove the private menu and use `OperationActions`. - Modify `src/prototypes/vehicle-management/style.css`: remove vehicle-only action-menu skin and size the sticky operation column for the shared component. ### Task 1: Lock the shared action contract with tests **Files:** - Create: `src/common/OperationActions.test.tsx` - [ ] **Step 1: Write regression tests for classification and markup order** Create the following test file: ```tsx import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it, vi } from 'vitest'; import { OperationActions, splitOperationActions, type OperationActionItem, } from './OperationActions'; const action = (key: string, label: string): OperationActionItem => ({ key, label, onClick: vi.fn(), }); describe('splitOperationActions', () => { it('separates the detail entry from history actions', () => { const items = [ action('history', '操作记录'), action('edit', '编辑'), action('view', '查看详情'), action('owner', '设置运维负责人'), ]; const result = splitOperationActions(items); expect(result.view?.label).toBe('查看详情'); expect(result.edit?.label).toBe('编辑'); expect(result.more.map((item) => item.label)).toEqual([ '操作记录', '设置运维负责人', ]); }); it('does not promote 查看记录 as a detail entry', () => { const result = splitOperationActions([ action('viewRecords', '查看记录'), action('process', '处置'), ]); expect(result.view).toBeUndefined(); expect(result.process?.label).toBe('处置'); expect(result.more.map((item) => item.label)).toEqual(['查看记录']); }); }); describe('OperationActions', () => { it('renders detail before the workflow action and more', () => { const html = renderToStaticMarkup( , ); expect(html.indexOf('>查看<')).toBeLessThan(html.indexOf('>编辑<')); expect(html.indexOf('>编辑<')).toBeLessThan(html.indexOf('aria-label="更多操作"')); }); it('does not render more when all menu actions are hidden', () => { const html = renderToStaticMarkup( , ); expect(html).toContain('>详情<'); expect(html).not.toContain('aria-label="更多操作"'); }); }); ``` - [ ] **Step 2: Run the focused tests and verify the old behavior fails** Run: ```bash npm run test:run -- src/common/OperationActions.test.tsx ``` Expected: at least the ordering and classification assertions fail because the current implementation puts `view` into “更多” when `edit` or `process` exists and treats history labels as `view`. ### Task 2: Implement the global shared-component behavior **Files:** - Modify: `src/common/OperationActions.tsx` - Test: `src/common/OperationActions.test.tsx` - [ ] **Step 1: Update the public contract and primary ordering** Replace the `view` prop comment, primary-selection block, and `outsidePrimaries` construction with: ```tsx /** * 查看 / 详情:对象详情入口,固定在外侧首位。 * 历史型“查看记录 / 操作记录”应通过 more 传入。 */ view?: OperationPrimaryAction; ``` ```tsx const visibleEdit = edit && !edit.hidden ? edit : undefined; const visibleProcess = process && !process.hidden ? process : undefined; const visibleView = view && !view.hidden ? view : undefined; const outsidePrimaries = useMemo(() => { const list: Array<{ kind: PrimaryKind; action: OperationPrimaryAction; defaultLabel: string }> = []; if (visibleView) { list.push({ kind: 'view', action: visibleView, defaultLabel: visibleView.label || '详情', }); } if (visibleEdit) { list.push({ kind: 'edit', action: visibleEdit, defaultLabel: '编辑' }); } else if (visibleProcess) { list.push({ kind: 'process', action: visibleProcess, defaultLabel: visibleProcess.label || '处理', }); } return list.slice(0, 2); }, [visibleEdit, visibleProcess, visibleView]); ``` Delete `hasWorkPrimary`, `viewInMore`, `viewOutside`, and the block that injects `visibleView` into `visibleMore`. - [ ] **Step 2: Keep deduplication without hiding history actions** Replace `visibleMore` with: ```tsx const visibleMore = useMemo(() => { const items: OperationActionItem[] = []; more.forEach((item) => { if (item.hidden) return; if (visibleEdit && (item.key === 'edit' || item.label === '编辑' || item.label === '编辑合同')) { return; } if ( visibleProcess && (item.key === 'process' || item.label === '处理' || item.label === '处置' || item.label === (visibleProcess.label || '')) ) { return; } if ( visibleView && (item.key === 'view' || item.label === '查看' || item.label === '详情' || item.label === '查看详情') ) { return; } items.push(item); }); return items; }, [more, visibleView, visibleEdit, visibleProcess]); ``` - [ ] **Step 3: Separate detail labels from history labels in the adapter** Replace the view label set and view classification with: ```tsx const DETAIL_LABELS = new Set(['详情', '查看', '查看详情']); const HISTORY_LABELS = new Set(['查看记录', '操作记录']); ``` ```tsx const viewItem = visible.find( (item) => item.key === 'view' || DETAIL_LABELS.has(item.label), ); const more = visible.filter( (item) => item !== editItem && item !== processItem && item !== viewItem, ); ``` Return `viewItem.label` unchanged. Preserve history items—including `key === 'viewRecords'`, `key === 'history'`, and `HISTORY_LABELS` matches—in `more`. - [ ] **Step 4: Update the component-level summary comment** Use: ```tsx /** * 列表操作列(强制):详情入口固定首位,编辑/处理外显一个, * 历史、低频管理和危险操作进入更多。 */ ``` - [ ] **Step 5: Run the focused tests** Run: ```bash npm run test:run -- src/common/OperationActions.test.tsx ``` Expected: 4 tests pass. - [ ] **Step 6: Run TypeScript checking** Run: ```bash npm run typecheck ``` Expected: exit code 0 with no TypeScript diagnostics. ### Task 3: Update every normative OneOS V2 document **Files:** - Modify: `src/resources/design-system/DESIGN.md` - Modify: `src/resources/design-system/chapters/03-components.md` - Modify: `src/prototypes/vm-shared/DESIGN.md` - Modify: `src/common/operation-actions-spec.md` - [ ] **Step 1: Replace the global §3.16 layout summary** In `src/resources/design-system/DESIGN.md`, define: ```text [查看/详情] [编辑/处理/处置] [⋮ 更多] ↑ 固定首位 ↑ 最多一个 ↑ 历史 / 管理 / 危险操作 ``` State that detail entry is external, “查看记录 / 操作记录” remains in more, only one workflow action is external when detail exists, and two workflow actions are allowed only when there is no detail entry. - [ ] **Step 2: Update the component chapter summary** In `src/resources/design-system/chapters/03-components.md`, replace the old sentence with: ```markdown - 操作列(强制)`OperationActions`:对象入口 `[查看/详情]` 固定首位,常用 `[编辑/处理]` 外显一个;历史、低频管理与危险操作进入 `[⋮ 更多]`;列宽 148–184;推荐 `fixed: 'right'`。详见 `vm-shared/DESIGN.md`。 ``` - [ ] **Step 3: Rewrite the shared full pattern** In `src/prototypes/vm-shared/DESIGN.md`: - change the diagram to the approved three-part order; - define `查看/详情` as the object entry; - define `查看记录/操作记录` as history and keep them in more; - change the suggested width to `148–184px`; - update the example so `view={{ label: '查看详情', ... }}` opens the object and `more` contains a history action; - replace the old forbidden-pattern text with prohibitions against hiding the detail entry, exposing history as the detail entry, or showing more than two primary actions. - [ ] **Step 4: Rewrite the public component spec** In `src/common/operation-actions-spec.md`, document the same ordering and replace the example with: ```tsx openDetail(record) }} edit={canEdit ? { onClick: () => openEdit(record) } : undefined} more={[ { key: 'history', label: '操作记录', onClick: () => openHistory(record) }, ]} /> ``` - [ ] **Step 5: Scan for stale normative wording** Run: ```bash rg -n "查看详情.*更多|查看记录.*外显|低频.*查看详情|\\[编辑\\].*\\[处理\\]" \ src/resources/design-system \ src/prototypes/vm-shared/DESIGN.md \ src/common/operation-actions-spec.md ``` Expected: no statement contradicts the new rules. Historical background text is acceptable only when explicitly marked as the old behavior. ### Task 4: Migrate the vehicle asset operation column **Files:** - Modify: `src/prototypes/vehicle-management/components/ListView.tsx` - Modify: `src/prototypes/vehicle-management/style.css` - [ ] **Step 1: Import the shared component and remove private-menu imports** Add the correct relative import: ```tsx import { OperationActions } from '../../../common/OperationActions'; ``` Remove `Eye`, `MoreHorizontal`, `ShieldAlert`, and `UserCog` from Lucide imports only if no remaining usage exists after migration. Keep imports still used elsewhere in `ListView.tsx`. - [ ] **Step 2: Delete the private `ActionsMoreMenu` component** Remove the complete `ActionsMoreMenu` function, including its local `open` state, click-outside listener, escape listener, and private menu markup. - [ ] **Step 3: Replace both vehicle action-cell call sites** At each current `` site, render: ```tsx onOpenDetail(row) }} edit={{ label: '编辑', onClick: () => onEdit(row) }} more={[ { key: 'owner', label: '设置运维负责人', onClick: () => onOps(row), }, ]} /> ``` If an existing local permission flag controls edit or operations management, pass `undefined` or `hidden: true` before rendering rather than creating an empty menu. Do not introduce new permission semantics. - [ ] **Step 4: Remove the private menu skin** Delete vehicle-only selectors whose sole owner was `ActionsMoreMenu`, including: ```css .va-actions-more-wrap .va-btn-more .va-actions-menu .va-actions-menu__item .va-actions-menu__icon .va-actions-menu__text .va-actions-menu__title .va-actions-menu__sub ``` Also remove `:has(.va-actions-menu)` stacking workarounds made obsolete by the portal-based shared menu. Keep unrelated table and sticky-column rules. - [ ] **Step 5: Size and align the shared operation column** Set the vehicle column styles to: ```css .va-th-actions, .va-td-actions { min-width: 184px !important; width: 184px !important; max-width: 184px !important; box-sizing: border-box; } .va-td-actions { overflow: visible !important; vertical-align: middle !important; } .va-td-actions .vm-operation-actions { width: 100%; justify-content: flex-start; } ``` Use the same width in the `ListView.tsx` column definition or width map if it is also hard-coded there. - [ ] **Step 6: Run static validation** Run: ```bash npm run typecheck npm run test:run -- src/common/OperationActions.test.tsx npm run build ``` Expected: all commands exit 0. ### Task 5: Browser verification **Files:** - Verify: `src/prototypes/vehicle-management/components/ListView.tsx` - Verify: `src/common/OperationActions.tsx` - Verify: `src/prototypes/vehicle-management/style.css` - [ ] **Step 1: Start the local prototype** Run: ```bash npm run dev -- --host 127.0.0.1 ``` Expected: Vite prints a local URL and remains running. - [ ] **Step 2: Verify desktop behavior** Open the vehicle asset list and confirm: - every row shows `查看`, then `编辑`, then the ellipsis; - `查看` enters the same detail flow as before; - `编辑` opens the same edit flow as before; - the ellipsis menu contains only `设置运维负责人`; - the menu is not clipped by the sticky operation column or scrolling container; - buttons remain on one line and the operation column is visually stable while horizontally scrolling. - [ ] **Step 3: Verify keyboard and constrained width** At desktop and a narrow viewport: - use Tab to focus `查看`, `编辑`, and the ellipsis in order; - press Enter or Space to open the menu; - press Escape and confirm it closes; - confirm no empty ellipsis appears when a permission-controlled menu item is absent; - confirm the shared 44px hit areas remain usable and do not overlap. - [ ] **Step 4: Record verification evidence** Capture at least one desktop screenshot with the menu open and one narrow-viewport screenshot. Note the tested page path and any permission state used in the final handoff. ### Task 6: Final consistency and completion checks **Files:** - Verify: `docs/superpowers/specs/2026-07-26-global-operation-actions-design.md` - Verify: all files modified in Tasks 1–4 - [ ] **Step 1: Run the full verification suite** Run: ```bash npm run test:run npm run typecheck npm run build ``` Expected: all commands exit 0. - [ ] **Step 2: Re-run the normative consistency scan** Run: ```bash rg -n "查看详情.*更多|低频.*查看详情|查看记录.*外显" \ src/resources/design-system \ src/prototypes/vm-shared/DESIGN.md \ src/common/operation-actions-spec.md \ src/common/OperationActions.tsx ``` Expected: no contradictory active rule remains. - [ ] **Step 3: Review the final change scope** Because `/Users/sylvawong/oneos-v2` is not currently a Git repository, list changed files by explicit path and verify that `.superpowers/brainstorm/` artifacts are not included in the deliverable. Do not claim commits were created.