474 lines
17 KiB
Markdown
474 lines
17 KiB
Markdown
|
|
# Event List Export 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:** Rename the existing event list export action to “事件导出”, add independent waveform export selection and download behavior, and show the visible event columns in the requested order with “暂降/暂升幅值(%)” before “持续时间(s)”.
|
||
|
|
|
||
|
|
**Architecture:** Keep all UI orchestration in `frontend/src/views/event/eventList/index.vue`, because the change is narrow and the page already owns table columns, export handlers, and row actions. Add one event-list API method and one request type for waveform export. Extend the existing eventList contract scripts so selection semantics and export payloads are checked without introducing a new test framework.
|
||
|
|
|
||
|
|
**Tech Stack:** Vue 3 `<script setup>`, Element Plus, TypeScript, existing `ProTable`, existing download hook, Node-based contract scripts.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
- Modify `frontend/src/views/event/eventList/check-search-layout-contract.mjs`: assert the event export button text and event export parameter source.
|
||
|
|
- Modify `frontend/src/views/event/eventList/check-visible-contract.mjs`: assert the waveform selection column and selection rules.
|
||
|
|
- Create `frontend/src/views/event/eventList/check-export-contract.mjs`: assert the new waveform export API, payload shape, button state, and cleanup behavior.
|
||
|
|
- Modify `frontend/src/api/event/eventList/interface/index.ts`: add `TransientWaveformExportParams`.
|
||
|
|
- Modify `frontend/src/api/event/eventList/index.ts`: add `exportTransientWaveforms`.
|
||
|
|
- Modify `frontend/src/views/event/eventList/index.vue`: add independent waveform selection UI and handlers, rename event export, and wire waveform export.
|
||
|
|
|
||
|
|
## Task 1: Contract Checks For Export Behavior
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `frontend/src/views/event/eventList/check-search-layout-contract.mjs`
|
||
|
|
- Modify: `frontend/src/views/event/eventList/check-visible-contract.mjs`
|
||
|
|
- Create: `frontend/src/views/event/eventList/check-export-contract.mjs`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Update search layout contract for event export naming**
|
||
|
|
|
||
|
|
In `frontend/src/views/event/eventList/check-search-layout-contract.mjs`, keep the existing current-search assertion and add a check that the button is now labeled “事件导出”:
|
||
|
|
|
||
|
|
```js
|
||
|
|
[
|
||
|
|
'event export button is named explicitly',
|
||
|
|
/<el-button[\s\S]*@click="handleEventExport"[\s\S]*>事件导出<\/el-button>/
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'event export uses the same current search params as the table request',
|
||
|
|
/exportTransientEvents,[\s\S]*buildEventQueryParams\(resolveCurrentSearchParams\(searchParam\)\)/
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Update visible contract for waveform selection column**
|
||
|
|
|
||
|
|
In `frontend/src/views/event/eventList/check-visible-contract.mjs`, add expectations that identify the manual waveform selection column and confirm the old business columns remain:
|
||
|
|
|
||
|
|
```js
|
||
|
|
[
|
||
|
|
'waveform selection column is rendered near the left side',
|
||
|
|
/prop:\s*'waveformSelection'[\s\S]*label:\s*'波形选择'[\s\S]*headerRender:\s*renderWaveformSelectionHeader[\s\S]*render:\s*renderWaveformSelectionCell/
|
||
|
|
],
|
||
|
|
[
|
||
|
|
'waveform selection requires event id, waveform flag and waveform path',
|
||
|
|
/const isWaveformExportable[\s\S]*Boolean\(row\.eventId\)[\s\S]*Number\(row\.fileFlag\)\s*===\s*1[\s\S]*Boolean\(row\.wavePath\)/
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Create export contract script**
|
||
|
|
|
||
|
|
Create `frontend/src/views/event/eventList/check-export-contract.mjs` with:
|
||
|
|
|
||
|
|
```js
|
||
|
|
/* eslint-env node */
|
||
|
|
import fs from 'node:fs'
|
||
|
|
import path from 'node:path'
|
||
|
|
import { fileURLToPath } from 'node:url'
|
||
|
|
|
||
|
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||
|
|
const pageFile = path.join(currentDir, 'index.vue')
|
||
|
|
const apiFile = path.resolve(currentDir, '../../../api/event/eventList/index.ts')
|
||
|
|
const interfaceFile = path.resolve(currentDir, '../../../api/event/eventList/interface/index.ts')
|
||
|
|
const source = fs.readFileSync(pageFile, 'utf8')
|
||
|
|
const apiSource = fs.readFileSync(apiFile, 'utf8')
|
||
|
|
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
|
||
|
|
|
||
|
|
const expectations = [
|
||
|
|
['waveform export api is imported by the page', /import \{[\s\S]*exportTransientWaveforms[\s\S]*\} from '@\/api\/event\/eventList'/],
|
||
|
|
['waveform export button is present and disabled without selection', /<el-button[\s\S]*:disabled="!selectedWaveformRows\.length"[\s\S]*@click="handleWaveformExport"[\s\S]*>波形导出<\/el-button>/],
|
||
|
|
['event export button does not depend on waveform selection', /<el-button type="primary" plain :icon="Download" @click="handleEventExport">事件导出<\/el-button>/],
|
||
|
|
['waveform selected rows are tracked independently', /const selectedWaveformRows\s*=\s*ref<EventList\.TransientEventRecord\[\]>\(\[\]\)/],
|
||
|
|
['waveform export payload uses event ids only', /eventIds:\s*exportableRows\.map\(row => row\.eventId\)/],
|
||
|
|
['waveform export uses server filename download hook', /useDownloadWithServerFileName\(exportTransientWaveforms,\s*'事件波形导出'/],
|
||
|
|
['waveform selection is cleaned after table reload triggers', /const clearWaveformSelection[\s\S]*selectedWaveformRows\.value\s*=\s*\[\]/],
|
||
|
|
['waveform export api method exists', /export const exportTransientWaveforms\s*=\s*\(params:\s*EventList\.TransientWaveformExportParams\)/.test(apiSource)],
|
||
|
|
['waveform export api path is stable', /downloadWithHeaders\('\/event\/list\/transient\/waveform\/export',\s*params\)/.test(apiSource)],
|
||
|
|
['waveform export params type contains eventIds', /export interface TransientWaveformExportParams\s*\{[\s\S]*eventIds:\s*string\[\][\s\S]*\}/.test(interfaceSource)]
|
||
|
|
]
|
||
|
|
|
||
|
|
const failures = expectations.filter(([, pattern]) => {
|
||
|
|
if (typeof pattern === 'boolean') return !pattern
|
||
|
|
return !pattern.test(source)
|
||
|
|
})
|
||
|
|
|
||
|
|
if (failures.length) {
|
||
|
|
console.error('eventList export contract check failed:')
|
||
|
|
for (const [name] of failures) {
|
||
|
|
console.error(`- ${name}`)
|
||
|
|
}
|
||
|
|
process.exit(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('eventList export contract check passed')
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Run contracts and verify red**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
node frontend/src/views/event/eventList/check-search-layout-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-visible-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-export-contract.mjs
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected:
|
||
|
|
|
||
|
|
- Existing checks may pass or fail depending on current user edits.
|
||
|
|
- `check-export-contract.mjs` must fail with missing waveform export API, button, selection state, and payload expectations.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Commit contract red state**
|
||
|
|
|
||
|
|
Do not commit a failing red state unless the user explicitly asks for granular commits. Keep it as local verification before implementation.
|
||
|
|
|
||
|
|
## Task 2: API Type And Export Method
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `frontend/src/api/event/eventList/interface/index.ts`
|
||
|
|
- Modify: `frontend/src/api/event/eventList/index.ts`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Add waveform export request type**
|
||
|
|
|
||
|
|
In `frontend/src/api/event/eventList/interface/index.ts`, add this interface inside `export namespace EventList` after `TransientPageParams`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
export interface TransientWaveformExportParams {
|
||
|
|
eventIds: string[]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Add waveform export API method**
|
||
|
|
|
||
|
|
In `frontend/src/api/event/eventList/index.ts`, add:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
export const exportTransientWaveforms = (params: EventList.TransientWaveformExportParams) => {
|
||
|
|
return http.downloadWithHeaders('/event/list/transient/waveform/export', params)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Run export contract and verify API checks improve**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
node frontend/src/views/event/eventList/check-export-contract.mjs
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: still fails because page UI and handlers are not implemented, but API method and type failures disappear.
|
||
|
|
|
||
|
|
## Task 3: Waveform Selection UI And State
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `frontend/src/views/event/eventList/index.vue`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Add imports**
|
||
|
|
|
||
|
|
Change imports in `index.vue`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
import { ElButton, ElCheckbox, ElRadioButton, ElRadioGroup } from 'element-plus'
|
||
|
|
import { Download, RefreshRight, View } from '@element-plus/icons-vue'
|
||
|
|
import {
|
||
|
|
exportTransientEvents,
|
||
|
|
exportTransientWaveforms,
|
||
|
|
getTransientEventDetail,
|
||
|
|
getTransientEventPage
|
||
|
|
} from '@/api/event/eventList'
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Add waveform selection state**
|
||
|
|
|
||
|
|
Add near the existing page refs:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const selectedWaveformRows = ref<EventList.TransientEventRecord[]>([])
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add exportable row helpers**
|
||
|
|
|
||
|
|
Add before `columns`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const isWaveformExportable = (row: EventList.TransientEventRecord) => {
|
||
|
|
return Boolean(row.eventId) && Number(row.fileFlag) === 1 && Boolean(row.wavePath)
|
||
|
|
}
|
||
|
|
|
||
|
|
const currentWaveformRows = computed(() => proTable.value?.tableData || [])
|
||
|
|
const currentExportableWaveformRows = computed(() => currentWaveformRows.value.filter(isWaveformExportable))
|
||
|
|
const selectedWaveformIds = computed(() => selectedWaveformRows.value.map(row => row.eventId))
|
||
|
|
const selectedWaveformIdSet = computed(() => new Set(selectedWaveformIds.value))
|
||
|
|
const isAllCurrentWaveformsSelected = computed(() => {
|
||
|
|
const rows = currentExportableWaveformRows.value
|
||
|
|
return rows.length > 0 && rows.every(row => selectedWaveformIdSet.value.has(row.eventId))
|
||
|
|
})
|
||
|
|
const isCurrentWaveformSelectionIndeterminate = computed(() => {
|
||
|
|
const rows = currentExportableWaveformRows.value
|
||
|
|
if (!rows.length) return false
|
||
|
|
const selectedCount = rows.filter(row => selectedWaveformIdSet.value.has(row.eventId)).length
|
||
|
|
return selectedCount > 0 && selectedCount < rows.length
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Add selection mutators**
|
||
|
|
|
||
|
|
Add after the helper computed values:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const clearWaveformSelection = () => {
|
||
|
|
selectedWaveformRows.value = []
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleWaveformRowSelection = (row: EventList.TransientEventRecord, checked: boolean) => {
|
||
|
|
if (!isWaveformExportable(row)) return
|
||
|
|
|
||
|
|
if (checked) {
|
||
|
|
if (!selectedWaveformIdSet.value.has(row.eventId)) {
|
||
|
|
selectedWaveformRows.value = [...selectedWaveformRows.value, row]
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
selectedWaveformRows.value = selectedWaveformRows.value.filter(item => item.eventId !== row.eventId)
|
||
|
|
}
|
||
|
|
|
||
|
|
const toggleCurrentWaveformSelection = (checked: boolean) => {
|
||
|
|
if (!checked) {
|
||
|
|
const currentIds = new Set(currentExportableWaveformRows.value.map(row => row.eventId))
|
||
|
|
selectedWaveformRows.value = selectedWaveformRows.value.filter(row => !currentIds.has(row.eventId))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
const nextRows = [...selectedWaveformRows.value]
|
||
|
|
const nextIds = new Set(nextRows.map(row => row.eventId))
|
||
|
|
currentExportableWaveformRows.value.forEach(row => {
|
||
|
|
if (!nextIds.has(row.eventId)) {
|
||
|
|
nextRows.push(row)
|
||
|
|
nextIds.add(row.eventId)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
selectedWaveformRows.value = nextRows
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 5: Add render functions for the manual selection column**
|
||
|
|
|
||
|
|
Add:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const renderWaveformSelectionHeader = () =>
|
||
|
|
h(ElCheckbox, {
|
||
|
|
modelValue: isAllCurrentWaveformsSelected.value,
|
||
|
|
indeterminate: isCurrentWaveformSelectionIndeterminate.value,
|
||
|
|
disabled: currentExportableWaveformRows.value.length === 0,
|
||
|
|
'onUpdate:modelValue': (value: string | number | boolean) => toggleCurrentWaveformSelection(Boolean(value))
|
||
|
|
})
|
||
|
|
|
||
|
|
const renderWaveformSelectionCell = ({ row }: { row: EventList.TransientEventRecord }) =>
|
||
|
|
h(ElCheckbox, {
|
||
|
|
modelValue: selectedWaveformIdSet.value.has(row.eventId),
|
||
|
|
disabled: !isWaveformExportable(row),
|
||
|
|
'onUpdate:modelValue': (value: string | number | boolean) => toggleWaveformRowSelection(row, Boolean(value))
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 6: Add waveform selection column**
|
||
|
|
|
||
|
|
In the `columns` array, add this before the index column:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
{
|
||
|
|
prop: 'waveformSelection',
|
||
|
|
label: '波形选择',
|
||
|
|
fixed: 'left',
|
||
|
|
width: 90,
|
||
|
|
isSetting: false,
|
||
|
|
headerRender: renderWaveformSelectionHeader,
|
||
|
|
render: renderWaveformSelectionCell
|
||
|
|
},
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 7: Place amplitude before duration in visible columns**
|
||
|
|
|
||
|
|
In the `columns` array, keep the visible event column order as `发生时刻`, `监测点名称`, `暂降/暂升幅值(%)`, `持续时间(s)`, `事件类型`, `相别` by placing the `featureAmplitude` column before the duration column.
|
||
|
|
|
||
|
|
- [ ] **Step 8: Clear waveform selection on reset and table request**
|
||
|
|
|
||
|
|
Update `handleSearchReset`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const handleSearchReset = () => {
|
||
|
|
eventTimeUnit.value = 'month'
|
||
|
|
eventTimeBaseDate.value = new Date()
|
||
|
|
clearWaveformSelection()
|
||
|
|
syncEventTimeRange()
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Update `getTableList`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const getTableList = (params: EventSearchParams) => {
|
||
|
|
// 分页查询按 API_DEBUG.md 转换时间范围与枚举筛选参数。
|
||
|
|
clearWaveformSelection()
|
||
|
|
return getTransientEventPage(buildEventQueryParams(resolveCurrentSearchParams(params)))
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 9: Run visible and export contracts**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
node frontend/src/views/event/eventList/check-visible-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-export-contract.mjs
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: visible contract passes for the new column; export contract still fails until buttons and export handlers are renamed/wired.
|
||
|
|
|
||
|
|
## Task 4: Export Buttons And Handlers
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `frontend/src/views/event/eventList/index.vue`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Replace table header buttons**
|
||
|
|
|
||
|
|
Replace the existing table header slot with:
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<template #tableHeader>
|
||
|
|
<el-button type="primary" plain :icon="Download" @click="handleEventExport">事件导出</el-button>
|
||
|
|
<el-button
|
||
|
|
type="primary"
|
||
|
|
plain
|
||
|
|
:icon="Download"
|
||
|
|
:disabled="!selectedWaveformRows.length"
|
||
|
|
@click="handleWaveformExport"
|
||
|
|
>
|
||
|
|
波形导出
|
||
|
|
</el-button>
|
||
|
|
</template>
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Rename event export handler**
|
||
|
|
|
||
|
|
Replace `handleExport` with:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const handleEventExport = () => {
|
||
|
|
const searchParam = (proTable.value?.searchParam || {}) as EventSearchParams
|
||
|
|
useDownloadWithServerFileName(
|
||
|
|
exportTransientEvents,
|
||
|
|
'暂态事件列表',
|
||
|
|
buildEventQueryParams(resolveCurrentSearchParams(searchParam)),
|
||
|
|
false
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 3: Add waveform export handler**
|
||
|
|
|
||
|
|
Add after `handleEventExport`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const handleWaveformExport = () => {
|
||
|
|
const exportableRows = selectedWaveformRows.value.filter(isWaveformExportable)
|
||
|
|
|
||
|
|
if (!exportableRows.length) {
|
||
|
|
ElMessage.warning('请先选择存在波形的事件')
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
useDownloadWithServerFileName(
|
||
|
|
exportTransientWaveforms,
|
||
|
|
'事件波形导出',
|
||
|
|
{
|
||
|
|
eventIds: exportableRows.map(row => row.eventId)
|
||
|
|
},
|
||
|
|
false,
|
||
|
|
'.zip'
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Run all eventList contract scripts**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
node frontend/src/views/event/eventList/check-search-layout-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-visible-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-export-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-query-params-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-route-contract.mjs
|
||
|
|
node frontend/src/views/event/eventList/check-time-range-contract.mjs
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: all scripts print `passed`.
|
||
|
|
|
||
|
|
## Task 5: Type And Lint Verification
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Verify only; no planned file edits.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Run TypeScript check**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
cd frontend
|
||
|
|
npm run type-check
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: command completes without TypeScript errors.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run lint if lightweight enough for the workspace**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
cd frontend
|
||
|
|
npm run lint
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: command completes without ESLint errors. If lint changes unrelated files because it uses `--fix`, inspect `git status --short` and keep only changes directly related to this task.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Inspect final diff**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
git diff -- frontend/src/views/event/eventList frontend/src/api/event/eventList
|
||
|
|
git status --short
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected:
|
||
|
|
|
||
|
|
- Event list page contains the new independent waveform selection column.
|
||
|
|
- API files contain only the waveform export type and method.
|
||
|
|
- Existing unrelated worktree changes are not reverted or folded into this task.
|
||
|
|
|
||
|
|
## Task 6: Commit Implementation
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Commit only files modified for this task.
|
||
|
|
|
||
|
|
- [ ] **Step 1: Stage task files only**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
git add -- frontend/src/views/event/eventList/index.vue frontend/src/views/event/eventList/check-search-layout-contract.mjs frontend/src/views/event/eventList/check-visible-contract.mjs frontend/src/views/event/eventList/check-export-contract.mjs frontend/src/api/event/eventList/index.ts frontend/src/api/event/eventList/interface/index.ts docs/superpowers/plans/2026-05-15-event-list-export-implementation-plan.md
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Commit**
|
||
|
|
|
||
|
|
Run:
|
||
|
|
|
||
|
|
```powershell
|
||
|
|
git commit -m "feat: add event waveform export selection"
|
||
|
|
```
|
||
|
|
|
||
|
|
Expected: commit succeeds and includes only the implementation, contracts, API method, and this plan.
|