Files
OneOS-V2/docs/superpowers/plans/2026-07-26-global-operation-actions.md
2026-07-29 16:04:39 +08:00

500 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(
<OperationActions
view={{ label: '查看', onClick: vi.fn() }}
edit={{ label: '编辑', onClick: vi.fn() }}
more={[action('owner', '设置运维负责人')]}
/>,
);
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(
<OperationActions
view={{ label: '详情', onClick: vi.fn() }}
more={[{ ...action('owner', '设置运维负责人'), hidden: true }]}
/>,
);
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`:对象入口 `[查看/详情]` 固定首位,常用 `[编辑/处理]` 外显一个;历史、低频管理与危险操作进入 `[⋮ 更多]`;列宽 148184推荐 `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 `148184px`;
- 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
<OperationActions
view={{ label: '查看', onClick: () => 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 `<ActionsMoreMenu ... />` site, render:
```tsx
<OperationActions
view={{ label: '查看', onClick: () => 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 14
- [ ] **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.