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

16 KiB
Raw Permalink Blame History

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:

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:

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:

  /**
   * 查看 / 详情:对象详情入口,固定在外侧首位。
   * 历史型“查看记录 / 操作记录”应通过 more 传入。
   */
  view?: OperationPrimaryAction;
  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:

  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:

const DETAIL_LABELS = new Set(['详情', '查看', '查看详情']);
const HISTORY_LABELS = new Set(['查看记录', '操作记录']);
  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:

/**
 * 列表操作列(强制):详情入口固定首位,编辑/处理外显一个,
 * 历史、低频管理和危险操作进入更多。
 */
  • Step 5: Run the focused tests

Run:

npm run test:run -- src/common/OperationActions.test.tsx

Expected: 4 tests pass.

  • Step 6: Run TypeScript checking

Run:

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:

[查看/详情] [编辑/处理/处置] [⋮ 更多]
    ↑ 固定首位       ↑ 最多一个       ↑ 历史 / 管理 / 危险操作

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:

- 操作列(强制)`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:

<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:

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:

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:

<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:

.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:

.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:

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:

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:

npm run test:run
npm run typecheck
npm run build

Expected: all commands exit 0.

  • Step 2: Re-run the normative consistency scan

Run:

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.