diff --git a/AGENTS.md b/AGENTS.md index 65cc548553..763c7787f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ VBI/ │ ├── src/ │ └── docs/ ├── practices/ # 不同复杂度的实践示例 -│ ├── demo/ # 标准版示例 +│ ├── standard/ # 标准版示例 │ │ ├── src/ │ │ └── docs/ │ ├── minimalist/ # 极简实现示例 diff --git a/CLAUDE.md b/CLAUDE.md index dc42ebbeca..475ab5beae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ VBI/ │ ├── src/ │ └── docs/ ├── practices/ # 不同复杂度的实践示例 -│ ├── demo/ # 标准版示例 +│ ├── standard/ # 标准版示例 │ │ ├── src/ │ │ └── docs/ │ ├── minimalist/ # 极简实现示例 diff --git a/apps/vbi_fe/package.json b/apps/vbi_fe/package.json index d43a9d9c2d..36fec07e5c 100644 --- a/apps/vbi_fe/package.json +++ b/apps/vbi_fe/package.json @@ -20,7 +20,7 @@ "@visactor/vquery": "workspace:*", "@visactor/vseed": "workspace:*", "antd": "6.1.3", - "demo": "workspace:*", + "standard": "workspace:*", "react": "19.2.3", "react-dom": "19.2.3", "react-router-dom": "7.12.0", diff --git a/apps/vbi_fe/src/pages/DocumentEditorPage.tsx b/apps/vbi_fe/src/pages/DocumentEditorPage.tsx index a3df608c24..41758659f9 100644 --- a/apps/vbi_fe/src/pages/DocumentEditorPage.tsx +++ b/apps/vbi_fe/src/pages/DocumentEditorPage.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Button, Layout, Spin, Typography, Space } from 'antd'; import { ArrowLeftOutlined } from '@ant-design/icons'; -import { APP } from 'demo'; +import { APP } from 'standard'; import { useCollaborativeBuilder } from '../hooks/useCollaborativeBuilder'; import { Collaborators } from '../components/Collaborators'; diff --git a/apps/vbi_fe/tsconfig.json b/apps/vbi_fe/tsconfig.json index 849a865ce2..d2df894d18 100644 --- a/apps/vbi_fe/tsconfig.json +++ b/apps/vbi_fe/tsconfig.json @@ -38,7 +38,7 @@ "path": "../../packages/vbi" }, { - "path": "../../practices/demo" + "path": "../../practices/standard" } ], "include": ["src"] diff --git a/apps/website/docs/zh-CN/vbi/api/_meta.json b/apps/website/docs/zh-CN/vbi/api/_meta.json index d676348411..e6bf9dbd1b 100644 --- a/apps/website/docs/zh-CN/vbi/api/_meta.json +++ b/apps/website/docs/zh-CN/vbi/api/_meta.json @@ -1,61 +1,21 @@ [ { "type": "file", - "name": "builder", - "label": "builder" - }, - { - "type": "file", - "name": "chart-type", - "label": "builder.chartType", - "collapsed": true - }, - { - "type": "dir", - "name": "measures", - "label": "builder.measures", - "collapsed": true + "name": "index", + "label": "API" }, { "type": "dir", - "name": "dimensions", - "label": "builder.dimensions", - "collapsed": true + "name": "chartBuilder", + "label": "chartBuilder", + "collapsible": true, + "collapsed": false }, { "type": "dir", - "name": "where-filter", - "label": "builder.whereFilter", - "collapsed": true - }, - { - "type": "dir", - "name": "having-filter", - "label": "builder.havingFilter", - "collapsed": true - }, - { - "type": "file", - "name": "theme", - "label": "builder.theme", - "collapsed": true - }, - { - "type": "file", - "name": "locale", - "label": "builder.locale", - "collapsed": true - }, - { - "type": "file", - "name": "limit", - "label": "builder.limit", - "collapsed": true - }, - { - "type": "file", - "name": "undo-manager", - "label": "builder.undoManager", - "collapsed": true + "name": "reportBuilder", + "label": "reportBuilder", + "collapsible": true, + "collapsed": false } ] diff --git a/apps/website/docs/zh-CN/vbi/api/chartBuilder/_meta.json b/apps/website/docs/zh-CN/vbi/api/chartBuilder/_meta.json new file mode 100644 index 0000000000..a62ed36469 --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/_meta.json @@ -0,0 +1,55 @@ +[ + { + "type": "file", + "name": "chartType", + "label": "chartBuilder.chartType" + }, + { + "type": "dir", + "name": "measures", + "label": "chartBuilder.measures", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir", + "name": "dimensions", + "label": "chartBuilder.dimensions", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir", + "name": "whereFilter", + "label": "chartBuilder.whereFilter", + "collapsible": true, + "collapsed": true + }, + { + "type": "dir", + "name": "havingFilter", + "label": "chartBuilder.havingFilter", + "collapsible": true, + "collapsed": true + }, + { + "type": "file", + "name": "theme", + "label": "chartBuilder.theme" + }, + { + "type": "file", + "name": "locale", + "label": "chartBuilder.locale" + }, + { + "type": "file", + "name": "limit", + "label": "chartBuilder.limit" + }, + { + "type": "file", + "name": "undoManager", + "label": "chartBuilder.undoManager" + } +] diff --git a/apps/website/docs/zh-CN/vbi/api/chart-type.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/chartType.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/chart-type.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/chartType.md index 360ef34c35..203ba3b13e 100644 --- a/apps/website/docs/zh-CN/vbi/api/chart-type.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/chartType.md @@ -1,4 +1,4 @@ -# chartType +# ChartTypeBuilder 图表类型构建器,用于切换和获取图表类型。支持表格、柱状图、折线图、饼图、散点图等多种图表类型 diff --git a/apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/_meta.json b/apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/_meta.json new file mode 100644 index 0000000000..b2935e2dae --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "file", + "name": "dimensionNode", + "label": "dimensionNode" + } +] diff --git a/apps/website/docs/zh-CN/vbi/api/dimensions/dimension-node.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/dimensionNode.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/dimensions/dimension-node.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/dimensionNode.md index 15695b8041..92cd8ec671 100644 --- a/apps/website/docs/zh-CN/vbi/api/dimensions/dimension-node.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/dimensionNode.md @@ -2,6 +2,8 @@ 维度节点构建器,用于配置单个维度 +## 属性 + ## 方法 ### constructor diff --git a/apps/website/docs/zh-CN/vbi/api/dimensions.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/index.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/dimensions.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/index.md index 86221e4d57..e0c17ad0a3 100644 --- a/apps/website/docs/zh-CN/vbi/api/dimensions.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/dimensions/index.md @@ -1,4 +1,4 @@ -# dimensions +# DimensionsBuilder 维度构建器,用于添加、修改、删除维度配置。维度是数据的分类字段,如:时间、地区、产品类别 diff --git a/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/_meta.json b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/_meta.json new file mode 100644 index 0000000000..53d6c9dd5d --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/_meta.json @@ -0,0 +1,12 @@ +[ + { + "type": "file", + "name": "havingNode", + "label": "havingNode" + }, + { + "type": "file", + "name": "havingGroup", + "label": "havingGroup" + } +] diff --git a/apps/website/docs/zh-CN/vbi/api/having-filter/having-group.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/havingGroup.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/having-filter/having-group.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/havingGroup.md index a00bb88797..51b9ea4175 100644 --- a/apps/website/docs/zh-CN/vbi/api/having-filter/having-group.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/havingGroup.md @@ -2,6 +2,8 @@ Having 分组构建器,用于配置一组条件的逻辑关系(AND/OR) +## 属性 + ## 方法 ### constructor diff --git a/apps/website/docs/zh-CN/vbi/api/having-filter/having-node.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/havingNode.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/having-filter/having-node.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/havingNode.md index e9f6e17fd3..b8a8560cab 100644 --- a/apps/website/docs/zh-CN/vbi/api/having-filter/having-node.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/havingNode.md @@ -2,6 +2,8 @@ Having 过滤节点构建器,用于配置单个 Having 过滤条件 +## 属性 + ## 方法 ### constructor diff --git a/apps/website/docs/zh-CN/vbi/api/having-filter.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/index.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/having-filter.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/index.md index c5825a6fd9..2c2f0a6cc0 100644 --- a/apps/website/docs/zh-CN/vbi/api/having-filter.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/havingFilter/index.md @@ -1,4 +1,4 @@ -# havingFilter +# HavingFilterBuilder Having 过滤构建器,用于添加、修改、删除分组后过滤条件。Having 过滤在数据聚合后生效,用于筛选分组结果 diff --git a/apps/website/docs/zh-CN/vbi/api/builder.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/index.md similarity index 97% rename from apps/website/docs/zh-CN/vbi/api/builder.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/index.md index bbfbaf2658..98b6b678f8 100644 --- a/apps/website/docs/zh-CN/vbi/api/builder.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/index.md @@ -25,7 +25,7 @@ **定义**: ```typescript -constructor(doc: Y.Doc, options: VBIChartBuilderOptions) +constructor(doc: Y.Doc, options: VBIChartBuilderOptions, dsl: Y.Map) ``` **参数**: @@ -34,6 +34,7 @@ constructor(doc: Y.Doc, options: VBIChartBuilderOptions) | --- | --- | --- | | `doc` | Y.Doc | - | | `options` | VBIChartBuilderOptions | - | +| `dsl` | Y.Map | - | ### applyUpdate diff --git a/apps/website/docs/zh-CN/vbi/api/limit.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/limit.md similarity index 98% rename from apps/website/docs/zh-CN/vbi/api/limit.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/limit.md index 13e1255b63..5375c6c57f 100644 --- a/apps/website/docs/zh-CN/vbi/api/limit.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/limit.md @@ -1,4 +1,4 @@ -# limit +# LimitBuilder 数据量限制构建器,用于设置和获取当前 limit diff --git a/apps/website/docs/zh-CN/vbi/api/locale.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/locale.md similarity index 98% rename from apps/website/docs/zh-CN/vbi/api/locale.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/locale.md index 8da6a04d0a..4c8a5d5950 100644 --- a/apps/website/docs/zh-CN/vbi/api/locale.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/locale.md @@ -1,4 +1,4 @@ -# locale +# LocaleBuilder 语言构建器,用于设置和获取当前语言 diff --git a/apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/_meta.json b/apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/_meta.json new file mode 100644 index 0000000000..e0b2e41c98 --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/_meta.json @@ -0,0 +1,7 @@ +[ + { + "type": "file", + "name": "measureNode", + "label": "measureNode" + } +] diff --git a/apps/website/docs/zh-CN/vbi/api/measures.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/index.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/measures.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/index.md index 8e2dff8c70..b11c339ac1 100644 --- a/apps/website/docs/zh-CN/vbi/api/measures.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/index.md @@ -1,4 +1,4 @@ -# measures +# MeasuresBuilder 度量构建器,用于添加、修改、删除度量配置。度量是数据的数值字段,如:销售额、利润、数量 diff --git a/apps/website/docs/zh-CN/vbi/api/measures/measure-node.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/measureNode.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/measures/measure-node.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/measureNode.md index d62a6f32e4..50e474abaa 100644 --- a/apps/website/docs/zh-CN/vbi/api/measures/measure-node.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/measures/measureNode.md @@ -2,6 +2,8 @@ 度量节点构建器,用于配置单个度量 +## 属性 + ## 方法 ### constructor diff --git a/apps/website/docs/zh-CN/vbi/api/theme.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/theme.md similarity index 98% rename from apps/website/docs/zh-CN/vbi/api/theme.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/theme.md index be52b89cbb..3b42c53f6b 100644 --- a/apps/website/docs/zh-CN/vbi/api/theme.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/theme.md @@ -1,4 +1,4 @@ -# theme +# ThemeBuilder 主题构建器,用于设置和获取当前主题 diff --git a/apps/website/docs/zh-CN/vbi/api/undo-manager.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/undoManager.md similarity index 98% rename from apps/website/docs/zh-CN/vbi/api/undo-manager.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/undoManager.md index 8f0bb958dd..db0e4dd362 100644 --- a/apps/website/docs/zh-CN/vbi/api/undo-manager.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/undoManager.md @@ -1,4 +1,4 @@ -# undoManager +# UndoManager 撤销/重做管理器,提供基于 YJS 的撤销和重做功能,支持栈管理和历史清除操作 diff --git a/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/_meta.json b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/_meta.json new file mode 100644 index 0000000000..b64faeca92 --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/_meta.json @@ -0,0 +1,12 @@ +[ + { + "type": "file", + "name": "whereNode", + "label": "whereNode" + }, + { + "type": "file", + "name": "whereGroup", + "label": "whereGroup" + } +] diff --git a/apps/website/docs/zh-CN/vbi/api/where-filter.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/index.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/where-filter.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/index.md index 84b76fad3d..3de386d1df 100644 --- a/apps/website/docs/zh-CN/vbi/api/where-filter.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/index.md @@ -1,4 +1,4 @@ -# whereFilter +# WhereFilterBuilder Where 过滤构建器,用于添加、修改、删除行级过滤条件。Where 过滤在数据查询前生效,用于筛选原始数据 diff --git a/apps/website/docs/zh-CN/vbi/api/where-filter/where-group.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/whereGroup.md similarity index 99% rename from apps/website/docs/zh-CN/vbi/api/where-filter/where-group.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/whereGroup.md index 10b91a6780..ec20bd463d 100644 --- a/apps/website/docs/zh-CN/vbi/api/where-filter/where-group.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/whereGroup.md @@ -2,6 +2,8 @@ Where 分组构建器,用于配置一组条件的逻辑关系(AND/OR) +## 属性 + ## 方法 ### constructor diff --git a/apps/website/docs/zh-CN/vbi/api/where-filter/where-node.md b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/whereNode.md similarity index 98% rename from apps/website/docs/zh-CN/vbi/api/where-filter/where-node.md rename to apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/whereNode.md index 8209726127..d59a09fc1e 100644 --- a/apps/website/docs/zh-CN/vbi/api/where-filter/where-node.md +++ b/apps/website/docs/zh-CN/vbi/api/chartBuilder/whereFilter/whereNode.md @@ -1,7 +1,9 @@ -# WhereNodeBuilder +# WhereFilterNodeBuilder Where 过滤节点构建器,用于配置单个 Where 过滤条件 +## 属性 + ## 方法 ### constructor diff --git a/apps/website/docs/zh-CN/vbi/api/dimensions/_meta.json b/apps/website/docs/zh-CN/vbi/api/dimensions/_meta.json deleted file mode 100644 index 2ebc47c154..0000000000 --- a/apps/website/docs/zh-CN/vbi/api/dimensions/_meta.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "type": "file", - "name": "dimension-node", - "label": "dimension-node", - "collapsed": true - } -] diff --git a/apps/website/docs/zh-CN/vbi/api/having-filter/_meta.json b/apps/website/docs/zh-CN/vbi/api/having-filter/_meta.json deleted file mode 100644 index 43797f8ebd..0000000000 --- a/apps/website/docs/zh-CN/vbi/api/having-filter/_meta.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "type": "file", - "name": "having-node", - "label": "having-node", - "collapsed": true - }, - { - "type": "file", - "name": "having-group", - "label": "having-group", - "collapsed": true - } -] diff --git a/apps/website/docs/zh-CN/vbi/api/index.md b/apps/website/docs/zh-CN/vbi/api/index.md index 41fc3a5f6d..1c97e50075 100644 --- a/apps/website/docs/zh-CN/vbi/api/index.md +++ b/apps/website/docs/zh-CN/vbi/api/index.md @@ -1,3 +1,4 @@ --- overview: true +title: API --- diff --git a/apps/website/docs/zh-CN/vbi/api/measures/_meta.json b/apps/website/docs/zh-CN/vbi/api/measures/_meta.json deleted file mode 100644 index c698d8794a..0000000000 --- a/apps/website/docs/zh-CN/vbi/api/measures/_meta.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "type": "file", - "name": "measure-node", - "label": "measure-node", - "collapsed": true - } -] diff --git a/apps/website/docs/zh-CN/vbi/api/reportBuilder/_meta.json b/apps/website/docs/zh-CN/vbi/api/reportBuilder/_meta.json new file mode 100644 index 0000000000..6f8413591c --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/reportBuilder/_meta.json @@ -0,0 +1,9 @@ +[ + { + "type": "dir", + "name": "page", + "label": "reportBuilder.page", + "collapsible": true, + "collapsed": true + } +] diff --git a/apps/website/docs/zh-CN/vbi/api/reportBuilder/index.md b/apps/website/docs/zh-CN/vbi/api/reportBuilder/index.md new file mode 100644 index 0000000000..8ad31032ac --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/reportBuilder/index.md @@ -0,0 +1,81 @@ +# VBIReportBuilder + +## 属性 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| **doc** | `Y.Doc` | - | +| **dsl** | `Y.Map` | - | +| **undoManager** | `UndoManager` | - | +| **page** | `ReportPageCollectionBuilder` | - | + + +## 方法 + +### constructor + +**定义**: + +```typescript +constructor(doc: Y.Doc, options: VBIReportBuilderOptions) +``` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `doc` | Y.Doc | - | +| `options` | VBIReportBuilderOptions | - | + +### applyUpdate + +**定义**: + +```typescript +applyUpdate(update: Uint8Array, transactionOrigin: any): any +``` + +**返回**: `any` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `update` | Uint8Array | - | +| `transactionOrigin` | any | - | + +### encodeStateAsUpdate + +**定义**: + +```typescript +encodeStateAsUpdate(targetStateVector: Uint8Array): any +``` + +**返回**: `any` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `targetStateVector` | Uint8Array | - | + +### build + +**定义**: + +```typescript +build(): VBIReportDSL +``` + +**返回**: `VBIReportDSL` + +### isEmpty + +**定义**: + +```typescript +isEmpty(): boolean +``` + +**返回**: `boolean` \ No newline at end of file diff --git a/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/_meta.json b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/_meta.json new file mode 100644 index 0000000000..73e9b2937a --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/_meta.json @@ -0,0 +1,12 @@ +[ + { + "type": "file", + "name": "reportPage", + "label": "reportPage" + }, + { + "type": "file", + "name": "reportText", + "label": "reportText" + } +] diff --git a/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/index.md b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/index.md new file mode 100644 index 0000000000..610c781ad8 --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/index.md @@ -0,0 +1,88 @@ +# ReportPageCollectionBuilder + +## 属性 + +## 方法 + +### constructor + +**定义**: + +```typescript +constructor(parent: VBIReportBuilder, doc: Y.Doc, dsl: Y.Map, options: VBIReportBuilderOptions) +``` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `parent` | VBIReportBuilder | - | +| `doc` | Y.Doc | - | +| `dsl` | Y.Map | - | +| `options` | VBIReportBuilderOptions | - | + +### add + +**定义**: + +```typescript +add(title: string, callback: (page: ReportPageBuilder) => void): VBIReportBuilder +``` + +**返回**: `VBIReportBuilder` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `title` | string | - | +| `callback` | (page: ReportPageBuilder) => void | - | + +### remove + +**定义**: + +```typescript +remove(pageId: string): VBIReportBuilder +``` + +**返回**: `VBIReportBuilder` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `pageId` | string | - | + +### update + +**定义**: + +```typescript +update(pageId: string, callback: (page: ReportPageBuilder) => void): VBIReportBuilder +``` + +**返回**: `VBIReportBuilder` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `pageId` | string | - | +| `callback` | (page: ReportPageBuilder) => void | - | + +### get + +**定义**: + +```typescript +get(pageId: string): ReportPageBuilder | undefined +``` + +**返回**: `ReportPageBuilder \| undefined` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `pageId` | string | - | \ No newline at end of file diff --git a/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/reportPage.md b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/reportPage.md new file mode 100644 index 0000000000..aef2b1eb60 --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/reportPage.md @@ -0,0 +1,95 @@ +# ReportPageBuilder + +## 属性 + +| 属性 | 类型 | 说明 | +| --- | --- | --- | +| **chart** | `VBIChartBuilder` | - | +| **text** | `ReportTextBuilder` | - | + + +## 方法 + +### constructor + +**定义**: + +```typescript +constructor(doc: Y.Doc, page: Y.Map, chartOptions: VBIChartBuilderOptions) +``` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `doc` | Y.Doc | - | +| `page` | Y.Map | - | +| `chartOptions` | VBIChartBuilderOptions | - | + +### getId + +**定义**: + +```typescript +getId(): string +``` + +**返回**: `string` + +### setTitle + +**定义**: + +```typescript +setTitle(title: string): this +``` + +**返回**: `this` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `title` | string | - | + +### setChart + +**定义**: + +```typescript +setChart(chartBuilder: VBIChartBuilder | VBIChartDSLInput): this +``` + +**返回**: `this` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `chartBuilder` | VBIChartBuilder \| VBIChartDSLInput | - | + +### setText + +**定义**: + +```typescript +setText(content: string): this +``` + +**返回**: `this` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `content` | string | - | + +### toJSON + +**定义**: + +```typescript +toJSON(): VBIReportPageDSL +``` + +**返回**: `VBIReportPageDSL` \ No newline at end of file diff --git a/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/reportText.md b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/reportText.md new file mode 100644 index 0000000000..49c038220d --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/api/reportBuilder/page/reportText.md @@ -0,0 +1,65 @@ +# ReportTextBuilder + +## 属性 + +## 方法 + +### constructor + +**定义**: + +```typescript +constructor(yMap: Y.Map) +``` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `yMap` | Y.Map | - | + +### getContent + +**定义**: + +```typescript +getContent(): string +``` + +**返回**: `string` + +### setContent + +**定义**: + +```typescript +setContent(content: string): this +``` + +**返回**: `this` + +**参数**: + +| 参数 | 类型 | 说明 | +| --- | --- | --- | +| `content` | string | - | + +### clear + +**定义**: + +```typescript +clear(): this +``` + +**返回**: `this` + +### toJSON + +**定义**: + +```typescript +toJSON(): VBIReportTextDSL +``` + +**返回**: `VBIReportTextDSL` \ No newline at end of file diff --git a/apps/website/docs/zh-CN/vbi/api/where-filter/_meta.json b/apps/website/docs/zh-CN/vbi/api/where-filter/_meta.json deleted file mode 100644 index 1dcf71bfbc..0000000000 --- a/apps/website/docs/zh-CN/vbi/api/where-filter/_meta.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "type": "file", - "name": "where-node", - "label": "where-node", - "collapsed": true - }, - { - "type": "file", - "name": "where-group", - "label": "where-group", - "collapsed": true - } -] diff --git a/apps/website/docs/zh-CN/vbi/practices/_meta.json b/apps/website/docs/zh-CN/vbi/practices/_meta.json index 0d866e2108..687a04756b 100644 --- a/apps/website/docs/zh-CN/vbi/practices/_meta.json +++ b/apps/website/docs/zh-CN/vbi/practices/_meta.json @@ -1,5 +1,6 @@ [ { "type": "file", "name": "standard", "label": "标准" }, + { "type": "file", "name": "standard-report", "label": "标准报表" }, { "type": "file", "name": "minimalist", "label": "极简" }, { "type": "file", "name": "streamlined", "label": "精简" }, { "type": "file", "name": "professional", "label": "专业" }, diff --git a/apps/website/docs/zh-CN/vbi/practices/standard-report.mdx b/apps/website/docs/zh-CN/vbi/practices/standard-report.mdx new file mode 100644 index 0000000000..65b7d9d549 --- /dev/null +++ b/apps/website/docs/zh-CN/vbi/practices/standard-report.mdx @@ -0,0 +1,7 @@ +# Standard Report + +import { APP } from 'standard-report' + +
+ +
diff --git a/apps/website/docs/zh-CN/vbi/practices/standard.mdx b/apps/website/docs/zh-CN/vbi/practices/standard.mdx index 9bfdd97556..f43f8c50cc 100644 --- a/apps/website/docs/zh-CN/vbi/practices/standard.mdx +++ b/apps/website/docs/zh-CN/vbi/practices/standard.mdx @@ -1,6 +1,6 @@ # Standard -import { APP } from 'demo' +import { APP } from 'standard'
diff --git a/apps/website/package.json b/apps/website/package.json index c85c1a1dd2..7de62a8da8 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -18,7 +18,8 @@ "@visactor/vquery": "workspace:*", "@visactor/vseed": "workspace:*", "@visactor/vtable": "1.23.1", - "demo": "workspace:*", + "standard": "workspace:*", + "standard-report": "workspace:*", "minimalist": "workspace:*", "professional": "workspace:*", "streamlined": "workspace:*", diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json index c4c94afd0b..0f13d3c378 100644 --- a/apps/website/tsconfig.json +++ b/apps/website/tsconfig.json @@ -29,7 +29,10 @@ "path": "../../packages/vbi" }, { - "path": "../../practices/demo" + "path": "../../practices/standard" + }, + { + "path": "../../practices/standard-report" }, { "path": "../../practices/minimalist" diff --git a/packages/vbi-react/DESIGN.md b/packages/vbi-react/DESIGN.md index fb2430c6f3..bee016228b 100644 --- a/packages/vbi-react/DESIGN.md +++ b/packages/vbi-react/DESIGN.md @@ -122,9 +122,9 @@ public buildVSeed = async (options?: { signal?: AbortSignal }): Promise { ## 11. Migration Strategy -### 11.1 From practices/demo +### 11.1 From practices/standard **Existing Code**: ```typescript -// practices/demo/src/hooks/useVBI.ts +// practices/standard/src/hooks/useVBI.ts import { useState, useEffect } from 'react' import { VBIChartBuilder } from '@visactor/vbi' @@ -1142,7 +1142,7 @@ const { dsl, builder } = useVBI(builder) 3. **Phase 3: Extract a standalone validation demo** - Extract the current starter preview into `practices/vbi-react-starter` - Restore `practices/professional` to its original role instead of keeping it as the long-term `vbi-react` showcase - - Keep `practices/demo`, `practices/minimalist`, and `practices/streamlined` unchanged during this rollout + - Keep `practices/standard`, `practices/minimalist`, and `practices/streamlined` unchanged during this rollout 4. **Phase 4: Documentation and release** - Reuse the completed components as website examples @@ -1158,7 +1158,7 @@ Rules for that stage: - `practices/vbi-react-starter` is the designated demo for validating the components layer end to end - `practices/professional` may still be used later for regression spot-checks, but it should not remain the primary `vbi-react` showcase - Current rollout status: `practices/vbi-react-starter` now exists as the standalone validation demo, and `practices/professional` is no longer the long-term host for the component showcase -- Do **not** modify `practices/demo`, `practices/minimalist`, or `practices/streamlined` as part of the components rollout unless a later explicit decision changes this scope +- Do **not** modify `practices/standard`, `practices/minimalist`, or `practices/streamlined` as part of the components rollout unless a later explicit decision changes this scope - Restoring `practices/professional` back toward its original pre-`vbi-react` role is allowed during this extraction - This scope supersedes the earlier plan that used `practices/professional` as the permanent first-line integration target @@ -1179,7 +1179,7 @@ This design will be integrated into the VBI documentation website under the **VB | **Hooks API** | Detailed hook documentation with examples | | **Components** | Prebuilt component reference | | **Examples** | Common usage patterns | -| **Migration Guide** | Upgrading from practices/demo | +| **Migration Guide** | Upgrading from practices/standard | ### 12.3 Design Document Integration @@ -1241,15 +1241,15 @@ This design document will become part of the architecture documentation, providi ### Phase 4: Integration and Documentation (1 week) -| Week | Task | Deliverable | Status | -| ---- | ------------------------------------------------------------------------------------------------------------------------ | -------------------- | --------------- | -| 7 | Restore `practices/professional` to its original role | legacy professional | **Completed** | -| 7 | Extract `practices/vbi-react-starter` for components validation | standalone demo | **Completed** | -| 7 | Keep `practices/demo`, `practices/minimalist`, and `practices/streamlined` unchanged during the first components rollout | scope guard | **In Progress** | -| 7 | Write README documentation | Documentation | **Pending** | -| 7 | Integrate docs under VBI website section | documentation pages | **In Progress** | -| 7 | Keep `DESIGN.md` synchronized with implementation status | living design record | **In Progress** | -| 7 | Publish Beta version | npm package | **Pending** | +| Week | Task | Deliverable | Status | +| ---- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------- | --------------- | +| 7 | Restore `practices/professional` to its original role | legacy professional | **Completed** | +| 7 | Extract `practices/vbi-react-starter` for components validation | standalone demo | **Completed** | +| 7 | Keep `practices/standard`, `practices/minimalist`, and `practices/streamlined` unchanged during the first components rollout | scope guard | **In Progress** | +| 7 | Write README documentation | Documentation | **Pending** | +| 7 | Integrate docs under VBI website section | documentation pages | **In Progress** | +| 7 | Keep `DESIGN.md` synchronized with implementation status | living design record | **In Progress** | +| 7 | Publish Beta version | npm package | **Pending** | --- @@ -1266,7 +1266,7 @@ This design document will become part of the architecture documentation, providi | **Testing** | Hook unit tests and starter component interaction tests are active today; richer snapshot coverage can be added later | | **Key Challenges** | Yjs lifecycle management, useSyncExternalStore adaptation | | **Estimated Timeline** | 7 weeks for MVP | -| **Backward Compatibility** | Smooth migration from practices/demo | +| **Backward Compatibility** | Smooth migration from practices/standard | | **Documentation Discipline** | DESIGN.md tracks completed vs pending work continuously | --- diff --git a/packages/vbi/docs/2026-03-19-measure-format/adr.md b/packages/vbi/docs/2026-03-19-measure-format/adr.md index 92e4d271ba..bbdbe545b2 100644 --- a/packages/vbi/docs/2026-03-19-measure-format/adr.md +++ b/packages/vbi/docs/2026-03-19-measure-format/adr.md @@ -11,7 +11,7 @@ VSeed 的 `Measure` 已支持数值格式相关配置: - `autoFormat?: boolean` - `numFormat?: NumFormat` -当前 VBI 还没有把这组能力完整接通到自己的 DSL、builder 和 `buildVSeed` 适配层;`practices/demo` 的 measure shelf 也还不能在 UI 上编辑格式配置。 +当前 VBI 还没有把这组能力完整接通到自己的 DSL、builder 和 `buildVSeed` 适配层;`practices/standard` 的 measure shelf 也还不能在 UI 上编辑格式配置。 本 ADR 的目标是将 measure format 能力以单一接口接入 VBI,并补齐到 Demo UI。 @@ -108,13 +108,13 @@ VBI 不向 VSeed 透传 `format` 字段本身。 VBI 的职责是表达统一配置,并在适配阶段映射为 VSeed 所需字段;VBI 不实现 formatter 创建逻辑,也不复制 VSeed 的格式化规则。 -### 5. `practices/demo` 需要补齐格式设置 UI +### 5. `practices/standard` 需要补齐格式设置 UI Demo 需要把这组能力暴露到 measure shelf 的交互中。 接入方式: -1. 在 `practices/demo/src/components/Shelfs/shelves/MeasureShelf.tsx` 的 measure 菜单中新增 `Format` 入口 +1. 在 `practices/standard/src/components/Shelfs/shelves/MeasureShelf.tsx` 的 measure 菜单中新增 `Format` 入口 2. `Format` 不做多级 submenu,改为打开独立弹窗 3. 弹窗沿用当前 shelf 交互模式,建议新增一个与 `openShelfRenameModal` 同级的 `openMeasureFormatModal` @@ -134,8 +134,8 @@ UI 结构: 对应配套改动: -1. `practices/demo/src/hooks/useVBIMeasures.ts` 中的 `MeasureNodeLike` 需要补上 `setFormat` / `getFormat` / `clearFormat` -2. `practices/demo/src/i18n/locales/zh-CN.json` 和 `practices/demo/src/i18n/locales/en-US.json` 需要新增 format 相关文案 +1. `practices/standard/src/hooks/useVBIMeasures.ts` 中的 `MeasureNodeLike` 需要补上 `setFormat` / `getFormat` / `clearFormat` +2. `practices/standard/src/i18n/locales/zh-CN.json` 和 `practices/standard/src/i18n/locales/en-US.json` 需要新增 format 相关文案 3. 如需在列表中提示当前状态,可在 measure 标签上补一个轻量摘要,例如“自动”或“自定义”,但这不是首批必需项 ### 6. 测试范围 @@ -174,8 +174,8 @@ Demo 测试覆盖以下内容: - VBI `MeasureNodeBuilder`: `packages/vbi/src/builder/features/measures/mea-node-builder.ts` - VBI `MeasuresBuilder`: `packages/vbi/src/builder/features/measures/mea-builder.ts` - VBI `buildVSeed`: `packages/vbi/src/builder/adapters/vquery-vseed/build-vseed.ts` -- Demo measure shelf: `practices/demo/src/components/Shelfs/shelves/MeasureShelf.tsx` -- Demo measures hook: `practices/demo/src/hooks/useVBIMeasures.ts` +- Demo measure shelf: `practices/standard/src/components/Shelfs/shelves/MeasureShelf.tsx` +- Demo measures hook: `practices/standard/src/hooks/useVBIMeasures.ts` ## 淘汰内容概述 diff --git a/packages/vbi/docs/2026-03-19-measure-format/plan.md b/packages/vbi/docs/2026-03-19-measure-format/plan.md index 8f0b7b4e8a..0778acad5b 100644 --- a/packages/vbi/docs/2026-03-19-measure-format/plan.md +++ b/packages/vbi/docs/2026-03-19-measure-format/plan.md @@ -5,7 +5,7 @@ ## 范围 -本计划聚焦 VBI 核心包(`packages/vbi`),不含 `practices/demo` UI 改造。 +本计划聚焦 VBI 核心包(`packages/vbi`),不含 `practices/standard` UI 改造。 ## Phase 1: 类型定义 diff --git a/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/adr.md b/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/adr.md index 28436d8ec8..0720add508 100644 --- a/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/adr.md +++ b/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/adr.md @@ -12,10 +12,10 @@ Proposed 2. 无法稳定表达“2024 年”“2024-Q1”“2024-03”这类自然周期筛选。 3. 旧草案中的 `dateOp`、`dateValue`、`granularity` 是平铺字段,容易产生非法组合,也会把日期语义拆散。 -同时,`practices/demo` 当前的 `where` UI 仍然是纯标量表单: +同时,`practices/standard` 当前的 `where` UI 仍然是纯标量表单: -1. [`FilterPanel.tsx`](../../practices/demo/src/components/Filter/FilterPanel.tsx) 只支持通用比较表单、`range`、`tags` 这类输入策略。 -2. [`WhereShelf.tsx`](../../practices/demo/src/components/Shelfs/shelves/WhereShelf.tsx) 和 [`useVBIWhereFilter.ts`](../../practices/demo/src/hooks/useVBIWhereFilter.ts) 只知道 `setOperator(...)` / `setValue(...)`。 +1. [`FilterPanel.tsx`](../../practices/standard/src/components/Filter/FilterPanel.tsx) 只支持通用比较表单、`range`、`tags` 这类输入策略。 +2. [`WhereShelf.tsx`](../../practices/standard/src/components/Shelfs/shelves/WhereShelf.tsx) 和 [`useVBIWhereFilter.ts`](../../practices/standard/src/hooks/useVBIWhereFilter.ts) 只知道 `setOperator(...)` / `setValue(...)`。 3. 日期维度虽然已有 `isDate` 标记,但还没有独立的日期筛选编辑器。 底层 `vquery` 已经补齐了当前阶段需要依赖的基础验证: @@ -296,7 +296,7 @@ const zVBIWhereScalarFilter = z.object({ export const zVBIWhereFilter = z.union([zVBIWhereDateFilter, zVBIWhereScalarFilter]) ``` -### 8. `practices/demo` UI 采用“同一入口、日期分流”的编辑方案 +### 8. `practices/standard` UI 采用“同一入口、日期分流”的编辑方案 UI 目标不是在现有标量表单里硬塞更多字段,而是在同一个 `where` 编辑入口里,对日期字段切换到独立的日期编辑模式。 @@ -309,7 +309,7 @@ UI 目标不是在现有标量表单里硬塞更多字段,而是在同一个 ` #### 建议的表单模型 -`practices/demo` 内部可以继续复用现有 `FilterItem` 外形,但在类型上要把日期分支单独表达出来: +`practices/standard` 内部可以继续复用现有 `FilterItem` 外形,但在类型上要把日期分支单独表达出来: ```typescript type DemoWhereScalarFilterItem = { @@ -353,21 +353,21 @@ type DemoWhereFilterItem = DemoWhereScalarFilterItem | DemoWhereDateFilterItem #### 组件改造点 -- [`practices/demo/src/components/Filter/FilterPanel.tsx`](../../practices/demo/src/components/Filter/FilterPanel.tsx) +- [`practices/standard/src/components/Filter/FilterPanel.tsx`](../../practices/standard/src/components/Filter/FilterPanel.tsx) - 根据 `field.isDate` 分流为标量编辑器或日期编辑器。 - 日期编辑器使用 `type` 选择器驱动动态子表单。 - 提交时,如果是日期模式,输出 `{ op: 'date', value: DatePredicate }`。 -- [`practices/demo/src/components/Filter/whereFilterUtils.ts`](../../practices/demo/src/components/Filter/whereFilterUtils.ts) +- [`practices/standard/src/components/Filter/whereFilterUtils.ts`](../../practices/standard/src/components/Filter/whereFilterUtils.ts) - 保留现有标量 operator/input strategy 工具函数。 - 新增日期表单的默认值、序列化、反序列化、展示文案生成工具。 - `getWhereDisplayText(...)` 需要支持 `op === 'date'` 的可读展示。 -- [`practices/demo/src/components/Shelfs/shelves/WhereShelf.tsx`](../../practices/demo/src/components/Shelfs/shelves/WhereShelf.tsx) +- [`practices/standard/src/components/Shelfs/shelves/WhereShelf.tsx`](../../practices/standard/src/components/Shelfs/shelves/WhereShelf.tsx) - 新增 `item.op === 'date'` 分支。 - add/update 时,日期节点改用 `node.setDate(...)`,非日期节点继续用 `setOperator(...)` / `setValue(...)`。 -- [`practices/demo/src/hooks/useVBIWhereFilter.ts`](../../practices/demo/src/hooks/useVBIWhereFilter.ts) +- [`practices/standard/src/hooks/useVBIWhereFilter.ts`](../../practices/standard/src/hooks/useVBIWhereFilter.ts) - mutator typing 增加 `setDate(...)`。 - `VBIWhereFilter` 命名同步替代旧的 `VBIFilter`。 @@ -383,7 +383,7 @@ type DemoWhereFilterItem = DemoWhereScalarFilterItem | DemoWhereDateFilterItem #### UI 测试要求 -`practices/demo` 至少需要补: +`practices/standard` 至少需要补: 1. 日期 filter 从 builder 值回填到表单的测试。 2. 日期表单序列化为 `{ op: 'date', value: DatePredicate }` 的测试。 @@ -412,7 +412,7 @@ type DemoWhereFilterItem = DemoWhereScalarFilterItem | DemoWhereDateFilterItem #### Demo 必补测试 -在 `practices/demo` UI 合入前,必须新增并通过: +在 `practices/standard` UI 合入前,必须新增并通过: 1. `whereFilterUtils` 的日期序列化与展示文案测试。 2. `FilterPanel` 的日期编辑模式测试。 @@ -425,13 +425,13 @@ type DemoWhereFilterItem = DemoWhereScalarFilterItem | DemoWhereDateFilterItem 1. `whereFilter` 的类型命名与模块风格统一。 2. 日期筛选 DSL 收敛为一个稳定入口,不再有平铺字段拼装。 3. builder API 保持克制,复杂度集中在 `DatePredicate`。 -4. `practices/demo` 能在不复制 DSL 的前提下,支持四类日期筛选器。 +4. `practices/standard` 能在不复制 DSL 的前提下,支持四类日期筛选器。 5. lowering 目标完全依赖已验证的 `vquery` 基础能力。 ### Negative 1. VBI 需要承担日期语义解析和时间上下文管理职责。 -2. `practices/demo` 的表单状态会从单一路径变成标量模式和日期模式两条路径。 +2. `practices/standard` 的表单状态会从单一路径变成标量模式和日期模式两条路径。 3. 日期边界行为如果没有测试锁定,风险会直接体现在 query 结果上。 ## Implementation Impact @@ -442,23 +442,23 @@ type DemoWhereFilterItem = DemoWhereScalarFilterItem | DemoWhereDateFilterItem - `packages/vbi/src/builder/features/whereFilter/where-node-builder.ts` - `packages/vbi/src/pipeline/vqueryDSL/buildWhere.ts` - `packages/vbi/tests/builder/features/whereFilter.test.ts` -- `practices/demo/src/components/Filter/FilterPanel.tsx` -- `practices/demo/src/components/Filter/whereFilterUtils.ts` -- `practices/demo/src/components/Shelfs/shelves/WhereShelf.tsx` -- `practices/demo/src/hooks/useVBIWhereFilter.ts` -- `practices/demo/src/i18n/locales/zh-CN.json` -- `practices/demo/src/i18n/locales/en-US.json` -- `practices/demo/tests/whereFilterUtils.test.ts` +- `practices/standard/src/components/Filter/FilterPanel.tsx` +- `practices/standard/src/components/Filter/whereFilterUtils.ts` +- `practices/standard/src/components/Shelfs/shelves/WhereShelf.tsx` +- `practices/standard/src/hooks/useVBIWhereFilter.ts` +- `practices/standard/src/i18n/locales/zh-CN.json` +- `practices/standard/src/i18n/locales/en-US.json` +- `practices/standard/tests/whereFilterUtils.test.ts` ## Reference - VBI WhereFilter Types: `packages/vbi/src/types/dsl/whereFilter/filters.ts` - VBI WhereFilter Node Builder: `packages/vbi/src/builder/features/whereFilter/where-node-builder.ts` - VBI buildWhere: `packages/vbi/src/pipeline/vqueryDSL/buildWhere.ts` -- Demo Filter Panel: `practices/demo/src/components/Filter/FilterPanel.tsx` -- Demo Where Utils: `practices/demo/src/components/Filter/whereFilterUtils.ts` -- Demo Where Shelf: `practices/demo/src/components/Shelfs/shelves/WhereShelf.tsx` -- Demo Where Hook: `practices/demo/src/hooks/useVBIWhereFilter.ts` +- Demo Filter Panel: `practices/standard/src/components/Filter/FilterPanel.tsx` +- Demo Where Utils: `practices/standard/src/components/Filter/whereFilterUtils.ts` +- Demo Where Shelf: `practices/standard/src/components/Shelfs/shelves/WhereShelf.tsx` +- Demo Where Hook: `practices/standard/src/hooks/useVBIWhereFilter.ts` - VQuery Where Tests: `packages/vquery/tests/unit/sql-builder/builders/where.test.ts` ## 淘汰内容简述 diff --git a/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/plan.md b/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/plan.md index ff77b7b74d..6f213ba621 100644 --- a/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/plan.md +++ b/packages/vbi/docs/2026-03-19-vbi-date-dimension-filter/plan.md @@ -5,7 +5,7 @@ ## 范围 -本计划聚焦 VBI 核心包(`packages/vbi`),不含 `practices/demo` UI 改造。 +本计划聚焦 VBI 核心包(`packages/vbi`),不含 `practices/standard` UI 改造。 ## Phase 1: 类型定义与 Schema diff --git a/packages/vbi/docs/2026-03-23-mea-dim-sort/plan.md b/packages/vbi/docs/2026-03-23-mea-dim-sort/plan.md index 95ea21c036..0888c36272 100644 --- a/packages/vbi/docs/2026-03-23-mea-dim-sort/plan.md +++ b/packages/vbi/docs/2026-03-23-mea-dim-sort/plan.md @@ -5,7 +5,7 @@ ## 范围 -本计划聚焦 VBI 核心包(`packages/vbi`),只处理排序 DSL、builder、`buildVQuery().orderBy` lowering,以及由默认排序引起的生成物和快照更新;不包含 `practices/demo` UI 改造。 +本计划聚焦 VBI 核心包(`packages/vbi`),只处理排序 DSL、builder、`buildVQuery().orderBy` lowering,以及由默认排序引起的生成物和快照更新;不包含 `practices/standard` UI 改造。 ## Phase 1: 排序类型与 Schema diff --git a/packages/vbi/docs/2026-03-24-create-report/adr.md b/packages/vbi/docs/2026-03-24-create-report/adr.md new file mode 100644 index 0000000000..4dcbe32473 --- /dev/null +++ b/packages/vbi/docs/2026-03-24-create-report/adr.md @@ -0,0 +1,116 @@ +# ADR-006: VBI Report DSL 与 ReportBuilder + +## Context + +`ADR-005` 已把单图表能力收敛为 `createChart`、`VBIChartBuilder`、`VBIChartDSL`,现在可以在不混淆命名的前提下引入 report。 + +本任务只解决 `packages/vbi` 内的 report 建模与 builder 设计: + +1. 新增 `VBI.createReport(...)`,让 report 成为和 chart 平级的一等入口。 +2. 一个 `report` 包含多个 `page`,一个 `page` 固定包含一个 `chart` 和一个 `text`。 +3. `VBIReportDSL`、zod schema、空 DSL helper 的风格与 `VBIChartDSL` 保持一致。 +4. `reportBuilder` 的使用、实现、协同编辑方式尽量复用 `chartBuilder`。 + +这里有三个直接风险: + +1. report 根节点重复定义 chart 字段,会破坏 Single Source of Truth。 +2. 一开始就抽象成通用 widgets/layout system,会明显超出当前需求。 +3. report 另起一套 chart 编辑逻辑,chart/report 很快会发生行为漂移。 + +## Decision + +### 1. 根入口统一为 `createReport(...)` + +新增并文档化以下入口: + +```ts +VBI.createReport(vbiReport, options) +createVBI(...).createReport(vbiReport, options) +``` + +对外命名统一为 `VBIReportDSL` / `VBIReportDSLInput` / `zVBIReportDSL` / `generateEmptyReportDSL` / `generateEmptyReportPageDSL` / `VBIReportBuilder` / `VBIReportBuilderInterface` / `VBIReportBuilderOptions`。`chart` 和 `report` 是 `VBI` 下的两个平级能力。 + +### 2. `types/dsl` 改名为 `types/chartDSL`,并与 `types/reportDSL` 平级 + +当前 `types/dsl` 实际承载的是 chart 领域模型,不适合在引入 report 后继续占用泛化名字。目录应调整为: + +1. `types/chartDSL/*`:现有 `VBIChartDSL` 及其子节点 schema。 +2. `types/reportDSL/*`:新增 `VBIReportDSL`、`VBIReportPageDSL`、`VBIReportTextDSL`。 +3. `types/index.ts` 只负责聚合导出,不再让 `chart` 和 `report` 共用 `dsl` 这个模糊目录名。 + +### 3. Report DSL 采用固定 page 结构,不做通用 block union + +首期 DSL 固定为: + +```ts +type VBIReportTextDSL = { content: string } +type VBIReportPageDSL = { id: string; title: string; chart: VBIChartDSL; text: VBIReportTextDSL } +type VBIReportDSL = { pages: VBIReportPageDSL[]; version: number } +``` + +约束如下: + +1. `pages` 必须是数组,因为页面顺序本身就是业务语义。 +2. `page.title` 是 page 的显示名,`page.add('Story One', ...)` 的第一个参数直接写入这里。 +3. `page.chart` 直接内嵌 `VBIChartDSL`,它是图表配置的唯一事实来源。 +4. `page.text` 先保持极简对象 `{ content }`,不在首期引入 rich-text schema。 +5. `report` 根节点不新增 `connectorId`,避免和 `page.chart.connectorId` 双写。 + +### 4. 默认值与空 DSL helper 保持 chart 风格 + +首期 helper 约定如下: + +```ts +generateEmptyReportDSL() => { pages: [], version: 0 } +generateEmptyReportPageDSL(connectorId) => ({ id, title: '', chart: generateEmptyChartDSL(connectorId), text: { content: '' } }) +``` + +`zVBIReportDSL` 与 `zVBIReportPageDSL` 也应使用同风格默认值,让 `build()` 产物保持稳定、最小、可预测。 + +### 5. `VBIReportBuilder` 提供 `reportBuilder.page.*`;图表 lowering 仍由 page.chart 负责 + +首期 builder 结构固定为: + +```ts +class VBIReportBuilder { + page: ReportPageCollectionBuilder + build(): VBIReportDSL + isEmpty(): boolean + applyUpdate(...) + encodeStateAsUpdate(...) +} +class ReportPageBuilder { + setChart(chartBuilder: VBIChartBuilder): this + setText(content: string): this +} +``` + +其中: + +1. `reportBuilder.page.add(title, callback)` / `remove(id)` / `update(id, callback)` 是主入口,`add` 与 `update` 都返回 reportBuilder 以便链式调用。 +2. 推荐用法固定为 `reportBuilder.page.add('Story One', page => page.setChart(chartBuilder).setText('hello world'))`。 +3. `setChart(chartBuilder)` 会把传入 `chartBuilder.build()` 的结果复制到当前 page 的 `chart` 子树,而不是共享同一个 builder 实例。 +4. `VBIReportBuilder` 不提供 report 级 `buildVQuery()` / `buildVSeed()`;这些能力仍属于 `page.chart`。 + +### 6. 实现上复用同一套 chart builder 内核 + +内部实现采用“同一套 chart builder 绑定不同 DSL map”的策略: + +1. report 的每个 `page.chart` 在 Yjs 中保存为独立 `Y.Map`。 +2. `ReportPageBuilder.chart` 直接复用 `VBIChartBuilder`,但绑定到 `page.chart` 子树,而不是根 `doc.getMap('dsl')`。 +3. `VBI.createChart(...)` 只是“chart builder 绑定根 DSL map”的特例;report page 内则绑定子 map。 +4. `VBIReportBuilderOptions` 只负责把 chart 相关 adapters/options 透传给每个 `page.chart` builder。 + +## Reference + +`packages/vbi/docs/2026-03-23-create-chart/adr.md`, `packages/vbi/src/vbi/create-vbi.ts`, `packages/vbi/src/vbi/generate-empty-dsl.ts`, `packages/vbi/src/builder/builder.ts` +`packages/vbi/src/types/builder/VBIInterface.ts`, `packages/vbi/src/types/dsl/vbi/vbi.ts`, `packages/vbi/docs/todo3-create-report/goal.md` + +## 淘汰内容概述 + +- 不把 page 设计成通用 `blocks: Array` +- 不继续保留泛化的 `types/dsl` 目录名 +- 不在 report 根节点重复保存 chart 的 `connectorId` 等字段 +- 不新增 report 级 `buildVQuery()` / `buildVSeed()` +- 不为 report 另写一套独立 chart 编辑与 lowering 逻辑 +- 不在首期支持“一个 page 多个 chart”或富文本样式系统 diff --git a/packages/vbi/docs/todo3-create-report/goal.md b/packages/vbi/docs/2026-03-24-create-report/goal.md similarity index 56% rename from packages/vbi/docs/todo3-create-report/goal.md rename to packages/vbi/docs/2026-03-24-create-report/goal.md index 5b1bffd3e1..0ad42c48f6 100644 --- a/packages/vbi/docs/todo3-create-report/goal.md +++ b/packages/vbi/docs/2026-03-24-create-report/goal.md @@ -5,6 +5,7 @@ 这里是 packages/vbi 计划要完成的开发任务: [] vbi内新增全新功能, VBI.createReport, 负责创建一个报告容器, 用于组织多个 chart -[] 一个report包含多个page -[] 一个page包含一个chart, 一个文本 +[] 一个report包含多个page, 一个page包含一个chart, 一个文本 +[] 设计一个合适的VBIReportDSL, 包括zod schema, 与当前的 VBIChartDSL 保持一致风格. +[] 一个VBIReportDSL应该包含多个page, 每个page包含一个VBIChartDSL, 一个文本. [] 需要设计好 reportBuilder, 使用风格、API风格、实现风格应该和chartBuilder是一致的.(vbi) diff --git a/packages/vbi/docs/2026-03-24-create-report/plan.md b/packages/vbi/docs/2026-03-24-create-report/plan.md new file mode 100644 index 0000000000..6c2d0fbaef --- /dev/null +++ b/packages/vbi/docs/2026-03-24-create-report/plan.md @@ -0,0 +1,110 @@ +# 执行计划: VBI createReport 与 ReportBuilder + +> 基于 ADR: `./adr.md` +> TDD 驱动: 先锁定 schema 和 API,再做目录重组与实现 + +## 范围 + +本计划只覆盖 `packages/vbi`,包含 `createReport` 根入口、`types/dsl -> types/chartDSL` 重命名、`types/reportDSL` 新增、`VBIReportDSL` / `VBIReportBuilder` / `reportBuilder.page.*` 落地,以及相关测试与生成物更新;不包含 report 级 `buildVQuery()` / `buildVSeed()`、多 chart page、rich-text 系统。 + +## Phase 1: 先锁定对外行为 + +### 1.1 Schema 测试 + +**测试文件**: `packages/vbi/tests/types/reportSchemas.test.ts`(新增) + +测试内容: + +1. `zVBIReportDSL` 正确 parse 最小 report DSL。 +2. `page.title` / `page.chart` / `page.text` 缺失时被 reject。 +3. `page.chart` 继续复用 `zVBIChartDSL` 的校验结果。 +4. `generateEmptyReportDSL()` 与 `generateEmptyReportPageDSL(connectorId)` 的默认值稳定。 + +### 1.2 Builder API 测试 + +**测试文件**: `packages/vbi/tests/builder/reportBuilder.test.ts`(新增) + +测试内容: + +1. `VBI.createReport(report)` 与 `createVBI(...).createReport(report)` 可正常创建 builder。 +2. `reportBuilder.page.add('Story One', page => page.setChart(chartBuilder).setText('hello world'))` 输出正确 DSL。 +3. `reportBuilder.page.remove(id)` / `update(id, callback)` 行为正确。 +4. `setChart(chartBuilder)` 复制 `chartBuilder.build()` 结果,而不是共享 builder 实例。 +5. `build()` / `isEmpty()` / `applyUpdate()` / `encodeStateAsUpdate()` 行为稳定。 + +## Phase 2: ChartDSL 目录重命名与 ReportDSL 类型落地 + +**改动范围**: + +- `packages/vbi/src/types/dsl/**` -> `packages/vbi/src/types/chartDSL/**` +- `packages/vbi/src/types/reportDSL/**`(新增) +- `packages/vbi/src/types/index.ts` + +改动内容: + +1. 将现有 chart schema、类型、导出整体迁移到 `types/chartDSL`。 +2. 新增 `report.ts` / `page.ts` / `text.ts`,定义 `VBIReportDSL`、`VBIReportPageDSL`、`VBIReportTextDSL` 与 zod schema。 +3. 更新所有 `src/**`、`tests/**` 内部 import,消除对旧 `types/dsl` 路径的直接依赖。 + +## Phase 3: 空 DSL helper 与 Builder 内核准备 + +**改动文件**: + +- `packages/vbi/src/vbi/generate-empty-report-dsl.ts`(新增) +- `packages/vbi/src/vbi/generate-empty-report-page-dsl.ts`(新增) +- `packages/vbi/src/builder/builder.ts` +- `packages/vbi/src/builder/modules/**` +- `packages/vbi/src/types/builder/**` + +改动内容: + +1. 新增 `generateEmptyReportDSL()` 与 `generateEmptyReportPageDSL(connectorId)`。 +2. 将 `VBIChartBuilder` 的内部实现抽成“可绑定任意 DSL map”的模式,支持 page.chart 复用。 +3. 保持根 `createChart(...)` 行为不变,只把“绑定根 map”变成特例。 + +## Phase 4: ReportBuilder 与 page API + +**改动文件**: + +- `packages/vbi/src/builder/report-builder.ts`(新增) +- `packages/vbi/src/builder/features/report-page/page-collection-builder.ts`(新增) +- `packages/vbi/src/builder/features/report-page/page-builder.ts`(新增) +- `packages/vbi/src/builder/index.ts` +- `packages/vbi/src/types/builder/VBIInterface.ts` + +改动内容: + +1. 新增 `VBIReportBuilder` 与 `VBIReportBuilderInterface`。 +2. 提供 `reportBuilder.page.add/remove/update/get` 主入口。 +3. `page.add(title, callback)` 把第一个参数写入 `page.title`。 +4. `ReportPageBuilder` 提供 `setChart(chartBuilder)` 与 `setText(content)` 链式 API。 + +## Phase 5: 根入口、导出与包内迁移 + +**改动文件**: + +- `packages/vbi/src/vbi/create-vbi.ts` +- `packages/vbi/src/vbi/from/from-vbi-report-dsl-input.ts`(新增) +- `packages/vbi/src/index.ts` +- `packages/vbi/src/vbi.ts` + +改动内容: + +1. `createVBI()` 返回实例新增 `createReport(...)`。 +2. `VBI` 对外导出 `createReport`、`generateEmptyReportDSL`、`generateEmptyReportPageDSL`。 +3. 包内源码和测试默认切到 `types/chartDSL` / `types/reportDSL` 与 report 新 API。 + +## Phase 6: 生成物与验证 + +```bash +pnpm --filter=@visactor/vbi run g +pnpm --filter=@visactor/vbi run test +pnpm run lint +pnpm run typecheck +``` + +验收标准: + +1. `createReport`、`VBIReportDSL`、`reportBuilder.page.*` 通过测试并可稳定构建 DSL。 +2. `types/chartDSL` / `types/reportDSL` 路径清晰,包内无旧 `types/dsl` 残留引用。 +3. 生成物 diff 只反映 report 能力与目录命名收敛,不引入无关行为变化。 diff --git a/packages/vbi/scripts/build-api.mjs b/packages/vbi/scripts/build-api.mjs index feae3ff067..c8f59632a4 100644 --- a/packages/vbi/scripts/build-api.mjs +++ b/packages/vbi/scripts/build-api.mjs @@ -14,102 +14,166 @@ import { Project, SyntaxKind } from 'ts-morph' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -const BUILDER_DIR = path.resolve(__dirname, '../src/builder') +const BUILDER_ROOTS = { + chart: path.resolve(__dirname, '../src/chart-builder'), + report: path.resolve(__dirname, '../src/report-builder'), +} const OUTPUT_DIR = path.resolve(__dirname, '../../../apps/website/docs/zh-CN/vbi/api') // ============================================================================ // 配置 // ============================================================================ -const BUILDER_CONFIG = [ - { name: 'Builder', file: 'builder.ts', category: 'main' }, - { - name: 'chartType', - label: 'chartType', - file: 'features/chart-type/chart-type-builder.ts', - category: 'features', - }, - { name: 'measures', label: 'measures', file: 'features/measures/mea-builder.ts', category: 'features' }, - { - name: 'dimensions', - label: 'dimensions', - file: 'features/dimensions/dim-builder.ts', - category: 'features', - }, - { - name: 'whereFilter', - label: 'whereFilter', - file: 'features/whereFilter/where-builder.ts', - category: 'features', - }, - { - name: 'havingFilter', - label: 'havingFilter', - file: 'features/havingFilter/having-builder.ts', - category: 'features', - }, - { - name: 'theme', - label: 'theme', - file: 'features/theme/theme-builder.ts', - category: 'features', - }, - { - name: 'locale', - label: 'locale', - file: 'features/locale/locale-builder.ts', - category: 'features', - }, +const API_SECTIONS = [ { - name: 'limit', - label: 'limit', - file: 'features/limit/limit-builder.ts', - category: 'features', + name: 'chartBuilder', + label: 'chartBuilder', + root: 'chart', + index: { + file: 'builder.ts', + displayName: 'VBIChartBuilder', + }, + items: [ + { + type: 'file', + name: 'chartType', + label: 'chartBuilder.chartType', + file: 'features/chart-type/chart-type-builder.ts', + displayName: 'ChartTypeBuilder', + }, + { + type: 'dir', + name: 'measures', + label: 'chartBuilder.measures', + file: 'features/measures/mea-builder.ts', + displayName: 'MeasuresBuilder', + children: [ + { + name: 'measureNode', + label: 'measureNode', + file: 'features/measures/mea-node-builder.ts', + displayName: 'MeasureNodeBuilder', + }, + ], + }, + { + type: 'dir', + name: 'dimensions', + label: 'chartBuilder.dimensions', + file: 'features/dimensions/dim-builder.ts', + displayName: 'DimensionsBuilder', + children: [ + { + name: 'dimensionNode', + label: 'dimensionNode', + file: 'features/dimensions/dim-node-builder.ts', + displayName: 'DimensionNodeBuilder', + }, + ], + }, + { + type: 'dir', + name: 'whereFilter', + label: 'chartBuilder.whereFilter', + file: 'features/whereFilter/where-builder.ts', + displayName: 'WhereFilterBuilder', + children: [ + { + name: 'whereNode', + label: 'whereNode', + file: 'features/whereFilter/where-node-builder.ts', + displayName: 'WhereFilterNodeBuilder', + }, + { + name: 'whereGroup', + label: 'whereGroup', + file: 'features/whereFilter/where-group-builder.ts', + displayName: 'WhereGroupBuilder', + }, + ], + }, + { + type: 'dir', + name: 'havingFilter', + label: 'chartBuilder.havingFilter', + file: 'features/havingFilter/having-builder.ts', + displayName: 'HavingFilterBuilder', + children: [ + { + name: 'havingNode', + label: 'havingNode', + file: 'features/havingFilter/having-node-builder.ts', + displayName: 'HavingFilterNodeBuilder', + }, + { + name: 'havingGroup', + label: 'havingGroup', + file: 'features/havingFilter/having-group-builder.ts', + displayName: 'HavingGroupBuilder', + }, + ], + }, + { + type: 'file', + name: 'theme', + label: 'chartBuilder.theme', + file: 'features/theme/theme-builder.ts', + displayName: 'ThemeBuilder', + }, + { + type: 'file', + name: 'locale', + label: 'chartBuilder.locale', + file: 'features/locale/locale-builder.ts', + displayName: 'LocaleBuilder', + }, + { + type: 'file', + name: 'limit', + label: 'chartBuilder.limit', + file: 'features/limit/limit-builder.ts', + displayName: 'LimitBuilder', + }, + { + type: 'file', + name: 'undoManager', + label: 'chartBuilder.undoManager', + file: 'features/undo-manager/undo-manager.ts', + displayName: 'UndoManager', + }, + ], }, { - name: 'undoManager', - label: 'undoManager', - file: 'features/undo-manager/undo-manager.ts', - category: 'features', - }, -] - -const NODE_BUILDER_CONFIG = [ - { - parent: 'measures', - name: 'MeasureNodeBuilder', - label: 'measure-node', - file: 'features/measures/mea-node-builder.ts', - }, - { - parent: 'dimensions', - name: 'DimensionNodeBuilder', - label: 'dimension-node', - file: 'features/dimensions/dim-node-builder.ts', - }, - { - parent: 'whereFilter', - name: 'WhereNodeBuilder', - label: 'where-node', - file: 'features/whereFilter/where-node-builder.ts', - }, - { - parent: 'whereFilter', - name: 'WhereGroupBuilder', - label: 'where-group', - file: 'features/whereFilter/where-group-builder.ts', - }, - { - parent: 'havingFilter', - name: 'HavingFilterNodeBuilder', - label: 'having-node', - file: 'features/havingFilter/having-node-builder.ts', - }, - { - parent: 'havingFilter', - name: 'HavingGroupBuilder', - label: 'having-group', - file: 'features/havingFilter/having-group-builder.ts', + name: 'reportBuilder', + label: 'reportBuilder', + root: 'report', + index: { + file: 'builder.ts', + displayName: 'VBIReportBuilder', + }, + items: [ + { + type: 'dir', + name: 'page', + label: 'reportBuilder.page', + file: 'features/page/page-collection-builder.ts', + displayName: 'ReportPageCollectionBuilder', + children: [ + { + name: 'reportPage', + label: 'reportPage', + file: 'features/page/page-builder.ts', + displayName: 'ReportPageBuilder', + }, + { + name: 'reportText', + label: 'reportText', + file: 'features/page/text-builder.ts', + displayName: 'ReportTextBuilder', + }, + ], + }, + ], }, ] @@ -117,12 +181,15 @@ const NODE_BUILDER_CONFIG = [ // 工具函数 // ============================================================================ -const kebabCase = (str) => str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() - const ensureDir = (dirPath) => { if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true }) } +const resetDir = (dirPath) => { + fs.rmSync(dirPath, { recursive: true, force: true }) + fs.mkdirSync(dirPath, { recursive: true }) +} + const writeFile = (filePath, content) => fs.writeFileSync(filePath, content, 'utf-8') const writeJson = (filePath, data) => writeFile(filePath, JSON.stringify(data, null, 2)) @@ -130,6 +197,14 @@ const writeJson = (filePath, data) => writeFile(filePath, JSON.stringify(data, n /** 转义 markdown table 中的 | 字符 */ const escapeTableCell = (str) => str.replace(/\|/g, '\\|') +const resolveBuilderPath = (config) => { + const root = BUILDER_ROOTS[config.root] + if (!root) { + throw new Error(`Unknown builder root "${config.root}"`) + } + return path.join(root, config.file) +} + // ============================================================================ // 类型解析 // ============================================================================ @@ -250,6 +325,7 @@ const parseClass = (filePath) => { // 普通方法 for (const method of classDecl.getMethods()) { + if (method.hasModifier(SyntaxKind.PrivateKeyword)) continue methods.push({ name: method.getName(), params: parseParams(method), @@ -330,9 +406,9 @@ const renderMethodDoc = (method) => { } const renderBuilderDoc = (config) => { - const filePath = path.join(BUILDER_DIR, config.file) + const filePath = resolveBuilderPath(config) const { description, properties, methods } = parseClass(filePath) - const displayName = config.category === 'main' ? 'VBIChartBuilder' : config.label || config.name + const displayName = config.displayName || config.label || config.name const parts = [ `# ${displayName}`, @@ -346,13 +422,55 @@ const renderBuilderDoc = (config) => { return parts.filter(Boolean).join('\n\n') } -const renderNodeBuilderDoc = (config) => { - const filePath = path.join(BUILDER_DIR, config.file) - const { description, methods } = parseClass(filePath) +const writeDoc = (filePath, content) => writeFile(filePath, content) - const parts = [`# ${config.name}`, description || '', '## 方法', methods.map(renderMethodDoc).join('\n\n')] +const toDocConfig = (root, config) => ({ ...config, root }) - return parts.filter(Boolean).join('\n\n') +const createMetaFileEntry = (name, label) => ({ type: 'file', name, label }) + +const createMetaDirEntry = (name, label, collapsed = true) => ({ + type: 'dir', + name, + label, + collapsible: true, + collapsed, +}) + +const generateSection = (section) => { + const sectionDir = path.join(OUTPUT_DIR, section.name) + ensureDir(sectionDir) + + writeDoc(path.join(sectionDir, 'index.md'), renderBuilderDoc(toDocConfig(section.root, section.index))) + console.log(`Generated: ${section.name}/index.md`) + + const sectionMeta = [] + for (const item of section.items) { + if (item.type === 'file') { + writeDoc(path.join(sectionDir, `${item.name}.md`), renderBuilderDoc(toDocConfig(section.root, item))) + console.log(`Generated: ${section.name}/${item.name}.md`) + sectionMeta.push(createMetaFileEntry(item.name, item.label)) + continue + } + + const itemDir = path.join(sectionDir, item.name) + ensureDir(itemDir) + writeDoc(path.join(itemDir, 'index.md'), renderBuilderDoc(toDocConfig(section.root, item))) + console.log(`Generated: ${section.name}/${item.name}/index.md`) + + const childMeta = [] + for (const child of item.children || []) { + writeDoc(path.join(itemDir, `${child.name}.md`), renderBuilderDoc(toDocConfig(section.root, child))) + console.log(`Generated: ${section.name}/${item.name}/${child.name}.md`) + childMeta.push(createMetaFileEntry(child.name, child.label)) + } + + writeJson(path.join(itemDir, '_meta.json'), childMeta) + console.log(`Generated: ${section.name}/${item.name}/_meta.json`) + sectionMeta.push(createMetaDirEntry(item.name, item.label)) + } + + writeJson(path.join(sectionDir, '_meta.json'), sectionMeta) + console.log(`Generated: ${section.name}/_meta.json`) } // ============================================================================ @@ -362,63 +480,19 @@ const renderNodeBuilderDoc = (config) => { function generateDocs() { console.log('Building API docs from builder classes...\n') - ensureDir(OUTPUT_DIR) - - // 收集哪些 parent 拥有子文档 - const parentsWithChildren = new Set(NODE_BUILDER_CONFIG.map((n) => kebabCase(n.parent))) - - // 1. 生成 Builder 文档(主 builder + 子 builder 均输出到 OUTPUT_DIR) - const apiMeta = [] - for (const builder of BUILDER_CONFIG) { - const fileName = kebabCase(builder.name) - const md = renderBuilderDoc(builder) - - if (builder.category === 'main') { - writeFile(path.join(OUTPUT_DIR, 'builder.md'), md) - console.log(`Generated: builder.md`) - apiMeta.push({ type: 'file', name: 'builder', label: 'builder' }) - } else { - writeFile(path.join(OUTPUT_DIR, `${fileName}.md`), md) - console.log(`Generated: ${fileName}.md`) - - apiMeta.push({ - type: parentsWithChildren.has(fileName) ? 'dir' : 'file', - name: fileName, - label: `builder.${builder.label || builder.name}`, - collapsed: true, - }) - } - } - - // 2. 生成 NodeBuilder 文档 + _meta.json(按 parent 分组,输出到 OUTPUT_DIR/) - const nodeMetaByParent = {} - for (const nodeBuilder of NODE_BUILDER_CONFIG) { - const parentName = kebabCase(nodeBuilder.parent) - ensureDir(path.join(OUTPUT_DIR, parentName)) - - const md = renderNodeBuilderDoc(nodeBuilder) - writeFile(path.join(OUTPUT_DIR, parentName, `${nodeBuilder.label}.md`), md) - console.log(`Generated: ${parentName}/${nodeBuilder.label}.md`) - - if (!nodeMetaByParent[parentName]) nodeMetaByParent[parentName] = [] - nodeMetaByParent[parentName].push({ - type: 'file', - name: nodeBuilder.label, - label: nodeBuilder.label, - collapsed: true, - }) - } + resetDir(OUTPUT_DIR) - // 3. 生成 _meta.json 文件 - for (const [parentName, items] of Object.entries(nodeMetaByParent)) { - writeJson(path.join(OUTPUT_DIR, parentName, '_meta.json'), items) - console.log(`Generated: api/${parentName}/_meta.json`) + for (const section of API_SECTIONS) { + generateSection(section) } - writeJson(path.join(OUTPUT_DIR, '_meta.json'), apiMeta) + writeJson(path.join(OUTPUT_DIR, '_meta.json'), [ + createMetaFileEntry('index', 'API'), + ...API_SECTIONS.map((section) => createMetaDirEntry(section.name, section.label, false)), + ]) console.log('Generated: api/_meta.json') - writeFile(path.join(OUTPUT_DIR, 'index.md'), '---\noverview: true\n---\n') + writeFile(path.join(OUTPUT_DIR, 'index.md'), '---\noverview: true\ntitle: API\n---\n') console.log('Generated: index.md') // 4. 确保父级 _meta.json 包含 api 条目 @@ -430,9 +504,7 @@ function generateDocs() { console.log('Updated: _meta.json') } - console.log( - `\n✅ Generated API docs for ${BUILDER_CONFIG.length} builders and ${NODE_BUILDER_CONFIG.length} node-builders`, - ) + console.log(`\n✅ Generated API docs for ${API_SECTIONS.length} builder sections`) } generateDocs() diff --git a/packages/vbi/src/builder/adapters/index.ts b/packages/vbi/src/chart-builder/adapters/index.ts similarity index 100% rename from packages/vbi/src/builder/adapters/index.ts rename to packages/vbi/src/chart-builder/adapters/index.ts diff --git a/packages/vbi/src/builder/adapters/vquery-vseed/build-vquery.ts b/packages/vbi/src/chart-builder/adapters/vquery-vseed/build-vquery.ts similarity index 75% rename from packages/vbi/src/builder/adapters/vquery-vseed/build-vquery.ts rename to packages/vbi/src/chart-builder/adapters/vquery-vseed/build-vquery.ts index c16177e0e4..656b21b894 100644 --- a/packages/vbi/src/builder/adapters/vquery-vseed/build-vquery.ts +++ b/packages/vbi/src/chart-builder/adapters/vquery-vseed/build-vquery.ts @@ -1,4 +1,4 @@ -import { buildVQuery as buildVQueryPipeline } from 'src/pipeline' +import { buildVQuery as buildVQueryPipeline } from 'src/chart-builder/pipeline' import type { VBIChartQueryBuilder } from 'src/types' import type { DefaultVBIQueryDSL } from './types' diff --git a/packages/vbi/src/builder/adapters/vquery-vseed/build-vseed.ts b/packages/vbi/src/chart-builder/adapters/vquery-vseed/build-vseed.ts similarity index 93% rename from packages/vbi/src/builder/adapters/vquery-vseed/build-vseed.ts rename to packages/vbi/src/chart-builder/adapters/vquery-vseed/build-vseed.ts index c31ebca524..d767185a27 100644 --- a/packages/vbi/src/builder/adapters/vquery-vseed/build-vseed.ts +++ b/packages/vbi/src/chart-builder/adapters/vquery-vseed/build-vseed.ts @@ -1,7 +1,7 @@ import type { Dimension, Measure } from '@visactor/vseed' -import { DimensionsBuilder, MeasuresBuilder } from 'src/builder/features' +import { DimensionsBuilder, MeasuresBuilder } from 'src/chart-builder/features' import type { VBIChartSeedBuilder } from 'src/types' -import { getConnector } from 'src/builder/connector' +import { getConnector } from 'src/chart-builder/connector' import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from './types' export const buildVSeedDSL: VBIChartSeedBuilder = async ({ diff --git a/packages/vbi/src/builder/adapters/vquery-vseed/index.ts b/packages/vbi/src/chart-builder/adapters/vquery-vseed/index.ts similarity index 100% rename from packages/vbi/src/builder/adapters/vquery-vseed/index.ts rename to packages/vbi/src/chart-builder/adapters/vquery-vseed/index.ts diff --git a/packages/vbi/src/builder/adapters/vquery-vseed/types.ts b/packages/vbi/src/chart-builder/adapters/vquery-vseed/types.ts similarity index 100% rename from packages/vbi/src/builder/adapters/vquery-vseed/types.ts rename to packages/vbi/src/chart-builder/adapters/vquery-vseed/types.ts diff --git a/packages/vbi/src/builder/builder.ts b/packages/vbi/src/chart-builder/builder.ts similarity index 92% rename from packages/vbi/src/builder/builder.ts rename to packages/vbi/src/chart-builder/builder.ts index 484cf16379..6b88a6020c 100644 --- a/packages/vbi/src/builder/builder.ts +++ b/packages/vbi/src/chart-builder/builder.ts @@ -1,7 +1,7 @@ import * as Y from 'yjs' -import { resolveVBIChartBuilderAdapters } from 'src/builder/adapters/vquery-vseed' -import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/builder/adapters/vquery-vseed' +import { resolveVBIChartBuilderAdapters } from 'src/chart-builder/adapters/vquery-vseed' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed' import { DimensionsBuilder, MeasuresBuilder, @@ -46,9 +46,9 @@ export class VBIChartBuilder) { + constructor(doc: Y.Doc, options?: VBIChartBuilderOptions, dsl?: Y.Map) { this.doc = doc - this.dsl = doc.getMap('dsl') as Y.Map + this.dsl = (dsl ?? doc.getMap('dsl')) as Y.Map this.adapters = resolveVBIChartBuilderAdapters(options?.adapters) this.undoManager = new UndoManager(this.dsl) diff --git a/packages/vbi/src/builder/connector.ts b/packages/vbi/src/chart-builder/connector.ts similarity index 100% rename from packages/vbi/src/builder/connector.ts rename to packages/vbi/src/chart-builder/connector.ts diff --git a/packages/vbi/src/builder/features/chart-type/chart-type-builder.ts b/packages/vbi/src/chart-builder/features/chart-type/chart-type-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/chart-type/chart-type-builder.ts rename to packages/vbi/src/chart-builder/features/chart-type/chart-type-builder.ts diff --git a/packages/vbi/src/builder/features/chart-type/dimension-encoding.ts b/packages/vbi/src/chart-builder/features/chart-type/dimension-encoding.ts similarity index 100% rename from packages/vbi/src/builder/features/chart-type/dimension-encoding.ts rename to packages/vbi/src/chart-builder/features/chart-type/dimension-encoding.ts diff --git a/packages/vbi/src/builder/features/chart-type/index.ts b/packages/vbi/src/chart-builder/features/chart-type/index.ts similarity index 100% rename from packages/vbi/src/builder/features/chart-type/index.ts rename to packages/vbi/src/chart-builder/features/chart-type/index.ts diff --git a/packages/vbi/src/builder/features/chart-type/measure-encoding.ts b/packages/vbi/src/chart-builder/features/chart-type/measure-encoding.ts similarity index 100% rename from packages/vbi/src/builder/features/chart-type/measure-encoding.ts rename to packages/vbi/src/chart-builder/features/chart-type/measure-encoding.ts diff --git a/packages/vbi/src/builder/features/chart-type/reapply-dimension-encodings.ts b/packages/vbi/src/chart-builder/features/chart-type/reapply-dimension-encodings.ts similarity index 100% rename from packages/vbi/src/builder/features/chart-type/reapply-dimension-encodings.ts rename to packages/vbi/src/chart-builder/features/chart-type/reapply-dimension-encodings.ts diff --git a/packages/vbi/src/builder/features/chart-type/reapply-measure-encodings.ts b/packages/vbi/src/chart-builder/features/chart-type/reapply-measure-encodings.ts similarity index 100% rename from packages/vbi/src/builder/features/chart-type/reapply-measure-encodings.ts rename to packages/vbi/src/chart-builder/features/chart-type/reapply-measure-encodings.ts diff --git a/packages/vbi/src/builder/features/dimensions/dim-builder.ts b/packages/vbi/src/chart-builder/features/dimensions/dim-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/dimensions/dim-builder.ts rename to packages/vbi/src/chart-builder/features/dimensions/dim-builder.ts diff --git a/packages/vbi/src/builder/features/dimensions/dim-node-builder.ts b/packages/vbi/src/chart-builder/features/dimensions/dim-node-builder.ts similarity index 96% rename from packages/vbi/src/builder/features/dimensions/dim-node-builder.ts rename to packages/vbi/src/chart-builder/features/dimensions/dim-node-builder.ts index dc5a1dd4a2..49d620bb47 100644 --- a/packages/vbi/src/builder/features/dimensions/dim-node-builder.ts +++ b/packages/vbi/src/chart-builder/features/dimensions/dim-node-builder.ts @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import type { VBIDimension, VBISort } from '../../../types' +import type { VBIDimension, VBISort } from 'src/types' /** * @description 维度节点构建器,用于配置单个维度 diff --git a/packages/vbi/src/builder/features/dimensions/dimension-utils.ts b/packages/vbi/src/chart-builder/features/dimensions/dimension-utils.ts similarity index 100% rename from packages/vbi/src/builder/features/dimensions/dimension-utils.ts rename to packages/vbi/src/chart-builder/features/dimensions/dimension-utils.ts diff --git a/packages/vbi/src/builder/features/dimensions/index.ts b/packages/vbi/src/chart-builder/features/dimensions/index.ts similarity index 100% rename from packages/vbi/src/builder/features/dimensions/index.ts rename to packages/vbi/src/chart-builder/features/dimensions/index.ts diff --git a/packages/vbi/src/builder/features/havingFilter/having-builder.ts b/packages/vbi/src/chart-builder/features/havingFilter/having-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/havingFilter/having-builder.ts rename to packages/vbi/src/chart-builder/features/havingFilter/having-builder.ts diff --git a/packages/vbi/src/builder/features/havingFilter/having-group-builder.ts b/packages/vbi/src/chart-builder/features/havingFilter/having-group-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/havingFilter/having-group-builder.ts rename to packages/vbi/src/chart-builder/features/havingFilter/having-group-builder.ts diff --git a/packages/vbi/src/builder/features/havingFilter/having-node-builder.ts b/packages/vbi/src/chart-builder/features/havingFilter/having-node-builder.ts similarity index 95% rename from packages/vbi/src/builder/features/havingFilter/having-node-builder.ts rename to packages/vbi/src/chart-builder/features/havingFilter/having-node-builder.ts index 728f44acde..02c7fc6032 100644 --- a/packages/vbi/src/builder/features/havingFilter/having-node-builder.ts +++ b/packages/vbi/src/chart-builder/features/havingFilter/having-node-builder.ts @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import { VBIHavingFilter, VBIHavingAggregate } from '../../../types' +import type { VBIHavingFilter, VBIHavingAggregate } from 'src/types' /** * @description Having 过滤节点构建器,用于配置单个 Having 过滤条件 diff --git a/packages/vbi/src/builder/features/havingFilter/having-utils.ts b/packages/vbi/src/chart-builder/features/havingFilter/having-utils.ts similarity index 100% rename from packages/vbi/src/builder/features/havingFilter/having-utils.ts rename to packages/vbi/src/chart-builder/features/havingFilter/having-utils.ts diff --git a/packages/vbi/src/builder/features/havingFilter/index.ts b/packages/vbi/src/chart-builder/features/havingFilter/index.ts similarity index 100% rename from packages/vbi/src/builder/features/havingFilter/index.ts rename to packages/vbi/src/chart-builder/features/havingFilter/index.ts diff --git a/packages/vbi/src/builder/features/index.ts b/packages/vbi/src/chart-builder/features/index.ts similarity index 100% rename from packages/vbi/src/builder/features/index.ts rename to packages/vbi/src/chart-builder/features/index.ts diff --git a/packages/vbi/src/builder/features/limit/index.ts b/packages/vbi/src/chart-builder/features/limit/index.ts similarity index 100% rename from packages/vbi/src/builder/features/limit/index.ts rename to packages/vbi/src/chart-builder/features/limit/index.ts diff --git a/packages/vbi/src/builder/features/limit/limit-builder.ts b/packages/vbi/src/chart-builder/features/limit/limit-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/limit/limit-builder.ts rename to packages/vbi/src/chart-builder/features/limit/limit-builder.ts diff --git a/packages/vbi/src/builder/features/locale/index.ts b/packages/vbi/src/chart-builder/features/locale/index.ts similarity index 100% rename from packages/vbi/src/builder/features/locale/index.ts rename to packages/vbi/src/chart-builder/features/locale/index.ts diff --git a/packages/vbi/src/builder/features/locale/locale-builder.ts b/packages/vbi/src/chart-builder/features/locale/locale-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/locale/locale-builder.ts rename to packages/vbi/src/chart-builder/features/locale/locale-builder.ts diff --git a/packages/vbi/src/builder/features/measures/index.ts b/packages/vbi/src/chart-builder/features/measures/index.ts similarity index 100% rename from packages/vbi/src/builder/features/measures/index.ts rename to packages/vbi/src/chart-builder/features/measures/index.ts diff --git a/packages/vbi/src/builder/features/measures/mea-builder.ts b/packages/vbi/src/chart-builder/features/measures/mea-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/measures/mea-builder.ts rename to packages/vbi/src/chart-builder/features/measures/mea-builder.ts diff --git a/packages/vbi/src/builder/features/measures/mea-node-builder.ts b/packages/vbi/src/chart-builder/features/measures/mea-node-builder.ts similarity index 96% rename from packages/vbi/src/builder/features/measures/mea-node-builder.ts rename to packages/vbi/src/chart-builder/features/measures/mea-node-builder.ts index 9a3b3f58a2..5e4a248eca 100644 --- a/packages/vbi/src/builder/features/measures/mea-node-builder.ts +++ b/packages/vbi/src/chart-builder/features/measures/mea-node-builder.ts @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import type { VBIMeasure, VBIMeasureFormat, VBISort } from '../../../types' +import type { VBIMeasure, VBIMeasureFormat, VBISort } from 'src/types' /** * @description 度量节点构建器,用于配置单个度量 diff --git a/packages/vbi/src/builder/features/measures/measure-utils.ts b/packages/vbi/src/chart-builder/features/measures/measure-utils.ts similarity index 100% rename from packages/vbi/src/builder/features/measures/measure-utils.ts rename to packages/vbi/src/chart-builder/features/measures/measure-utils.ts diff --git a/packages/vbi/src/builder/features/theme/index.ts b/packages/vbi/src/chart-builder/features/theme/index.ts similarity index 100% rename from packages/vbi/src/builder/features/theme/index.ts rename to packages/vbi/src/chart-builder/features/theme/index.ts diff --git a/packages/vbi/src/builder/features/theme/theme-builder.ts b/packages/vbi/src/chart-builder/features/theme/theme-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/theme/theme-builder.ts rename to packages/vbi/src/chart-builder/features/theme/theme-builder.ts diff --git a/packages/vbi/src/builder/features/undo-manager/index.ts b/packages/vbi/src/chart-builder/features/undo-manager/index.ts similarity index 100% rename from packages/vbi/src/builder/features/undo-manager/index.ts rename to packages/vbi/src/chart-builder/features/undo-manager/index.ts diff --git a/packages/vbi/src/builder/features/undo-manager/undo-manager.ts b/packages/vbi/src/chart-builder/features/undo-manager/undo-manager.ts similarity index 100% rename from packages/vbi/src/builder/features/undo-manager/undo-manager.ts rename to packages/vbi/src/chart-builder/features/undo-manager/undo-manager.ts diff --git a/packages/vbi/src/builder/features/whereFilter/index.ts b/packages/vbi/src/chart-builder/features/whereFilter/index.ts similarity index 100% rename from packages/vbi/src/builder/features/whereFilter/index.ts rename to packages/vbi/src/chart-builder/features/whereFilter/index.ts diff --git a/packages/vbi/src/builder/features/whereFilter/where-builder.ts b/packages/vbi/src/chart-builder/features/whereFilter/where-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/whereFilter/where-builder.ts rename to packages/vbi/src/chart-builder/features/whereFilter/where-builder.ts diff --git a/packages/vbi/src/builder/features/whereFilter/where-group-builder.ts b/packages/vbi/src/chart-builder/features/whereFilter/where-group-builder.ts similarity index 100% rename from packages/vbi/src/builder/features/whereFilter/where-group-builder.ts rename to packages/vbi/src/chart-builder/features/whereFilter/where-group-builder.ts diff --git a/packages/vbi/src/builder/features/whereFilter/where-node-builder.ts b/packages/vbi/src/chart-builder/features/whereFilter/where-node-builder.ts similarity index 95% rename from packages/vbi/src/builder/features/whereFilter/where-node-builder.ts rename to packages/vbi/src/chart-builder/features/whereFilter/where-node-builder.ts index 0c32a3e2f0..0b1056d241 100644 --- a/packages/vbi/src/builder/features/whereFilter/where-node-builder.ts +++ b/packages/vbi/src/chart-builder/features/whereFilter/where-node-builder.ts @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import type { VBIWhereDatePredicate, VBIWhereFilter } from '../../../types' +import type { VBIWhereDatePredicate, VBIWhereFilter } from 'src/types' /** * @description Where 过滤节点构建器,用于配置单个 Where 过滤条件 diff --git a/packages/vbi/src/builder/features/whereFilter/where-utils.ts b/packages/vbi/src/chart-builder/features/whereFilter/where-utils.ts similarity index 100% rename from packages/vbi/src/builder/features/whereFilter/where-utils.ts rename to packages/vbi/src/chart-builder/features/whereFilter/where-utils.ts diff --git a/packages/vbi/src/builder/index.ts b/packages/vbi/src/chart-builder/index.ts similarity index 81% rename from packages/vbi/src/builder/index.ts rename to packages/vbi/src/chart-builder/index.ts index b881dbd3af..f0901b2ba4 100644 --- a/packages/vbi/src/builder/index.ts +++ b/packages/vbi/src/chart-builder/index.ts @@ -1,12 +1,11 @@ export { VBIChartBuilder } from './builder' -export { VBI } from '../vbi' -export * from './adapters' export { MeasuresBuilder, DimensionsBuilder, ChartTypeBuilder, - HavingFilterBuilder, WhereFilterBuilder, + WhereGroupBuilder, + HavingFilterBuilder, ThemeBuilder, LocaleBuilder, LimitBuilder, diff --git a/packages/vbi/src/builder/modules/apply-update.ts b/packages/vbi/src/chart-builder/modules/apply-update.ts similarity index 100% rename from packages/vbi/src/builder/modules/apply-update.ts rename to packages/vbi/src/chart-builder/modules/apply-update.ts diff --git a/packages/vbi/src/builder/modules/build.ts b/packages/vbi/src/chart-builder/modules/build.ts similarity index 100% rename from packages/vbi/src/builder/modules/build.ts rename to packages/vbi/src/chart-builder/modules/build.ts diff --git a/packages/vbi/src/builder/modules/encode-state-as-update.ts b/packages/vbi/src/chart-builder/modules/encode-state-as-update.ts similarity index 100% rename from packages/vbi/src/builder/modules/encode-state-as-update.ts rename to packages/vbi/src/chart-builder/modules/encode-state-as-update.ts diff --git a/packages/vbi/src/builder/modules/get-schema.ts b/packages/vbi/src/chart-builder/modules/get-schema.ts similarity index 100% rename from packages/vbi/src/builder/modules/get-schema.ts rename to packages/vbi/src/chart-builder/modules/get-schema.ts diff --git a/packages/vbi/src/builder/modules/index.ts b/packages/vbi/src/chart-builder/modules/index.ts similarity index 100% rename from packages/vbi/src/builder/modules/index.ts rename to packages/vbi/src/chart-builder/modules/index.ts diff --git a/packages/vbi/src/builder/modules/is-empty.ts b/packages/vbi/src/chart-builder/modules/is-empty.ts similarity index 100% rename from packages/vbi/src/builder/modules/is-empty.ts rename to packages/vbi/src/chart-builder/modules/is-empty.ts diff --git a/packages/vbi/src/pipeline/index.ts b/packages/vbi/src/chart-builder/pipeline/index.ts similarity index 100% rename from packages/vbi/src/pipeline/index.ts rename to packages/vbi/src/chart-builder/pipeline/index.ts diff --git a/packages/vbi/src/pipeline/vqueryDSL/aggregateMap.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/aggregateMap.ts similarity index 100% rename from packages/vbi/src/pipeline/vqueryDSL/aggregateMap.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/aggregateMap.ts diff --git a/packages/vbi/src/pipeline/vqueryDSL/buildGroupBy.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildGroupBy.ts similarity index 90% rename from packages/vbi/src/pipeline/vqueryDSL/buildGroupBy.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildGroupBy.ts index 43599f9398..fcf9c3e60d 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/buildGroupBy.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildGroupBy.ts @@ -1,6 +1,6 @@ import type { VQueryDSL } from '@visactor/vquery' import type { buildPipe } from './types' -import { DimensionsBuilder } from '../../builder' +import { DimensionsBuilder } from '../../features' export const buildGroupBy: buildPipe = (queryDSL, context) => { const result = { ...queryDSL } diff --git a/packages/vbi/src/pipeline/vqueryDSL/buildHaving.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildHaving.ts similarity index 98% rename from packages/vbi/src/pipeline/vqueryDSL/buildHaving.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildHaving.ts index e9132d8ce9..e701a028d9 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/buildHaving.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildHaving.ts @@ -1,6 +1,6 @@ import type { VQueryDSL } from '@visactor/vquery' import type { buildPipe } from './types' -import type { VBIHavingClause, VBIHavingFilter, VBIHavingGroup } from '../../types' +import type { VBIHavingClause, VBIHavingFilter, VBIHavingGroup } from 'src/types' import { mapAggregateForVQuery } from './aggregateMap' export const buildHaving: buildPipe = (queryDSL, context) => { diff --git a/packages/vbi/src/pipeline/vqueryDSL/buildLimit.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildLimit.ts similarity index 100% rename from packages/vbi/src/pipeline/vqueryDSL/buildLimit.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildLimit.ts diff --git a/packages/vbi/src/pipeline/vqueryDSL/buildOrderBy.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildOrderBy.ts similarity index 93% rename from packages/vbi/src/pipeline/vqueryDSL/buildOrderBy.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildOrderBy.ts index f74f1a96a4..4f780c20c0 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/buildOrderBy.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildOrderBy.ts @@ -1,5 +1,5 @@ import type { VQueryDSL } from '@visactor/vquery' -import { DimensionsBuilder, MeasuresBuilder } from '../../builder' +import { DimensionsBuilder, MeasuresBuilder } from '../../features' import type { buildPipe } from './types' const toOrderItem = (node: { id: string; sort?: { order: 'asc' | 'desc' } }) => ({ diff --git a/packages/vbi/src/pipeline/vqueryDSL/buildSelect.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildSelect.ts similarity index 94% rename from packages/vbi/src/pipeline/vqueryDSL/buildSelect.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildSelect.ts index 2d36d7bb5b..c00aa1e633 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/buildSelect.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildSelect.ts @@ -1,6 +1,6 @@ import type { VQueryDSL } from '@visactor/vquery' import type { buildPipe } from './types' -import { MeasuresBuilder, DimensionsBuilder } from '../../builder' +import { MeasuresBuilder, DimensionsBuilder } from '../../features' import { mapAggregateForVQuery, mapDimensionAggregateForVQuery } from './aggregateMap' export const buildSelect: buildPipe = (queryDSL, context) => { diff --git a/packages/vbi/src/pipeline/vqueryDSL/buildWhere.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildWhere.ts similarity index 98% rename from packages/vbi/src/pipeline/vqueryDSL/buildWhere.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildWhere.ts index ab4a3d4975..14852f9f17 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/buildWhere.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/buildWhere.ts @@ -1,6 +1,6 @@ import type { VQueryDSL } from '@visactor/vquery' import type { buildPipe } from './types' -import type { VBIWhereDatePredicate, VBIWhereFilter, VBIWhereClause, VBIWhereGroup } from '../../types' +import type { VBIWhereDatePredicate, VBIWhereFilter, VBIWhereClause, VBIWhereGroup } from 'src/types' import { resolveDatePredicate } from './resolveDatePredicate' export const buildWhere: buildPipe = (queryDSL, context) => { diff --git a/packages/vbi/src/pipeline/vqueryDSL/index.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/index.ts similarity index 91% rename from packages/vbi/src/pipeline/vqueryDSL/index.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/index.ts index ea94e46105..07a12d88ff 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/index.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/index.ts @@ -1,6 +1,6 @@ import { pipe } from 'remeda' import type { VQueryDSL } from '@visactor/vquery' -import type { VBIChartBuilderInterface, VBIChartDSL } from '../../types' +import type { VBIChartBuilderInterface, VBIChartDSL } from 'src/types' import type { buildPipe } from './types' import { buildSelect } from './buildSelect' import { buildGroupBy } from './buildGroupBy' diff --git a/packages/vbi/src/pipeline/vqueryDSL/resolveDatePredicate.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/resolveDatePredicate.ts similarity index 99% rename from packages/vbi/src/pipeline/vqueryDSL/resolveDatePredicate.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/resolveDatePredicate.ts index d0e79c4efd..0a038b775e 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/resolveDatePredicate.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/resolveDatePredicate.ts @@ -1,4 +1,4 @@ -import type { VBIWhereDateBounds, VBIWhereDatePredicate } from '../../types' +import type { VBIWhereDateBounds, VBIWhereDatePredicate } from 'src/types' export type DateRange = { start: string diff --git a/packages/vbi/src/pipeline/vqueryDSL/types.ts b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/types.ts similarity index 76% rename from packages/vbi/src/pipeline/vqueryDSL/types.ts rename to packages/vbi/src/chart-builder/pipeline/vqueryDSL/types.ts index f9c0ac86db..4af9465693 100644 --- a/packages/vbi/src/pipeline/vqueryDSL/types.ts +++ b/packages/vbi/src/chart-builder/pipeline/vqueryDSL/types.ts @@ -1,5 +1,5 @@ import type { VQueryDSL } from '@visactor/vquery' -import type { VBIChartBuilderInterface, VBIChartDSL } from '../../types' +import type { VBIChartBuilderInterface, VBIChartDSL } from 'src/types' export type buildPipeContext = { vbiDSL: VBIChartDSL diff --git a/packages/vbi/src/index.ts b/packages/vbi/src/index.ts index 78585899ee..b8303aa271 100644 --- a/packages/vbi/src/index.ts +++ b/packages/vbi/src/index.ts @@ -1,9 +1,8 @@ export { VBI } from './vbi' export { createVBI } from './vbi/create-vbi' export type { VBIInstance } from './vbi/create-vbi' -export { VBIChartBuilder } from './builder' -export { defaultVBIChartBuilderAdapters, resolveVBIChartBuilderAdapters } from './builder/adapters' export { + VBIChartBuilder, MeasuresBuilder, DimensionsBuilder, ChartTypeBuilder, @@ -13,7 +12,9 @@ export { LocaleBuilder, LimitBuilder, UndoManager, -} from './builder' +} from './chart-builder' +export { VBIReportBuilder, ReportPageBuilder, ReportPageCollectionBuilder, ReportTextBuilder } from './report-builder' +export { defaultVBIChartBuilderAdapters, resolveVBIChartBuilderAdapters } from './chart-builder/adapters' export * from './types' export { id, @@ -24,4 +25,4 @@ export { preorderTraverse, findTreeNodesBy, } from './utils' -export { buildVQuery } from './pipeline' +export { buildVQuery } from './chart-builder/pipeline' diff --git a/packages/vbi/src/report-builder/builder.ts b/packages/vbi/src/report-builder/builder.ts new file mode 100644 index 0000000000..fb6b409059 --- /dev/null +++ b/packages/vbi/src/report-builder/builder.ts @@ -0,0 +1,42 @@ +import * as Y from 'yjs' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' +import type { VBIReportDSL, VBIReportBuilderInterface, VBIReportBuilderOptions } from 'src/types' +import { UndoManager, ReportPageCollectionBuilder } from './features' +import { applyUpdateToDoc, encodeDocStateAsUpdate, buildVBIReportDSL, isEmptyVBIReportDSL } from './modules' +import { getOrCreateReportPages } from 'src/vbi/from/report-page-y-map' + +export class VBIReportBuilder + implements VBIReportBuilderInterface +{ + public doc: Y.Doc + public dsl: Y.Map + public undoManager: UndoManager + public page: ReportPageCollectionBuilder + + constructor(doc: Y.Doc, options?: VBIReportBuilderOptions) { + this.doc = doc + this.dsl = doc.getMap('dsl') as Y.Map + + doc.transact(() => { + getOrCreateReportPages(this.dsl) + if (this.dsl.get('version') === undefined) { + this.dsl.set('version', 0) + } + }) + + this.undoManager = new UndoManager(this.dsl) + this.page = new ReportPageCollectionBuilder(this, doc, this.dsl, options) + } + + public applyUpdate = (update: Uint8Array, transactionOrigin?: any) => { + return applyUpdateToDoc(this.doc, update, transactionOrigin) + } + + public encodeStateAsUpdate = (targetStateVector?: Uint8Array) => { + return encodeDocStateAsUpdate(this.doc, targetStateVector) + } + + public build = (): VBIReportDSL => buildVBIReportDSL(this.dsl) + + public isEmpty = (): boolean => isEmptyVBIReportDSL(this.dsl) +} diff --git a/packages/vbi/src/report-builder/features/index.ts b/packages/vbi/src/report-builder/features/index.ts new file mode 100644 index 0000000000..f9849b3def --- /dev/null +++ b/packages/vbi/src/report-builder/features/index.ts @@ -0,0 +1,2 @@ +export { ReportPageBuilder, ReportPageCollectionBuilder, ReportTextBuilder } from './page' +export { UndoManager } from 'src/chart-builder/features/undo-manager' diff --git a/packages/vbi/src/report-builder/features/page/index.ts b/packages/vbi/src/report-builder/features/page/index.ts new file mode 100644 index 0000000000..c12698f28b --- /dev/null +++ b/packages/vbi/src/report-builder/features/page/index.ts @@ -0,0 +1,3 @@ +export { ReportPageBuilder } from './page-builder' +export { ReportPageCollectionBuilder } from './page-collection-builder' +export { ReportTextBuilder } from './text-builder' diff --git a/packages/vbi/src/report-builder/features/page/page-builder.ts b/packages/vbi/src/report-builder/features/page/page-builder.ts new file mode 100644 index 0000000000..dfc64617b2 --- /dev/null +++ b/packages/vbi/src/report-builder/features/page/page-builder.ts @@ -0,0 +1,51 @@ +import * as Y from 'yjs' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' +import type { VBIChartBuilderOptions, VBIChartDSLInput, VBIReportPageDSL } from 'src/types' +import { VBIChartBuilder } from 'src/chart-builder/builder' +import { fillVBIChartDSLMap } from 'src/vbi/from/fill-vbi-chart-dsl-map' +import { getOrCreateReportChartMap, getOrCreateReportTextMap } from 'src/vbi/from/report-page-y-map' +import { ReportTextBuilder } from './text-builder' + +const isChartBuilderLike = (value: unknown): value is { build: () => VBIChartDSLInput } => { + return typeof value === 'object' && value !== null && typeof (value as { build?: unknown }).build === 'function' +} + +export class ReportPageBuilder { + public chart: VBIChartBuilder + public text: ReportTextBuilder + + constructor( + private doc: Y.Doc, + private page: Y.Map, + chartOptions?: VBIChartBuilderOptions, + ) { + this.chart = new VBIChartBuilder(doc, chartOptions, getOrCreateReportChartMap(page)) + this.text = new ReportTextBuilder(getOrCreateReportTextMap(page)) + } + + getId(): string { + return this.page.get('id') + } + + setTitle(title: string): this { + this.page.set('title', title) + return this + } + + setChart(chartBuilder: VBIChartBuilder | VBIChartDSLInput): this { + const chartDSL = isChartBuilderLike(chartBuilder) ? chartBuilder.build() : chartBuilder + this.doc.transact(() => { + fillVBIChartDSLMap(this.chart.dsl, chartDSL) + }) + return this + } + + setText(content: string): this { + this.text.setContent(content) + return this + } + + toJSON(): VBIReportPageDSL { + return this.page.toJSON() as VBIReportPageDSL + } +} diff --git a/packages/vbi/src/report-builder/features/page/page-collection-builder.ts b/packages/vbi/src/report-builder/features/page/page-collection-builder.ts new file mode 100644 index 0000000000..fe9a6d61ed --- /dev/null +++ b/packages/vbi/src/report-builder/features/page/page-collection-builder.ts @@ -0,0 +1,75 @@ +import * as Y from 'yjs' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' +import type { VBIReportBuilderOptions } from 'src/types' +import type { VBIReportBuilder } from 'src/report-builder/builder' +import { generateEmptyReportPageDSL } from 'src/vbi/generate-empty-report-page-dsl' +import { createReportPageYMap, getOrCreateReportPages, locateReportPageIndexById } from 'src/vbi/from/report-page-y-map' +import { ReportPageBuilder } from './page-builder' + +export class ReportPageCollectionBuilder { + constructor( + private parent: VBIReportBuilder, + private doc: Y.Doc, + private dsl: Y.Map, + private options?: VBIReportBuilderOptions, + ) { + doc.transact(() => { + getOrCreateReportPages(this.dsl) + }) + } + + add( + title: string, + callback?: (page: ReportPageBuilder) => void, + ): VBIReportBuilder { + const pageMap = createReportPageYMap({ + ...generateEmptyReportPageDSL(), + title, + }) + + this.doc.transact(() => { + getOrCreateReportPages(this.dsl).push([pageMap]) + }) + + if (callback) { + callback(this.createPageBuilder(pageMap)) + } + + return this.parent + } + + remove(pageId: string): VBIReportBuilder { + this.doc.transact(() => { + const pages = getOrCreateReportPages(this.dsl) + const index = locateReportPageIndexById(pages, pageId) + if (index !== -1) { + pages.delete(index, 1) + } + }) + return this.parent + } + + update( + pageId: string, + callback: (page: ReportPageBuilder) => void, + ): VBIReportBuilder { + this.doc.transact(() => { + const page = this.get(pageId) + if (!page) { + throw new Error(`Report page with id "${pageId}" not found`) + } + callback(page) + }) + return this.parent + } + + get(pageId: string): ReportPageBuilder | undefined { + const pages = getOrCreateReportPages(this.dsl) + const index = locateReportPageIndexById(pages, pageId) + return index === -1 ? undefined : this.createPageBuilder(pages.get(index)) + } + + private createPageBuilder(page: Y.Map) { + return new ReportPageBuilder(this.doc, page, this.options?.chart) + } +} diff --git a/packages/vbi/src/report-builder/features/page/text-builder.ts b/packages/vbi/src/report-builder/features/page/text-builder.ts new file mode 100644 index 0000000000..168e8baa2e --- /dev/null +++ b/packages/vbi/src/report-builder/features/page/text-builder.ts @@ -0,0 +1,24 @@ +import * as Y from 'yjs' +import type { VBIReportTextDSL } from 'src/types' + +export class ReportTextBuilder { + constructor(private yMap: Y.Map) {} + + getContent(): string { + return this.yMap.get('content') ?? '' + } + + setContent(content: string): this { + this.yMap.set('content', content) + return this + } + + clear(): this { + this.yMap.set('content', '') + return this + } + + toJSON(): VBIReportTextDSL { + return this.yMap.toJSON() as VBIReportTextDSL + } +} diff --git a/packages/vbi/src/report-builder/index.ts b/packages/vbi/src/report-builder/index.ts new file mode 100644 index 0000000000..b1940ac8bd --- /dev/null +++ b/packages/vbi/src/report-builder/index.ts @@ -0,0 +1,2 @@ +export { VBIReportBuilder } from './builder' +export { ReportPageBuilder, ReportPageCollectionBuilder, ReportTextBuilder, UndoManager } from './features' diff --git a/packages/vbi/src/report-builder/modules/build.ts b/packages/vbi/src/report-builder/modules/build.ts new file mode 100644 index 0000000000..504dc5e39b --- /dev/null +++ b/packages/vbi/src/report-builder/modules/build.ts @@ -0,0 +1,7 @@ +import * as Y from 'yjs' +import type { VBIReportDSL } from 'src/types' +import { zVBIReportDSL } from 'src/types/reportDSL/report' + +export const buildVBIReportDSL = (dsl: Y.Map): VBIReportDSL => { + return zVBIReportDSL.parse(dsl.toJSON()) +} diff --git a/packages/vbi/src/report-builder/modules/index.ts b/packages/vbi/src/report-builder/modules/index.ts new file mode 100644 index 0000000000..02aed3256d --- /dev/null +++ b/packages/vbi/src/report-builder/modules/index.ts @@ -0,0 +1,4 @@ +export { applyUpdateToDoc } from 'src/chart-builder/modules/apply-update' +export { encodeDocStateAsUpdate } from 'src/chart-builder/modules/encode-state-as-update' +export { buildVBIReportDSL } from './build' +export { isEmptyVBIReportDSL } from './is-empty' diff --git a/packages/vbi/src/report-builder/modules/is-empty.ts b/packages/vbi/src/report-builder/modules/is-empty.ts new file mode 100644 index 0000000000..b18f3667bf --- /dev/null +++ b/packages/vbi/src/report-builder/modules/is-empty.ts @@ -0,0 +1,6 @@ +import * as Y from 'yjs' + +export const isEmptyVBIReportDSL = (dsl: Y.Map): boolean => { + const pages = dsl.get('pages') + return !(pages instanceof Y.Array) || pages.length === 0 +} diff --git a/packages/vbi/src/types/builder/VBIInterface.ts b/packages/vbi/src/types/builder/VBIInterface.ts index f0a23e0994..d90d52b596 100644 --- a/packages/vbi/src/types/builder/VBIInterface.ts +++ b/packages/vbi/src/types/builder/VBIInterface.ts @@ -1,5 +1,5 @@ -import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/builder/adapters/vquery-vseed/types' -import type { VBIChartDSL } from '../dsl' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' +import type { VBIChartDSL } from '../chartDSL' import type { BuildVSeedOptions } from './build-vseed' import type { MeasuresBuilder, @@ -11,7 +11,7 @@ import type { LocaleBuilder, LimitBuilder, UndoManager, -} from 'src/builder/features' +} from 'src/chart-builder/features' import type { Map, Doc } from 'yjs' export interface VBIChartBuilderInterface { diff --git a/packages/vbi/src/types/builder/adapter.ts b/packages/vbi/src/types/builder/adapter.ts index e914db0dc5..3a6320e747 100644 --- a/packages/vbi/src/types/builder/adapter.ts +++ b/packages/vbi/src/types/builder/adapter.ts @@ -1,6 +1,6 @@ -import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/builder/adapters/vquery-vseed/types' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' import type { Map } from 'yjs' -import type { VBIChartDSL } from '../dsl' +import type { VBIChartDSL } from '../chartDSL' import type { BuildVSeedOptions } from './build-vseed' import type { VBIChartBuilderInterface } from './VBIInterface' diff --git a/packages/vbi/src/types/builder/context.ts b/packages/vbi/src/types/builder/context.ts index e21605b824..f784b363f9 100644 --- a/packages/vbi/src/types/builder/context.ts +++ b/packages/vbi/src/types/builder/context.ts @@ -1,4 +1,4 @@ -import { VBIChartDSL } from '../dsl' +import { VBIChartDSL } from '../chartDSL' export interface BuilderContext { getVBIChartDSL(): VBIChartDSL diff --git a/packages/vbi/src/types/builder/index.ts b/packages/vbi/src/types/builder/index.ts index e2a45c02db..e8ebc3e193 100644 --- a/packages/vbi/src/types/builder/index.ts +++ b/packages/vbi/src/types/builder/index.ts @@ -8,4 +8,5 @@ export type { VBIChartBuilderAdapters, VBIChartBuilderOptions, } from './adapter' +export type { VBIReportBuilderInterface, VBIReportBuilderOptions } from './report' export type { ObserveCallback, ObserveDeepCallback } from './observe' diff --git a/packages/vbi/src/types/builder/report.ts b/packages/vbi/src/types/builder/report.ts new file mode 100644 index 0000000000..4771cfaeff --- /dev/null +++ b/packages/vbi/src/types/builder/report.ts @@ -0,0 +1,23 @@ +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' +import type { ReportPageCollectionBuilder } from 'src/report-builder/features/page' +import type { UndoManager } from 'src/chart-builder/features' +import type { Doc, Map } from 'yjs' +import type { VBIReportDSL } from '../reportDSL' +import type { VBIChartBuilderOptions } from './adapter' + +export interface VBIReportBuilderOptions { + chart?: VBIChartBuilderOptions +} + +export interface VBIReportBuilderInterface { + doc: Doc + dsl: Map + undoManager: UndoManager + page: ReportPageCollectionBuilder + + applyUpdate: (update: Uint8Array, origin?: any) => void + encodeStateAsUpdate: (targetStateVector?: Uint8Array) => Uint8Array + + build: () => VBIReportDSL + isEmpty: () => boolean +} diff --git a/packages/vbi/src/types/dsl/dimensions/aggregate.ts b/packages/vbi/src/types/chartDSL/dimensions/aggregate.ts similarity index 100% rename from packages/vbi/src/types/dsl/dimensions/aggregate.ts rename to packages/vbi/src/types/chartDSL/dimensions/aggregate.ts diff --git a/packages/vbi/src/types/dsl/dimensions/dimensions.ts b/packages/vbi/src/types/chartDSL/dimensions/dimensions.ts similarity index 100% rename from packages/vbi/src/types/dsl/dimensions/dimensions.ts rename to packages/vbi/src/types/chartDSL/dimensions/dimensions.ts diff --git a/packages/vbi/src/types/dsl/encoding.ts b/packages/vbi/src/types/chartDSL/encoding.ts similarity index 100% rename from packages/vbi/src/types/dsl/encoding.ts rename to packages/vbi/src/types/chartDSL/encoding.ts diff --git a/packages/vbi/src/types/dsl/havingFilter/having.ts b/packages/vbi/src/types/chartDSL/havingFilter/having.ts similarity index 100% rename from packages/vbi/src/types/dsl/havingFilter/having.ts rename to packages/vbi/src/types/chartDSL/havingFilter/having.ts diff --git a/packages/vbi/src/types/dsl/index.ts b/packages/vbi/src/types/chartDSL/index.ts similarity index 100% rename from packages/vbi/src/types/dsl/index.ts rename to packages/vbi/src/types/chartDSL/index.ts diff --git a/packages/vbi/src/types/dsl/locale/locale.ts b/packages/vbi/src/types/chartDSL/locale/locale.ts similarity index 100% rename from packages/vbi/src/types/dsl/locale/locale.ts rename to packages/vbi/src/types/chartDSL/locale/locale.ts diff --git a/packages/vbi/src/types/dsl/measures/aggregate.ts b/packages/vbi/src/types/chartDSL/measures/aggregate.ts similarity index 100% rename from packages/vbi/src/types/dsl/measures/aggregate.ts rename to packages/vbi/src/types/chartDSL/measures/aggregate.ts diff --git a/packages/vbi/src/types/dsl/measures/measures.ts b/packages/vbi/src/types/chartDSL/measures/measures.ts similarity index 100% rename from packages/vbi/src/types/dsl/measures/measures.ts rename to packages/vbi/src/types/chartDSL/measures/measures.ts diff --git a/packages/vbi/src/types/dsl/sort.ts b/packages/vbi/src/types/chartDSL/sort.ts similarity index 100% rename from packages/vbi/src/types/dsl/sort.ts rename to packages/vbi/src/types/chartDSL/sort.ts diff --git a/packages/vbi/src/types/dsl/theme/theme.ts b/packages/vbi/src/types/chartDSL/theme/theme.ts similarity index 100% rename from packages/vbi/src/types/dsl/theme/theme.ts rename to packages/vbi/src/types/chartDSL/theme/theme.ts diff --git a/packages/vbi/src/types/dsl/vbi/vbi.ts b/packages/vbi/src/types/chartDSL/vbi/vbi.ts similarity index 100% rename from packages/vbi/src/types/dsl/vbi/vbi.ts rename to packages/vbi/src/types/chartDSL/vbi/vbi.ts diff --git a/packages/vbi/src/types/dsl/whereFilter/date.ts b/packages/vbi/src/types/chartDSL/whereFilter/date.ts similarity index 100% rename from packages/vbi/src/types/dsl/whereFilter/date.ts rename to packages/vbi/src/types/chartDSL/whereFilter/date.ts diff --git a/packages/vbi/src/types/dsl/whereFilter/filters.ts b/packages/vbi/src/types/chartDSL/whereFilter/filters.ts similarity index 100% rename from packages/vbi/src/types/dsl/whereFilter/filters.ts rename to packages/vbi/src/types/chartDSL/whereFilter/filters.ts diff --git a/packages/vbi/src/types/index.ts b/packages/vbi/src/types/index.ts index 5ded67b181..176a260f07 100644 --- a/packages/vbi/src/types/index.ts +++ b/packages/vbi/src/types/index.ts @@ -1,3 +1,4 @@ -export * from './dsl' +export * from './chartDSL' +export * from './reportDSL' export * from './builder' export * from './connector' diff --git a/packages/vbi/src/types/reportDSL/index.ts b/packages/vbi/src/types/reportDSL/index.ts new file mode 100644 index 0000000000..2882249292 --- /dev/null +++ b/packages/vbi/src/types/reportDSL/index.ts @@ -0,0 +1,3 @@ +export type { VBIReportTextDSL, VBIReportTextDSLInput } from './text' +export type { VBIReportPageDSL, VBIReportPageDSLInput } from './page' +export type { VBIReportDSL, VBIReportDSLInput } from './report' diff --git a/packages/vbi/src/types/reportDSL/page.ts b/packages/vbi/src/types/reportDSL/page.ts new file mode 100644 index 0000000000..a1f7c236ed --- /dev/null +++ b/packages/vbi/src/types/reportDSL/page.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' +import { zVBIChartDSL } from '../chartDSL/vbi/vbi' +import { zVBIReportTextDSL } from './text' + +export const zVBIReportPageDSL = z.object({ + id: z.string(), + title: z.string(), + chart: zVBIChartDSL, + text: zVBIReportTextDSL.optional().default({ content: '' }), +}) + +export type VBIReportPageDSLInput = z.input +export type VBIReportPageDSL = z.output diff --git a/packages/vbi/src/types/reportDSL/report.ts b/packages/vbi/src/types/reportDSL/report.ts new file mode 100644 index 0000000000..943d33509b --- /dev/null +++ b/packages/vbi/src/types/reportDSL/report.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' +import { zVBIReportPageDSL } from './page' + +export const zVBIReportDSL = z.object({ + pages: z.array(zVBIReportPageDSL).optional().default([]), + version: z.number().int().min(0).optional().default(0), +}) + +export type VBIReportDSLInput = z.input +export type VBIReportDSL = z.output diff --git a/packages/vbi/src/types/reportDSL/text.ts b/packages/vbi/src/types/reportDSL/text.ts new file mode 100644 index 0000000000..7767348404 --- /dev/null +++ b/packages/vbi/src/types/reportDSL/text.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export const zVBIReportTextDSL = z.object({ + content: z.string().optional().default(''), +}) + +export type VBIReportTextDSLInput = z.input +export type VBIReportTextDSL = z.output diff --git a/packages/vbi/src/utils/filter-guards.ts b/packages/vbi/src/utils/filter-guards.ts index 87d88a9191..9589e6aa9d 100644 --- a/packages/vbi/src/utils/filter-guards.ts +++ b/packages/vbi/src/utils/filter-guards.ts @@ -5,7 +5,7 @@ import type { VBIHavingClause, VBIHavingFilter, VBIHavingGroup, -} from 'src/types/dsl' +} from 'src/types/chartDSL' export function isVBIFilter(clause: VBIWhereClause): clause is VBIWhereFilter { return 'field' in clause diff --git a/packages/vbi/src/vbi/create-vbi.ts b/packages/vbi/src/vbi/create-vbi.ts index 2f5be19979..ed6dc82785 100644 --- a/packages/vbi/src/vbi/create-vbi.ts +++ b/packages/vbi/src/vbi/create-vbi.ts @@ -1,19 +1,29 @@ -import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/builder/adapters/vquery-vseed/types' -import { connectorMap, getConnector, registerConnector } from 'src/builder/connector' -import type { VBIChartBuilder } from 'src/builder/builder' -import type { VBIChartDSLInput, VBIChartBuilderOptions } from 'src/types' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' +import { connectorMap, getConnector, registerConnector } from 'src/chart-builder/connector' +import type { VBIChartBuilder } from 'src/chart-builder/builder' +import type { VBIReportBuilder } from 'src/report-builder/builder' +import type { VBIChartDSLInput, VBIChartBuilderOptions, VBIReportBuilderOptions, VBIReportDSLInput } from 'src/types' import { createChartBuilderFromVBIChartDSLInput } from './from/from-vbi-dsl-input' +import { createReportBuilderFromVBIReportDSLInput } from './from/from-vbi-report-dsl-input' import { generateEmptyChartDSL } from './generate-empty-dsl' +import { generateEmptyReportDSL } from './generate-empty-report-dsl' +import { generateEmptyReportPageDSL } from './generate-empty-report-page-dsl' export interface VBIInstance { connectorMap: typeof connectorMap registerConnector: typeof registerConnector getConnector: typeof getConnector generateEmptyChartDSL: typeof generateEmptyChartDSL + generateEmptyReportDSL: typeof generateEmptyReportDSL + generateEmptyReportPageDSL: typeof generateEmptyReportPageDSL createChart: ( vbi: VBIChartDSLInput, builderOptions?: VBIChartBuilderOptions, ) => VBIChartBuilder + createReport: ( + report: VBIReportDSLInput, + builderOptions?: VBIReportBuilderOptions, + ) => VBIReportBuilder } const mergeBuilderOptions = ( @@ -38,6 +48,14 @@ const mergeBuilderOptions = ( + base?: VBIChartBuilderOptions, + overrides?: VBIReportBuilderOptions, +) => { + const chart = mergeBuilderOptions(base, overrides?.chart) + return chart ? { chart } : undefined +} + export function createVBI(): VBIInstance export function createVBI( defaultBuilderOptions: VBIChartBuilderOptions, @@ -48,12 +66,21 @@ export function createVBI) => { return createChartBuilderFromVBIChartDSLInput(vbi, mergeBuilderOptions(defaultBuilderOptions, builderOptions)) } + const createReport = (report: VBIReportDSLInput, builderOptions?: VBIReportBuilderOptions) => { + return createReportBuilderFromVBIReportDSLInput( + report, + mergeReportBuilderOptions(defaultBuilderOptions, builderOptions), + ) + } return { connectorMap, registerConnector, getConnector, generateEmptyChartDSL, + generateEmptyReportDSL, + generateEmptyReportPageDSL, createChart, + createReport, } } diff --git a/packages/vbi/src/vbi/from/fill-vbi-chart-dsl-map.ts b/packages/vbi/src/vbi/from/fill-vbi-chart-dsl-map.ts new file mode 100644 index 0000000000..5c11d9f3fe --- /dev/null +++ b/packages/vbi/src/vbi/from/fill-vbi-chart-dsl-map.ts @@ -0,0 +1,15 @@ +import * as Y from 'yjs' +import type { VBIChartDSLInput } from 'src/types' +import { ensureYArray } from '../normalize/ensure-y-array' +import { ensureWhereGroup } from '../normalize/ensure-where-group' +import { ensureHavingGroup } from '../normalize/ensure-having-group' +import { setBaseDSLFields } from './set-base-dsl-fields' + +export const fillVBIChartDSLMap = (dsl: Y.Map, vbi: VBIChartDSLInput) => { + dsl.clear() + setBaseDSLFields(dsl, vbi) + dsl.set('whereFilter', ensureWhereGroup(vbi.whereFilter)) + dsl.set('havingFilter', ensureHavingGroup(vbi.havingFilter)) + dsl.set('measures', ensureYArray(vbi.measures, 'field')) + dsl.set('dimensions', ensureYArray(vbi.dimensions, 'field')) +} diff --git a/packages/vbi/src/vbi/from/from-vbi-dsl-input.ts b/packages/vbi/src/vbi/from/from-vbi-dsl-input.ts index f16ad33676..755db83d71 100644 --- a/packages/vbi/src/vbi/from/from-vbi-dsl-input.ts +++ b/packages/vbi/src/vbi/from/from-vbi-dsl-input.ts @@ -1,11 +1,8 @@ import * as Y from 'yjs' -import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/builder/adapters/vquery-vseed/types' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' import type { VBIChartDSLInput, VBIChartBuilderOptions } from 'src/types' -import { VBIChartBuilder } from 'src/builder/builder' -import { ensureYArray } from '../normalize/ensure-y-array' -import { ensureWhereGroup } from '../normalize/ensure-where-group' -import { ensureHavingGroup } from '../normalize/ensure-having-group' -import { setBaseDSLFields } from './set-base-dsl-fields' +import { VBIChartBuilder } from 'src/chart-builder/builder' +import { fillVBIChartDSLMap } from './fill-vbi-chart-dsl-map' export const createChartBuilderFromVBIChartDSLInput = ( vbi: VBIChartDSLInput, @@ -15,12 +12,8 @@ export const createChartBuilderFromVBIChartDSLInput = { - setBaseDSLFields(dsl, vbi) - dsl.set('whereFilter', ensureWhereGroup(vbi.whereFilter)) - dsl.set('havingFilter', ensureHavingGroup(vbi.havingFilter)) - dsl.set('measures', ensureYArray(vbi.measures, 'field')) - dsl.set('dimensions', ensureYArray(vbi.dimensions, 'field')) + fillVBIChartDSLMap(dsl, vbi) }) - return new VBIChartBuilder(doc, options) + return new VBIChartBuilder(doc, options, dsl) } diff --git a/packages/vbi/src/vbi/from/from-vbi-report-dsl-input.ts b/packages/vbi/src/vbi/from/from-vbi-report-dsl-input.ts new file mode 100644 index 0000000000..3608c334e0 --- /dev/null +++ b/packages/vbi/src/vbi/from/from-vbi-report-dsl-input.ts @@ -0,0 +1,22 @@ +import * as Y from 'yjs' +import type { DefaultVBIQueryDSL, DefaultVBISeedDSL } from 'src/chart-builder/adapters/vquery-vseed/types' +import type { VBIReportDSLInput, VBIReportBuilderOptions } from 'src/types' +import { zVBIReportDSL } from 'src/types/reportDSL/report' +import { VBIReportBuilder } from 'src/report-builder/builder' +import { ensureReportPages } from './report-page-y-map' + +export const createReportBuilderFromVBIReportDSLInput = ( + report: VBIReportDSLInput, + options?: VBIReportBuilderOptions, +) => { + const doc = new Y.Doc() + const dsl = doc.getMap('dsl') + const normalized = zVBIReportDSL.parse(report) + + doc.transact(() => { + dsl.set('version', normalized.version) + dsl.set('pages', ensureReportPages(normalized.pages)) + }) + + return new VBIReportBuilder(doc, options) +} diff --git a/packages/vbi/src/vbi/from/report-page-y-map.ts b/packages/vbi/src/vbi/from/report-page-y-map.ts new file mode 100644 index 0000000000..44952af871 --- /dev/null +++ b/packages/vbi/src/vbi/from/report-page-y-map.ts @@ -0,0 +1,69 @@ +import * as Y from 'yjs' +import type { VBIReportPageDSLInput, VBIReportTextDSLInput } from 'src/types' +import { id } from 'src/utils' +import { generateEmptyChartDSL } from '../generate-empty-dsl' +import { fillVBIChartDSLMap } from './fill-vbi-chart-dsl-map' + +const createReportTextYMap = (text?: VBIReportTextDSLInput) => { + const yMap = new Y.Map() + yMap.set('content', text?.content ?? '') + return yMap +} + +export const createReportPageYMap = (page: VBIReportPageDSLInput): Y.Map => { + const yMap = new Y.Map() + const chart = new Y.Map() + + yMap.set('id', page.id || id.uuid()) + yMap.set('title', page.title) + fillVBIChartDSLMap(chart, page.chart) + yMap.set('chart', chart) + yMap.set('text', createReportTextYMap(page.text)) + return yMap +} + +export const ensureReportPages = (pages?: VBIReportPageDSLInput[]) => { + const yArray = new Y.Array() + for (const page of pages ?? []) { + yArray.push([createReportPageYMap(page)]) + } + return yArray +} + +export const getOrCreateReportPages = (dsl: Y.Map) => { + const pages = dsl.get('pages') + if (pages instanceof Y.Array) { + return pages as Y.Array> + } + + const nextPages = new Y.Array>() + dsl.set('pages', nextPages) + return nextPages +} + +export const getOrCreateReportChartMap = (page: Y.Map) => { + const chart = page.get('chart') + if (chart instanceof Y.Map) { + return chart as Y.Map + } + + const nextChart = new Y.Map() + fillVBIChartDSLMap(nextChart, generateEmptyChartDSL('')) + page.set('chart', nextChart) + return nextChart +} + +export const getOrCreateReportTextMap = (page: Y.Map) => { + const text = page.get('text') + if (text instanceof Y.Map) { + return text as Y.Map + } + + const nextText = createReportTextYMap() + page.set('text', nextText) + return nextText +} + +export const locateReportPageIndexById = (pages: Y.Array>, pageId: string) => { + return pages.toArray().findIndex((page) => page.get('id') === pageId) +} diff --git a/packages/vbi/src/vbi/from/set-base-dsl-fields.ts b/packages/vbi/src/vbi/from/set-base-dsl-fields.ts index c6bd27bef9..463717d75b 100644 --- a/packages/vbi/src/vbi/from/set-base-dsl-fields.ts +++ b/packages/vbi/src/vbi/from/set-base-dsl-fields.ts @@ -2,10 +2,10 @@ import * as Y from 'yjs' import type { VBIChartDSLInput } from 'src/types' export const setBaseDSLFields = (dsl: Y.Map, vbi: VBIChartDSLInput) => { - if (vbi.connectorId) dsl.set('connectorId', vbi.connectorId) - if (vbi.chartType) dsl.set('chartType', vbi.chartType) - if (vbi.theme) dsl.set('theme', vbi.theme) - if (vbi.limit) dsl.set('limit', vbi.limit) - if (vbi.locale) dsl.set('locale', vbi.locale) + if (vbi.connectorId !== undefined) dsl.set('connectorId', vbi.connectorId) + if (vbi.chartType !== undefined) dsl.set('chartType', vbi.chartType) + if (vbi.theme !== undefined) dsl.set('theme', vbi.theme) + if (vbi.limit !== undefined) dsl.set('limit', vbi.limit) + if (vbi.locale !== undefined) dsl.set('locale', vbi.locale) if (vbi.version !== undefined) dsl.set('version', vbi.version) } diff --git a/packages/vbi/src/vbi/generate-empty-report-dsl.ts b/packages/vbi/src/vbi/generate-empty-report-dsl.ts new file mode 100644 index 0000000000..4482a2087a --- /dev/null +++ b/packages/vbi/src/vbi/generate-empty-report-dsl.ts @@ -0,0 +1,8 @@ +import type { VBIReportDSL } from 'src/types' + +export const generateEmptyReportDSL = (): VBIReportDSL => { + return { + pages: [], + version: 0, + } +} diff --git a/packages/vbi/src/vbi/generate-empty-report-page-dsl.ts b/packages/vbi/src/vbi/generate-empty-report-page-dsl.ts new file mode 100644 index 0000000000..1bd994490a --- /dev/null +++ b/packages/vbi/src/vbi/generate-empty-report-page-dsl.ts @@ -0,0 +1,15 @@ +import type { VBIConnectorId } from 'src/types/connector/connector' +import type { VBIReportPageDSL } from 'src/types' +import { id } from 'src/utils' +import { generateEmptyChartDSL } from './generate-empty-dsl' + +export const generateEmptyReportPageDSL = (connectorId: VBIConnectorId = ''): VBIReportPageDSL => { + return { + id: id.uuid(), + title: '', + chart: generateEmptyChartDSL(connectorId), + text: { + content: '', + }, + } +} diff --git a/packages/vbi/src/vbi/normalize/ensure-having-group.ts b/packages/vbi/src/vbi/normalize/ensure-having-group.ts index f25f534447..8b6801b406 100644 --- a/packages/vbi/src/vbi/normalize/ensure-having-group.ts +++ b/packages/vbi/src/vbi/normalize/ensure-having-group.ts @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import { createHavingGroup } from 'src/builder/features/havingFilter/having-utils' +import { createHavingGroup } from 'src/chart-builder/features/havingFilter/having-utils' import { ensureYArray } from './ensure-y-array' import type { FilterGroupInput } from './types' diff --git a/packages/vbi/src/vbi/normalize/ensure-where-group.ts b/packages/vbi/src/vbi/normalize/ensure-where-group.ts index b266627d48..5ad0ccc68d 100644 --- a/packages/vbi/src/vbi/normalize/ensure-where-group.ts +++ b/packages/vbi/src/vbi/normalize/ensure-where-group.ts @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import { createWhereGroup } from 'src/builder/features/whereFilter/where-utils' +import { createWhereGroup } from 'src/chart-builder/features/whereFilter/where-utils' import { ensureYArray } from './ensure-y-array' import type { FilterGroupInput } from './types' diff --git a/packages/vbi/tests/builder/builder.test.ts b/packages/vbi/tests/builder/builder.test.ts index 5509ab47aa..f92d1ec201 100644 --- a/packages/vbi/tests/builder/builder.test.ts +++ b/packages/vbi/tests/builder/builder.test.ts @@ -1,6 +1,6 @@ import { createVBI, VBI } from '@visactor/vbi' -import { VBIChartDSL } from 'src/types/dsl' -import { getConnector, registerConnector } from 'src/builder/connector' +import { VBIChartDSL } from 'src/types/chartDSL' +import { getConnector, registerConnector } from 'src/chart-builder/connector' describe('VBI', () => { test('build', () => { diff --git a/packages/vbi/tests/builder/features/chartType.test.ts b/packages/vbi/tests/builder/features/chartType.test.ts index a98b962b29..1739f07416 100644 --- a/packages/vbi/tests/builder/features/chartType.test.ts +++ b/packages/vbi/tests/builder/features/chartType.test.ts @@ -1,5 +1,5 @@ import { VBI } from '@visactor/vbi' -import { VBIChartDSL } from 'src/types/dsl' +import { VBIChartDSL } from 'src/types/chartDSL' describe('ChartTypeBuilder', () => { test('changeChartType', () => { diff --git a/packages/vbi/tests/builder/features/configFeatures.test.ts b/packages/vbi/tests/builder/features/configFeatures.test.ts index 6ee8d3e01c..2bf3aea7b8 100644 --- a/packages/vbi/tests/builder/features/configFeatures.test.ts +++ b/packages/vbi/tests/builder/features/configFeatures.test.ts @@ -1,15 +1,15 @@ import { VBI } from '@visactor/vbi' -import * as features from 'src/builder/features' -import { ChartTypeBuilder } from 'src/builder/features/chart-type' -import { DimensionsBuilder } from 'src/builder/features/dimensions' -import { HavingFilterBuilder } from 'src/builder/features/havingFilter' -import { LimitBuilder } from 'src/builder/features/limit' -import { LocaleBuilder } from 'src/builder/features/locale' -import { MeasuresBuilder } from 'src/builder/features/measures' -import { ThemeBuilder } from 'src/builder/features/theme' -import { UndoManager } from 'src/builder/features/undo-manager' -import { WhereFilterBuilder } from 'src/builder/features/whereFilter' -import type { VBIChartDSL } from 'src/types/dsl' +import * as features from 'src/chart-builder/features' +import { ChartTypeBuilder } from 'src/chart-builder/features/chart-type' +import { DimensionsBuilder } from 'src/chart-builder/features/dimensions' +import { HavingFilterBuilder } from 'src/chart-builder/features/havingFilter' +import { LimitBuilder } from 'src/chart-builder/features/limit' +import { LocaleBuilder } from 'src/chart-builder/features/locale' +import { MeasuresBuilder } from 'src/chart-builder/features/measures' +import { ThemeBuilder } from 'src/chart-builder/features/theme' +import { UndoManager } from 'src/chart-builder/features/undo-manager' +import { WhereFilterBuilder } from 'src/chart-builder/features/whereFilter' +import type { VBIChartDSL } from 'src/types/chartDSL' describe('feature barrels', () => { test('export all feature builders from the root barrel', () => { diff --git a/packages/vbi/tests/builder/features/dimensions.test.ts b/packages/vbi/tests/builder/features/dimensions.test.ts index 051c4040a0..4ca228c0d1 100644 --- a/packages/vbi/tests/builder/features/dimensions.test.ts +++ b/packages/vbi/tests/builder/features/dimensions.test.ts @@ -1,6 +1,6 @@ import { VBI } from '@visactor/vbi' -import { VBIChartDSL } from 'src/types/dsl' -import { DimensionsBuilder } from 'src/builder/features/dimensions/dim-builder' +import { VBIChartDSL } from 'src/types/chartDSL' +import { DimensionsBuilder } from 'src/chart-builder/features/dimensions/dim-builder' describe('DimensionsBuilder', () => { test('addDimension', () => { diff --git a/packages/vbi/tests/builder/features/filterCoverage.test.ts b/packages/vbi/tests/builder/features/filterCoverage.test.ts index 5c8ca274e5..ba7ab43136 100644 --- a/packages/vbi/tests/builder/features/filterCoverage.test.ts +++ b/packages/vbi/tests/builder/features/filterCoverage.test.ts @@ -1,18 +1,18 @@ import * as Y from 'yjs' import { VBI } from '@visactor/vbi' -import { HavingFilterBuilder } from 'src/builder/features/havingFilter/having-builder' +import { HavingFilterBuilder } from 'src/chart-builder/features/havingFilter/having-builder' import { createHavingGroup, findEntry as findHavingEntry, isHavingGroup, -} from 'src/builder/features/havingFilter/having-utils' -import { WhereFilterBuilder } from 'src/builder/features/whereFilter/where-builder' +} from 'src/chart-builder/features/havingFilter/having-utils' +import { WhereFilterBuilder } from 'src/chart-builder/features/whereFilter/where-builder' import { createWhereGroup, findEntry as findWhereEntry, isWhereGroup, -} from 'src/builder/features/whereFilter/where-utils' -import type { VBIChartDSL } from 'src/types/dsl' +} from 'src/chart-builder/features/whereFilter/where-utils' +import type { VBIChartDSL } from 'src/types/chartDSL' describe('Where filter internals', () => { test('constructor initializes a missing whereFilter root on plain Y DSL', () => { diff --git a/packages/vbi/tests/builder/features/havingFilter.test.ts b/packages/vbi/tests/builder/features/havingFilter.test.ts index 96e1a3994e..e815ca8139 100644 --- a/packages/vbi/tests/builder/features/havingFilter.test.ts +++ b/packages/vbi/tests/builder/features/havingFilter.test.ts @@ -1,9 +1,9 @@ import * as Y from 'yjs' import { VBI } from '@visactor/vbi' -import { VBIChartDSL } from 'src/types/dsl' -import { HavingFilterNodeBuilder } from 'src/builder/features/havingFilter/having-node-builder' -import { HavingGroupBuilder } from 'src/builder/features/havingFilter/having-group-builder' -import { HavingFilterBuilder } from 'src/builder/features/havingFilter/having-builder' +import { VBIChartDSL } from 'src/types/chartDSL' +import { HavingFilterNodeBuilder } from 'src/chart-builder/features/havingFilter/having-node-builder' +import { HavingGroupBuilder } from 'src/chart-builder/features/havingFilter/having-group-builder' +import { HavingFilterBuilder } from 'src/chart-builder/features/havingFilter/having-builder' describe('HavingFilterBuilder', () => { test('add having filter', () => { diff --git a/packages/vbi/tests/builder/features/measures.test.ts b/packages/vbi/tests/builder/features/measures.test.ts index e33a1a383d..866a4531fe 100644 --- a/packages/vbi/tests/builder/features/measures.test.ts +++ b/packages/vbi/tests/builder/features/measures.test.ts @@ -1,6 +1,6 @@ import { VBI } from '@visactor/vbi' -import { VBIChartDSL } from 'src/types/dsl' -import { MeasuresBuilder } from 'src/builder/features/measures/mea-builder' +import { VBIChartDSL } from 'src/types/chartDSL' +import { MeasuresBuilder } from 'src/chart-builder/features/measures/mea-builder' import { registerDemoConnector } from '../../demoConnector' describe('MeasuresBuilder', () => { diff --git a/packages/vbi/tests/builder/features/sort.test.ts b/packages/vbi/tests/builder/features/sort.test.ts index fc042e37c7..d12ab52d42 100644 --- a/packages/vbi/tests/builder/features/sort.test.ts +++ b/packages/vbi/tests/builder/features/sort.test.ts @@ -1,5 +1,5 @@ import { VBI } from '@visactor/vbi' -import type { VBIChartDSL } from 'src/types/dsl' +import type { VBIChartDSL } from 'src/types/chartDSL' describe('sort builders', () => { test('DimensionNodeBuilder set/get/clearSort works', () => { diff --git a/packages/vbi/tests/builder/features/whereFilter.test.ts b/packages/vbi/tests/builder/features/whereFilter.test.ts index c4c502d82f..66dd0dca5c 100644 --- a/packages/vbi/tests/builder/features/whereFilter.test.ts +++ b/packages/vbi/tests/builder/features/whereFilter.test.ts @@ -1,6 +1,6 @@ import * as Y from 'yjs' import { VBI } from '@visactor/vbi' -import type { VBIChartDSL, VBIWhereFilter } from 'src/types/dsl' +import type { VBIChartDSL, VBIWhereFilter } from 'src/types/chartDSL' describe('WhereFilterBuilder', () => { test('addWhereFilter', () => { diff --git a/packages/vbi/tests/builder/filters.test.ts b/packages/vbi/tests/builder/filters.test.ts index 785956eeed..2655f75665 100644 --- a/packages/vbi/tests/builder/filters.test.ts +++ b/packages/vbi/tests/builder/filters.test.ts @@ -1,5 +1,5 @@ import { VBI } from '@visactor/vbi' -import type { VBIChartDSL, VBIWhereFilter } from 'src/types/dsl' +import type { VBIChartDSL, VBIWhereFilter } from 'src/types/chartDSL' describe('WhereFilterBuilder', () => { test('add', () => { diff --git a/packages/vbi/tests/builder/reportBuilder.test.ts b/packages/vbi/tests/builder/reportBuilder.test.ts new file mode 100644 index 0000000000..57abca5d68 --- /dev/null +++ b/packages/vbi/tests/builder/reportBuilder.test.ts @@ -0,0 +1,109 @@ +import { createVBI, VBI } from '@visactor/vbi' + +describe('VBIReportBuilder', () => { + test('page.add builds report from chart builder and text', () => { + const chartBuilder = VBI.createChart(VBI.generateEmptyChartDSL('demo')) + chartBuilder.measures.add('sales', (node) => { + node.setAlias('Sales').setAggregate({ func: 'sum' }).setEncoding('yAxis') + }) + + const reportBuilder = VBI.createReport(VBI.generateEmptyReportDSL()) + const report = reportBuilder.page + .add('Story One', (page) => page.setChart(chartBuilder).setText('hello world')) + .build() + + expect(report).toEqual({ + pages: [ + { + id: 'id-2', + title: 'Story One', + chart: { + connectorId: 'demo', + chartType: 'table', + measures: [ + { + id: 'id-1', + aggregate: { func: 'sum' }, + alias: 'Sales', + encoding: 'yAxis', + field: 'sales', + }, + ], + dimensions: [], + whereFilter: { id: 'root', op: 'and', conditions: [] }, + havingFilter: { id: 'root', op: 'and', conditions: [] }, + theme: 'light', + locale: 'zh-CN', + version: 0, + }, + text: { content: 'hello world' }, + }, + ], + version: 0, + }) + }) + + test('page.update and page.remove work on existing pages', () => { + const reportBuilder = VBI.createReport(VBI.generateEmptyReportDSL()) + const pageId = reportBuilder.page.add('Story One').build().pages[0].id + + reportBuilder.page.update(pageId, (page) => { + page.setTitle('Story Two').setText('updated') + }) + + expect(reportBuilder.build().pages[0]).toMatchObject({ + id: pageId, + title: 'Story Two', + text: { content: 'updated' }, + }) + + reportBuilder.page.remove(pageId) + expect(reportBuilder.isEmpty()).toBe(true) + }) + + test('setChart copies chart DSL instead of sharing builder state', () => { + const chartBuilder = VBI.createChart(VBI.generateEmptyChartDSL('demo')) + const reportBuilder = VBI.createReport(VBI.generateEmptyReportDSL()) + const pageId = reportBuilder.page.add('Story One', (page) => page.setChart(chartBuilder)).build().pages[0].id + + chartBuilder.dimensions.add('area', (node) => { + node.setAlias('Area') + }) + + expect(reportBuilder.page.get(pageId)?.chart.build().dimensions).toEqual([]) + }) + + test('createReport uses default chart builder options from createVBI', () => { + type CustomQueryDSL = { source: 'factory'; count: number } + type CustomSeedDSL = { type: 'custom-seed'; queryDSL: CustomQueryDSL } + + const CustomVBI = createVBI({ + adapters: { + buildVQuery: ({ vbiDSL }) => ({ source: 'factory', count: vbiDSL.measures.length }), + buildVSeed: async ({ queryDSL }) => ({ type: 'custom-seed', queryDSL }), + }, + }) + + const chartBuilder = CustomVBI.createChart(CustomVBI.generateEmptyChartDSL('demo')) + const reportBuilder = CustomVBI.createReport(CustomVBI.generateEmptyReportDSL()) + const pageId = reportBuilder.page.add('Story One', (page) => page.setChart(chartBuilder)).build().pages[0].id + + expect(reportBuilder.page.get(pageId)?.chart.buildVQuery()).toEqual({ + source: 'factory', + count: 0, + }) + }) + + test('report builders sync through YJS updates', () => { + const b1 = VBI.createReport(VBI.generateEmptyReportDSL()) + const b2 = VBI.createReport(VBI.generateEmptyReportDSL()) + + b2.applyUpdate(b1.encodeStateAsUpdate()) + b1.applyUpdate(b2.encodeStateAsUpdate()) + + b1.page.add('Story One', (page) => page.setText('synced')) + b2.applyUpdate(b1.encodeStateAsUpdate()) + + expect(b2.build()).toEqual(b1.build()) + }) +}) diff --git a/packages/vbi/tests/builder/undo-manager.test.ts b/packages/vbi/tests/builder/undo-manager.test.ts index 23c2db44ef..65310deae8 100644 --- a/packages/vbi/tests/builder/undo-manager.test.ts +++ b/packages/vbi/tests/builder/undo-manager.test.ts @@ -1,5 +1,5 @@ import { VBI } from '@visactor/vbi' -import { VBIChartDSL } from 'src/types/dsl' +import { VBIChartDSL } from 'src/types/chartDSL' describe('UndoManager', () => { test('basic undo/redo', () => { diff --git a/packages/vbi/tests/builder/yjs.test.ts b/packages/vbi/tests/builder/yjs.test.ts index eb6360c482..8f5305e694 100644 --- a/packages/vbi/tests/builder/yjs.test.ts +++ b/packages/vbi/tests/builder/yjs.test.ts @@ -1,5 +1,5 @@ import { VBI } from '@visactor/vbi' -import { VBIChartDSL } from 'src/types/dsl' +import { VBIChartDSL } from 'src/types/chartDSL' describe('VBI YJS Integration', () => { test('sync between two builders', () => { diff --git a/packages/vbi/tests/examples/whereFilter/whereFilter.test.ts b/packages/vbi/tests/examples/whereFilter/whereFilter.test.ts index eb6c39d90b..737becda99 100644 --- a/packages/vbi/tests/examples/whereFilter/whereFilter.test.ts +++ b/packages/vbi/tests/examples/whereFilter/whereFilter.test.ts @@ -1988,12 +1988,12 @@ describe('WhereFilter', () => { { "field": "order_date", "op": ">=", - "value": "2026-02-21", + "value": "2026-02-22", }, { "field": "order_date", "op": "<", - "value": "2026-03-23", + "value": "2026-03-24", }, { "field": "sales", diff --git a/packages/vbi/tests/query/orderBy.test.ts b/packages/vbi/tests/query/orderBy.test.ts index c47d9ea452..2dd559f322 100644 --- a/packages/vbi/tests/query/orderBy.test.ts +++ b/packages/vbi/tests/query/orderBy.test.ts @@ -1,5 +1,5 @@ import { VBI } from '@visactor/vbi' -import type { VBIChartDSL } from 'src/types/dsl' +import type { VBIChartDSL } from 'src/types/chartDSL' describe('orderBy', () => { test('defaults to the first dimension when no explicit sort exists', () => { diff --git a/packages/vbi/tests/types/dateFilterSchemas.test.ts b/packages/vbi/tests/types/dateFilterSchemas.test.ts index b9f989f4dd..a79fe62d6c 100644 --- a/packages/vbi/tests/types/dateFilterSchemas.test.ts +++ b/packages/vbi/tests/types/dateFilterSchemas.test.ts @@ -3,7 +3,7 @@ import { zVBIWhereFilter, zVBIWhereDateFilter, zVBIWhereScalarFilter, -} from 'src/types/dsl/whereFilter/filters' +} from 'src/types/chartDSL/whereFilter/filters' describe('VBIWhereDatePredicate schema', () => { test('parse range predicate', () => { diff --git a/packages/vbi/tests/types/reportSchemas.test.ts b/packages/vbi/tests/types/reportSchemas.test.ts new file mode 100644 index 0000000000..fa19878f10 --- /dev/null +++ b/packages/vbi/tests/types/reportSchemas.test.ts @@ -0,0 +1,59 @@ +import { zVBIReportDSL } from 'src/types/reportDSL/report' +import { generateEmptyReportDSL } from 'src/vbi/generate-empty-report-dsl' +import { generateEmptyReportPageDSL } from 'src/vbi/generate-empty-report-page-dsl' + +describe('report DSL schemas', () => { + test('parse minimal report DSL', () => { + expect(zVBIReportDSL.parse(generateEmptyReportDSL())).toEqual({ + pages: [], + version: 0, + }) + }) + + test('page requires title, chart and text', () => { + const page = generateEmptyReportPageDSL('demo') + const withoutTitle: Partial = { ...page } + const withoutChart: Partial = { ...page } + const withoutText: Partial = { ...page } + + delete withoutTitle.title + delete withoutChart.chart + delete withoutText.text + + expect(() => zVBIReportDSL.parse({ pages: [withoutTitle], version: 0 })).toThrow() + expect(() => zVBIReportDSL.parse({ pages: [withoutChart], version: 0 })).toThrow() + expect(zVBIReportDSL.parse({ pages: [withoutText], version: 0 }).pages[0].text).toEqual({ content: '' }) + }) + + test('page.chart reuses chart schema validation', () => { + const page = generateEmptyReportPageDSL('demo') + + expect(() => + zVBIReportDSL.parse({ + pages: [{ ...page, chart: { ...page.chart, connectorId: 1 } }], + version: 0, + }), + ).toThrow() + }) + + test('empty report helpers stay stable', () => { + expect(generateEmptyReportPageDSL('demo')).toEqual({ + id: 'id-1', + title: '', + chart: { + connectorId: 'demo', + chartType: 'table', + measures: [], + dimensions: [], + whereFilter: { id: 'root', op: 'and', conditions: [] }, + havingFilter: { id: 'root', op: 'and', conditions: [] }, + theme: 'light', + locale: 'zh-CN', + version: 0, + }, + text: { + content: '', + }, + }) + }) +}) diff --git a/packages/vbi/tests/types/runtimeSchemas.test.ts b/packages/vbi/tests/types/runtimeSchemas.test.ts index 1c002767c3..8c26e9d65b 100644 --- a/packages/vbi/tests/types/runtimeSchemas.test.ts +++ b/packages/vbi/tests/types/runtimeSchemas.test.ts @@ -1,12 +1,12 @@ -import { zDimensionAggregate } from 'src/types/dsl/dimensions/aggregate' -import { zVBIDimensionGroupSchema, zVBIDimensionTree } from 'src/types/dsl/dimensions/dimensions' -import { zVBIHavingClause, zVBIHavingFilter, zVBIHavingGroup } from 'src/types/dsl/havingFilter/having' -import { zVBIDSLLocale } from 'src/types/dsl/locale/locale' -import { zAggregate } from 'src/types/dsl/measures/aggregate' -import { zVBIMeasure, zVBIMeasureGroup, zVBIMeasureTree } from 'src/types/dsl/measures/measures' -import { zVBIDSLTheme } from 'src/types/dsl/theme/theme' -import { zVBIChartDSL } from 'src/types/dsl/vbi/vbi' -import { zVBIWhereFilter, zVBIWhereClause, zVBIWhereGroup } from 'src/types/dsl/whereFilter/filters' +import { zDimensionAggregate } from 'src/types/chartDSL/dimensions/aggregate' +import { zVBIDimensionGroupSchema, zVBIDimensionTree } from 'src/types/chartDSL/dimensions/dimensions' +import { zVBIHavingClause, zVBIHavingFilter, zVBIHavingGroup } from 'src/types/chartDSL/havingFilter/having' +import { zVBIDSLLocale } from 'src/types/chartDSL/locale/locale' +import { zAggregate } from 'src/types/chartDSL/measures/aggregate' +import { zVBIMeasure, zVBIMeasureGroup, zVBIMeasureTree } from 'src/types/chartDSL/measures/measures' +import { zVBIDSLTheme } from 'src/types/chartDSL/theme/theme' +import { zVBIChartDSL } from 'src/types/chartDSL/vbi/vbi' +import { zVBIWhereFilter, zVBIWhereClause, zVBIWhereGroup } from 'src/types/chartDSL/whereFilter/filters' import { findTreeNodesBy, id, diff --git a/packages/vbi/tests/types/sortSchemas.test.ts b/packages/vbi/tests/types/sortSchemas.test.ts index 6d8118cdc5..11e04eaca2 100644 --- a/packages/vbi/tests/types/sortSchemas.test.ts +++ b/packages/vbi/tests/types/sortSchemas.test.ts @@ -1,7 +1,7 @@ -import { zVBIDimensionSchema } from 'src/types/dsl/dimensions/dimensions' -import { zVBIMeasure } from 'src/types/dsl/measures/measures' -import { zVBISort } from 'src/types/dsl/sort' -import { zVBIChartDSL } from 'src/types/dsl/vbi/vbi' +import { zVBIDimensionSchema } from 'src/types/chartDSL/dimensions/dimensions' +import { zVBIMeasure } from 'src/types/chartDSL/measures/measures' +import { zVBISort } from 'src/types/chartDSL/sort' +import { zVBIChartDSL } from 'src/types/chartDSL/vbi/vbi' describe('sort schemas', () => { test('zVBISort parses asc and desc', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bc7e584f6..2849b39a7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -204,9 +204,6 @@ importers: antd: specifier: 6.1.3 version: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - demo: - specifier: workspace:* - version: link:../../practices/demo react: specifier: 19.2.3 version: 19.2.3 @@ -219,6 +216,9 @@ importers: remeda: specifier: 2.28.0 version: 2.28.0 + standard: + specifier: workspace:* + version: link:../../practices/standard yjs: specifier: 13.6.28 version: 13.6.28 @@ -295,15 +295,18 @@ importers: '@visactor/vtable': specifier: 1.23.1 version: 1.23.1 - demo: - specifier: workspace:* - version: link:../../practices/demo minimalist: specifier: workspace:* version: link:../../practices/minimalist professional: specifier: workspace:* version: link:../../practices/professional + standard: + specifier: workspace:* + version: link:../../practices/standard + standard-report: + specifier: workspace:* + version: link:../../practices/standard-report streamlined: specifier: workspace:* version: link:../../practices/streamlined @@ -580,23 +583,11 @@ importers: specifier: 3.2.4 version: 3.2.4(@types/debug@4.1.13)(@types/node@24.10.1)(happy-dom@20.8.4)(jiti@2.6.1)(jsdom@26.1.0)(terser@5.46.1)(yaml@2.8.2) - practices/demo: + practices/minimalist: dependencies: '@ant-design/icons': specifier: 6.1.0 version: 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@dnd-kit/core': - specifier: ^6.3.1 - version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@dnd-kit/modifiers': - specifier: ^9.0.0 - version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) - '@dnd-kit/sortable': - specifier: ^10.0.0 - version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) - '@dnd-kit/utilities': - specifier: ^3.2.2 - version: 3.2.2(react@19.2.3) '@visactor/vbi': specifier: workspace:* version: link:../../packages/vbi @@ -615,9 +606,85 @@ importers: antd: specifier: 6.1.3 version: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - dayjs: - specifier: 1.11.20 - version: 1.11.20 + react-dom: + specifier: '>=16.9.0' + version: 19.2.3(react@19.2.3) + zustand: + specifier: 5.0.6 + version: 5.0.6(@types/react@19.2.14)(react@19.2.3) + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 + '@rsbuild/plugin-react': + specifier: ^1.4.3 + version: 1.4.6(@rsbuild/core@1.7.3) + '@rslib/core': + specifier: ^0.19.3 + version: 0.19.6(typescript@5.9.3) + '@rstest/adapter-rslib': + specifier: ^0.1.1 + version: 0.1.1(@rslib/core@0.19.6(typescript@5.9.3))(typescript@5.9.3) + '@rstest/core': + specifier: 0.8.3 + version: 0.8.3(happy-dom@20.8.4)(jsdom@27.3.0) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.1 + version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/react': + specifier: ^19.2.8 + version: 19.2.14 + eslint: + specifier: ^9.39.1 + version: 9.39.1(jiti@2.6.1) + globals: + specifier: ^16.5.0 + version: 16.5.0 + happy-dom: + specifier: ^20.3.3 + version: 20.8.4 + prettier: + specifier: ^3.7.3 + version: 3.7.3 + react: + specifier: ^19.2.3 + version: 19.2.3 + typescript: + specifier: 5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.48.0 + version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + + practices/professional: + dependencies: + '@ant-design/icons': + specifier: 6.1.0 + version: 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@visactor/vbi': + specifier: workspace:* + version: link:../../packages/vbi + '@visactor/vbi-react': + specifier: workspace:* + version: link:../../packages/vbi-react + '@visactor/vchart': + specifier: 2.0.15 + version: 2.0.15 + '@visactor/vquery': + specifier: workspace:* + version: link:../../packages/vquery + '@visactor/vseed': + specifier: workspace:* + version: link:../../packages/vseed + '@visactor/vtable': + specifier: 1.23.1 + version: 1.23.1 + antd: + specifier: 6.1.3 + version: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) zustand: specifier: 5.0.6 version: 5.0.6(@types/react@19.2.7)(react@19.2.3) @@ -643,6 +710,9 @@ importers: '@types/react': specifier: 19.2.7 version: 19.2.7 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.7) eslint: specifier: 9.39.1 version: 9.39.1(jiti@2.6.1) @@ -668,11 +738,23 @@ importers: specifier: 8.48.0 version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - practices/minimalist: + practices/standard: dependencies: '@ant-design/icons': specifier: 6.1.0 version: 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.3) '@visactor/vbi': specifier: workspace:* version: link:../../packages/vbi @@ -691,60 +773,60 @@ importers: antd: specifier: 6.1.3 version: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react-dom: - specifier: '>=16.9.0' - version: 19.2.3(react@19.2.3) + dayjs: + specifier: 1.11.20 + version: 1.11.20 zustand: specifier: 5.0.6 - version: 5.0.6(@types/react@19.2.14)(react@19.2.3) + version: 5.0.6(@types/react@19.2.7)(react@19.2.3) devDependencies: '@eslint/js': - specifier: ^9.39.1 + specifier: 9.39.1 version: 9.39.1 '@rsbuild/plugin-react': - specifier: ^1.4.3 - version: 1.4.6(@rsbuild/core@1.7.3) + specifier: 1.4.2 + version: 1.4.2(@rsbuild/core@2.0.0-beta.6(core-js@3.47.0)) '@rslib/core': - specifier: ^0.19.3 - version: 0.19.6(typescript@5.9.3) - '@rstest/adapter-rslib': - specifier: ^0.1.1 - version: 0.1.1(@rslib/core@0.19.6(typescript@5.9.3))(typescript@5.9.3) + specifier: 0.18.6 + version: 0.18.6(typescript@5.9.3) '@rstest/core': specifier: 0.8.3 - version: 0.8.3(happy-dom@20.8.4)(jsdom@27.3.0) + version: 0.8.3(happy-dom@20.8.4)(jsdom@26.1.0) '@testing-library/jest-dom': - specifier: ^6.9.1 + specifier: 6.9.1 version: 6.9.1 '@testing-library/react': - specifier: ^16.3.1 - version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.3.1 + version: 16.3.1(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@types/react': - specifier: ^19.2.8 - version: 19.2.14 + specifier: 19.2.7 + version: 19.2.7 eslint: - specifier: ^9.39.1 + specifier: 9.39.1 version: 9.39.1(jiti@2.6.1) globals: - specifier: ^16.5.0 + specifier: 16.5.0 version: 16.5.0 - happy-dom: - specifier: ^20.3.3 - version: 20.8.4 + jsdom: + specifier: 26.1.0 + version: 26.1.0 prettier: - specifier: ^3.7.3 + specifier: 3.7.3 version: 3.7.3 react: - specifier: ^19.2.3 + specifier: 19.2.3 version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) typescript: specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.48.0 + specifier: 8.48.0 version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - practices/professional: + practices/standard-report: dependencies: '@ant-design/icons': specifier: 6.1.0 @@ -752,24 +834,27 @@ importers: '@visactor/vbi': specifier: workspace:* version: link:../../packages/vbi - '@visactor/vbi-react': - specifier: workspace:* - version: link:../../packages/vbi-react - '@visactor/vchart': - specifier: 2.0.15 - version: 2.0.15 '@visactor/vquery': specifier: workspace:* version: link:../../packages/vquery '@visactor/vseed': specifier: workspace:* version: link:../../packages/vseed - '@visactor/vtable': - specifier: 1.23.1 - version: 1.23.1 antd: specifier: 6.1.3 version: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + motion: + specifier: 12.38.0 + version: 12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + standard: + specifier: workspace:* + version: link:../standard zustand: specifier: 5.0.6 version: 5.0.6(@types/react@19.2.7)(react@19.2.3) @@ -795,9 +880,6 @@ importers: '@types/react': specifier: 19.2.7 version: 19.2.7 - '@types/react-dom': - specifier: 19.2.3 - version: 19.2.3(@types/react@19.2.7) eslint: specifier: 9.39.1 version: 9.39.1(jiti@2.6.1) @@ -810,12 +892,6 @@ importers: prettier: specifier: 3.7.3 version: 3.7.3 - react: - specifier: 19.2.3 - version: 19.2.3 - react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -5543,6 +5619,20 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -6789,6 +6879,26 @@ packages: moo-color@1.0.3: resolution: {integrity: sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==} + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.38.0: + resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -14343,6 +14453,15 @@ snapshots: forwarded@0.2.0: {} + framer-motion@12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + fresh@2.0.0: {} fs-extra@10.1.0: @@ -16165,6 +16284,20 @@ snapshots: dependencies: color-name: 1.1.4 + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + framer-motion: 12.38.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + mri@1.2.0: {} mrmime@2.0.1: {} diff --git a/practices/demo/rstest.setup.ts b/practices/demo/rstest.setup.ts deleted file mode 100644 index 32b38ae281..0000000000 --- a/practices/demo/rstest.setup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { expect } from '@rstest/core'; -import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; - -expect.extend(jestDomMatchers); diff --git a/practices/demo/src/App/app.css b/practices/demo/src/App/app.css deleted file mode 100644 index 145c2744ab..0000000000 --- a/practices/demo/src/App/app.css +++ /dev/null @@ -1,20 +0,0 @@ -.demo-app-root { - width: 100%; - height: 100%; - min-width: 0; - min-height: 0; - overflow: hidden; -} - -.demo-app-root:fullscreen { - width: 100vw; - height: 100vh; -} - -.demo-app-workbench { - width: 100%; - height: 100%; - min-width: 0; - min-height: 0; - overflow: hidden; -} diff --git a/practices/demo/src/hooks/useVBIStore.ts b/practices/demo/src/hooks/useVBIStore.ts deleted file mode 100644 index 192cc7cb7f..0000000000 --- a/practices/demo/src/hooks/useVBIStore.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useVBIStore } from 'src/model'; -import type { BearState } from 'src/model/VBIStore'; - -/** - * VBI Store Hook - * 提供全局状态管理 - */ -export const useVBIStoreHook = (): BearState => { - return useVBIStore() as BearState; -}; diff --git a/practices/demo/src/model/VBIStore.ts b/practices/demo/src/model/VBIStore.ts deleted file mode 100644 index 2a973eff8b..0000000000 --- a/practices/demo/src/model/VBIStore.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { VBIChartBuilder, VBIChartDSL } from '@visactor/vbi'; -import { VSeed } from '@visactor/vseed'; -import { defaultBuilder } from 'src/utils/demoConnector'; -import { create } from 'zustand'; - -type DestroyCallback = () => void; - -export interface BearState { - loading: boolean; - vseed: VSeed | null; - builder: VBIChartBuilder; - initialized: boolean; - - dsl: VBIChartDSL; - - initialize: (builder?: VBIChartBuilder) => DestroyCallback; - bindEvent: () => DestroyCallback; - logState: () => Promise; - - setDsl: (dsl: VBIChartDSL) => void; - setLoading: (loading: boolean) => void; - setVSeed: (vseed: VSeed | null) => void; -} - -export const useVBIStore = create((set, get) => ({ - loading: false, - vseed: null, - initialized: false, - builder: defaultBuilder, - dsl: defaultBuilder.dsl.toJSON() as VBIChartDSL, - - setLoading: (loading: boolean) => set({ loading }), - setVSeed: (vseed: VSeed | null) => set({ vseed }), - setDsl: (dsl: VBIChartDSL) => set({ dsl }), - logState: async () => { - const { builder, vseed } = get(); - - console.group('selected builder'); - - console.info('builder', builder); - console.info('vbi', builder.build()); - console.info('vquery', builder.buildVQuery()); - console.info('vseed', vseed); - - console.groupEnd(); - }, - - // 初始化 - initialize: (builder?: VBIChartBuilder) => { - if (builder) { - set({ builder }); - } - set({ initialized: true }); - - const callback = get().bindEvent(); - - return () => { - callback(); - set({ loading: false, vseed: null, initialized: false }); - }; - }, - - bindEvent: () => { - const { builder, setLoading, setVSeed, setDsl } = get(); - - const updateAll = async () => { - if (builder.isEmpty()) { - setLoading(false); - setVSeed(null); - return; - } - - setLoading(true); - try { - const newVSeed = await builder.buildVSeed(); - setVSeed(newVSeed); - setDsl(builder.dsl.toJSON() as VBIChartDSL); - } catch (e: any) { - console.error('VSeed Build Error:', e); - // 静默处理错误,不显示消息 - } finally { - setLoading(false); - } - }; - - builder.doc.on('update', updateAll); - return () => { - builder.doc.off('update', updateAll); - }; - }, -})); diff --git a/practices/demo/src/model/index.ts b/practices/demo/src/model/index.ts deleted file mode 100644 index 31d0c7f0c0..0000000000 --- a/practices/demo/src/model/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useVBIStore } from './VBIStore'; diff --git a/practices/demo/tests/index.test.tsx b/practices/demo/tests/index.test.tsx deleted file mode 100644 index dbf14352bf..0000000000 --- a/practices/demo/tests/index.test.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { expect, test } from '@rstest/core'; - -test('initial test', async () => { - expect(1).toBe(1); -}); diff --git a/practices/standard-report/.gitignore b/practices/standard-report/.gitignore new file mode 100644 index 0000000000..da7920ab50 --- /dev/null +++ b/practices/standard-report/.gitignore @@ -0,0 +1,15 @@ +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ +tsconfig.tsbuildinfo +storybook-static + +# IDE +.vscode/* +!.vscode/extensions.json +.idea diff --git a/practices/demo/.prettierignore b/practices/standard-report/.prettierignore similarity index 100% rename from practices/demo/.prettierignore rename to practices/standard-report/.prettierignore diff --git a/practices/demo/.prettierrc b/practices/standard-report/.prettierrc similarity index 100% rename from practices/demo/.prettierrc rename to practices/standard-report/.prettierrc diff --git a/practices/standard-report/AGENTS.md b/practices/standard-report/AGENTS.md new file mode 100644 index 0000000000..2426d3eb77 --- /dev/null +++ b/practices/standard-report/AGENTS.md @@ -0,0 +1,28 @@ +# AGENTS.md + +You are an expert in JavaScript, Rspack, Rsbuild, Rslib, and practice-level frontend development. You write maintainable, performant, and accessible code. + +## Commands + +- `pnpm run build` - Build the library for production +- `pnpm run dev` - Turn on watch mode, watch for changes and rebuild the library + +## Docs + +- Rslib: https://rslib.rs/llms.txt +- Rsbuild: https://rsbuild.rs/llms.txt +- Rspack: https://rspack.rs/llms.txt + +## Tools + +### Rstest + +- Run `pnpm run test` to test your code + +### ESLint + +- Run `pnpm run lint` to lint your code + +### Prettier + +- Run `pnpm run format` to format your code diff --git a/practices/standard-report/README.md b/practices/standard-report/README.md new file mode 100644 index 0000000000..4c6bbe491a --- /dev/null +++ b/practices/standard-report/README.md @@ -0,0 +1,23 @@ +# standard-report + +## Setup + +Install the dependencies: + +```bash +pnpm install +``` + +## Get started + +Build the practice: + +```bash +pnpm run build +``` + +Build the practice in watch mode: + +```bash +pnpm run dev +``` diff --git a/practices/standard-report/docs/2026-03-24-standard-report/adr.md b/practices/standard-report/docs/2026-03-24-standard-report/adr.md new file mode 100644 index 0000000000..3903000d86 --- /dev/null +++ b/practices/standard-report/docs/2026-03-24-standard-report/adr.md @@ -0,0 +1,182 @@ +# ADR-002: practices/standard-report 报表分页与全屏编辑方案 + +## Status + +Proposed + +## Context + +`@visactor/vbi` 现在已经提供 `createReport(...)`、`VBIReportBuilder`、`reportBuilder.page.add/remove/update/get` 等能力,`practices/standard` 也已经有一套稳定的单图表可视化查询编辑器骨架。 + +这次目标不是继续在 `practices/standard` 里叠加报表能力,而是新建一个独立实践 `practices/standard-report`,用于验证以下组合场景: + +1. 报表默认存在 1 个 page。 +2. page 支持新增、删除、切换。 +3. 每个 page 只承载 1 个图表。 +4. 点击编辑后,进入全屏可视化查询编辑态。 +5. page 切换必须是横向翻页,具有幻灯片式转场。 + +现有 `practices/standard` 的核心工作台由 `FieldsPanel + ShelfPanel + ChartPanel + Toolbar` 组成,已经具备“编辑单个 `VBIChartBuilder`”的完整能力,因此标准报表实践的关键不是重写编辑器,而是决定: + +1. 报表页容器如何组织。 +2. page 管理与幻灯片切换如何分工。 +3. 全屏编辑是独立路由、独立草稿,还是直接绑定当前 page。 + +## Decision + +### 1. 新增独立实践 `practices/standard-report`,以 `practices/standard` 为代码基线 + +实现方式: + +1. 复制当前 `practices/standard` 为新的 `practices/standard-report`。 +2. `practices/standard` 保持单图表 practice 定位,不直接混入报表工作流。 +3. `standard-report` 内优先复用 `standard` 已有的编辑器组件、hook、store 和渲染面板。 +4. 新实践只补报表壳层、page 管理层和 page 内 chart 绑定层。 + +原因: + +1. 单图表 practice 和报表 practice 是两个不同交互模型,不应该在同一个 demo 里混合演进。 +2. 独立 practice 更适合验证 `VBI.createReport(...)` 的完整闭环。 +3. `standard` 已经是稳定编辑器基线,复制再裁剪的风险低于在原工程中条件分支扩展。 + +### 2. `VBIReportBuilder` 是唯一状态源,UI 只保存 `activePageId` 和编辑态 + +状态规则: + +1. 根状态使用 `VBI.createReport(...)` 创建的 `VBIReportBuilder`。 +2. UI 层只额外维护 `activePageId`、`editorOpen` 等视图状态。 +3. page 列表、page 标题、page 图表内容全部从 `reportBuilder.build().pages` 或 `reportBuilder.page.get(...)` 派生。 +4. page 切换以 `page.id` 为主键,不以数组 index 作为长期状态主键。 + +初始化规则: + +1. 报表初始化时如果没有 page,则立即创建 `Page 1`。 +2. 默认第一页直接作为当前激活页。 +3. 新增 page 时自动切到新 page。 +4. 删除当前 page 时,激活相邻 page;如果删到最后一个,则立即补回一个空 page。 + +原因: + +1. `page.id` 在增删场景下比 index 更稳定。 +2. 默认总有 1 个 page,可以避免空壳报表带来的大量边界分支。 +3. 直接围绕 `VBIReportBuilder` 建模,符合 Single Source of Truth。 + +### 3. 顶部 page 管理使用 `Tabs type="editable-card"`,内容区切换使用 `Carousel` + +组件分工: + +1. 顶部 page bar 使用 Ant Design `Tabs` 的 `editable-card` 模式。 +2. `Tabs` 负责 page 标题展示、激活态、增加、删除。 +3. 内容区使用 Ant Design `Carousel` 展示每个 page 的内容。 +4. `Tabs.onChange` 驱动 `Carousel` 切换;`Carousel.afterChange` 反向同步当前 page。 + +交互约束: + +1. `Tabs` 只承担 page 管理,不承担主内容渲染。 +2. `Carousel` 关闭自动播放,使用横向滚动切页。 +3. `Carousel` 开启 arrows,关闭 infinite。 +4. page 新增、点击、箭头切换都必须落到同一个 `activePageId`。 + +原因: + +1. Ant Design 文档明确 `Tabs` 的 card / editable-card 适合“管理可关闭视图”。 +2. Ant Design `Carousel` 天然提供横向切换与转场动画,更符合“像幻灯片一般”的要求。 +3. 单用 `Tabs` 无法很好表达幻灯片式切换;单用 `Carousel` 又缺少清晰的 add/remove 管理入口。 + +### 4. page 编辑使用全屏 `Drawer`,不切新路由,不开独立页面 + +组件决策: + +1. 点击 page 上的 `编辑` 按钮后,打开 `Drawer`。 +2. `Drawer` 使用全屏尺寸,作为 page 的沉浸式编辑态。 +3. `Drawer` 内复用 `practices/standard` 的现有工作台骨架。 +4. 关闭 `Drawer` 后返回报表视图,不丢失当前 page 上下文。 + +原因: + +1. Ant Design 文档明确 `Drawer` 适合“在不离开当前页面上下文的情况下处理较重子任务”。 +2. 报表编辑本质上是“编辑当前 page 的图表”这个子任务,不是跳转到另一个业务页面。 +3. 相比 `Modal`,`Drawer` 更适合承载完整字段面板、shelves、图表预览这类重编辑界面。 + +### 5. 全屏编辑直接绑定当前 `page.chart`,不维护第二份 chart draft + +实现约束: + +1. 打开编辑器时,通过当前 page 的 y-map 创建该 page 专属的 `VBIChartBuilder`。 +2. 编辑器内所有操作直接写入当前 `page.chart`。 +3. 关闭编辑器不需要额外“提交”步骤。 +4. 报表视图中的 page 预览天然和编辑结果保持一致。 + +明确不做: + +1. 不在 `standard-report` 里维护 `draftChartBuilder`。 +2. 不引入“打开编辑器时复制,点击保存再覆盖”的双写模型。 +3. 不新增 page 级 undo/commit 同步层。 + +原因: + +1. 当前实践目标是验证 report DSL 与 chart editor 的绑定链路,不是设计复杂草稿系统。 +2. 直接绑定 page.chart 可以保持唯一状态源。 +3. 双 builder 模型会引入额外的复制、覆盖、取消、冲突合并问题,超出本次范围。 + +### 6. 每个 page 只展示一个图表卡片,报表视图负责浏览,不负责完整编辑 + +页面布局: + +1. 每个 page 在报表视图中展示 `title + chart preview + edit action`。 +2. 报表视图只保留轻量浏览和翻页动作。 +3. 字段拖拽、shelves 配置、filter 编辑等重交互全部放到全屏编辑器中完成。 +4. page 内不再并排出现完整编辑器和完整报表浏览器。 + +原因: + +1. 目标已经明确“每个 page 内一个图表,点击编辑按钮全屏编辑”。 +2. 浏览态和编辑态分离后,报表体验更接近幻灯片。 +3. 这能最大化复用 `standard` 工作台,而不是在 report 页里再塞一套缩小版编辑器。 + +### 7. 本次标准报表实践只覆盖分页报表,不扩展多图布局和自由排版 + +范围约束: + +1. 一个 page 只允许一个 chart。 +2. 不支持 page 内多个 chart block。 +3. 不支持文本、注释、自由布局、拖拽排版。 +4. 不处理 report 导出、演示播放控制、主题模板。 + +原因: + +1. 当前目标是先验证 `createReport` 的最小可用报表交互。 +2. 多图布局会把问题从“分页报表”升级为“版面系统”。 +3. 先把“单页单图 + 翻页 + 全屏编辑”跑通,后续再扩展更合理。 + +### 8. 验证范围 + +必须覆盖以下验证: + +1. `practices/standard-report` 能独立启动和构建。 +2. 默认初始化后总是存在 1 个 page。 +3. `page.add()`、`page.remove()` 能正确驱动 `Tabs` 与 `Carousel` 同步更新。 +4. page 切换具有横向转场,不是瞬时替换。 +5. 点击 `编辑` 会打开全屏 `Drawer`。 +6. 编辑器内修改 chart 后,关闭 `Drawer` 返回报表视图能看到更新结果。 +7. 删除当前 page 后,激活页会自动落到合法 page。 +8. 删除到最后一页时,会自动补回空白第一页。 + +## Reference + +- `practices/standard/src/App/App.tsx` +- `@visactor/vbi` report builder API +- Ant Design Tabs: https://ant.design/llms-full.txt +- Ant Design Carousel: https://ant.design/llms-full.txt +- Ant Design Drawer: https://ant.design/llms-full.txt + +## 淘汰内容概述 + +本方案明确不采用以下做法: + +- 不在 `practices/standard` 原工程里直接混入 report 模式 +- 不使用 `Tabs` 单独承担 page 切换主动画 +- 不使用 `Carousel` 单独承担 page 的新增和删除管理 +- 不通过新路由跳走到单独 chart editor 页面 +- 不为 page.chart 维护第二份独立 draft builder +- 不在本次实践中扩展多图布局、富文本和自由排版 diff --git a/practices/demo/docs/todo2-standard-report/goal.md b/practices/standard-report/docs/2026-03-24-standard-report/goal.md similarity index 82% rename from practices/demo/docs/todo2-standard-report/goal.md rename to practices/standard-report/docs/2026-03-24-standard-report/goal.md index 81b43776c7..b43edbefe2 100644 --- a/practices/demo/docs/todo2-standard-report/goal.md +++ b/practices/standard-report/docs/2026-03-24-standard-report/goal.md @@ -1,12 +1,6 @@ ------- -status: in-progress ---- - ---- - 生成 architecture-decisions 文档, 命名为 ./adr.md -这里是 practices/demo 计划要完成的开发任务: +这里是 practices/standard 计划要完成的开发任务: [] 拷贝demo项目为 standard-report 项目实践进行改造 [] VBI 现已支持createReport 能力. [] 默认有一个page, 支持增加、删除page, [] 每个page内一个图表, 点击编辑按钮, 即可全屏编辑可视化查询. 支持横向翻页, 像幻灯片一般, 有转场动画.(standard-report) diff --git a/practices/standard-report/docs/2026-03-24-standard-report/plan.md b/practices/standard-report/docs/2026-03-24-standard-report/plan.md new file mode 100644 index 0000000000..e563873c2a --- /dev/null +++ b/practices/standard-report/docs/2026-03-24-standard-report/plan.md @@ -0,0 +1,133 @@ +# Plan: practices/standard-report 报表分页与全屏编辑落地 + +## Phase 1: 建立 `practices/standard-report` 工程骨架 + +目标:从 `practices/standard` 复制出独立 practice,并完成 workspace 接入。 + +任务: + +1. 复制 `practices/standard` 为 `practices/standard-report` +2. 修改新 practice 的 `package.json` 包名、README、docs README +3. 更新根工作区引用、`pnpm-lock.yaml`、需要消费该 practice 的 app 依赖和 tsconfig references +4. 保证 `practices/standard` 与 `practices/standard-report` 可并存,不互相覆盖 + +交付物: + +- 新目录 `practices/standard-report` +- 新 workspace 包名与依赖链路可解析 + +## Phase 2: 引入 report 根状态与 page 生命周期 + +目标:把单图表 builder 初始化切换为 report builder 初始化。 + +任务: + +1. 新增 `standard-report` 专用 store 或在现有 store 上拆出 report 版本 +2. 根状态改为 `VBI.createReport(...)` +3. 实现默认 1 个 page 的初始化逻辑 +4. 增加 `activePageId`、`editorOpen` 等视图状态 +5. 封装 `addPage`、`removePage`、`setActivePage`、`ensureAtLeastOnePage` 等动作 + +约束: + +1. `VBIReportBuilder` 是唯一状态源 +2. page 主键必须使用 `page.id` +3. 删除最后一页时必须自动补回空白第一页 + +交付物: + +- report store / hooks +- page 生命周期动作 + +## Phase 3: 搭建报表浏览视图 + +目标:让 practice 从“单图表编辑器”变成“报表分页浏览器”。 + +任务: + +1. 顶部实现 `Tabs type="editable-card"` page 管理栏 +2. 内容区接入 `Carousel`,按 page 渲染横向滑动卡片 +3. 每个 page 卡片展示标题、图表预览、编辑按钮 +4. 实现 `Tabs` 与 `Carousel` 的双向同步 +5. 处理新增 page 后自动激活、删除 page 后自动回落到相邻 page + +约束: + +1. 报表视图只负责浏览,不嵌入完整编辑器 +2. 每个 page 只显示 1 个 chart preview +3. page 切换必须保留横向转场动画 + +交付物: + +- report page tabs +- report carousel view +- page preview card + +## Phase 4: 复用 `standard` 工作台实现全屏编辑 + +目标:点击 page 编辑后,进入全屏图表编辑态。 + +任务: + +1. 抽出 `standard` 当前工作台骨架,形成可接收外部 chart builder 的复用入口 +2. 在 `standard-report` 中接入全屏 `Drawer` +3. 打开编辑器时,绑定当前 `page.chart` 对应的 `VBIChartBuilder` +4. 关闭编辑器后返回报表浏览态 +5. 确保编辑结果实时反映到 page preview + +约束: + +1. 不新建第二份 draft chart builder +2. 不新增保存/提交按钮作为唯一写入入口 +3. 所有编辑直接落到当前 page.chart + +交付物: + +- 可复用的 chart workbench +- report drawer editor + +## Phase 5: 处理标题、空态与边界交互 + +目标:补齐可用性和关键边界条件。 + +任务: + +1. page 默认命名为 `Page 1`、`Page 2` 等 +2. 支持 page 标题重命名 +3. 新 page 默认生成空 chart,并可直接进入编辑 +4. 无有效 preview 时展示空态卡片 +5. 删除当前 page、删除非当前 page、连续新增 page 的交互保持稳定 + +交付物: + +- page title 策略 +- empty state +- page edge-case handling + +## Phase 6: 测试与验证 + +目标:确保新 practice 和报表交互闭环可运行。 + +任务: + +1. 为 report store/page actions 增加测试 +2. 为默认 1 page、add/remove、active page fallback 增加测试 +3. 为 `Tabs + Carousel` 同步行为增加测试 +4. 为全屏 `Drawer` 打开/关闭与编辑结果回写增加测试 +5. 运行 `standard-report` 的 `typecheck`、`lint`、`test` +6. 如网站或其他 app 需要接入,再补对应引用验证 + +建议验证顺序: + +1. `pnpm --filter=standard-report run typecheck` +2. `pnpm --filter=standard-report run lint` +3. `pnpm --filter=standard-report run test` +4. 必要时执行一次根目录 `pnpm run typecheck` + +## 里程碑 + +1. M1: `standard-report` 工程可独立安装、构建、启动 +2. M2: 默认 1 page,支持 add/remove/switch +3. M3: page 浏览视图具备横向幻灯片切换 +4. M4: 点击编辑后进入全屏 `Drawer`,并直接编辑当前 page.chart +5. M5: 测试与类型检查通过 diff --git a/practices/standard-report/docs/2026-03-24-view-edit-chart/adr.md b/practices/standard-report/docs/2026-03-24-view-edit-chart/adr.md new file mode 100644 index 0000000000..16a18f5091 --- /dev/null +++ b/practices/standard-report/docs/2026-03-24-view-edit-chart/adr.md @@ -0,0 +1,174 @@ +# ADR-003: practices/standard-report 图表 view/edit 复用 standard 与全屏过渡 + +## Status + +Proposed + +## Context + +`practices/standard-report` 当前已经跑通“报表分页 + 单页单图 + 点击编辑”的基础链路,但图表预览和图表编辑仍然走了两条不同实现: + +1. 报表页内预览走 `usePagePreview(...)` + `pageBuilder.chart.buildVSeed()` + `VSeedRender`。 +2. 点击编辑后,`ReportEditorDrawer` 内直接挂载 `StandardAPP builder={pageBuilder.chart}`。 + +这会带来 4 个直接问题: + +1. `standard-report` 实际维护了一套自己的图表预览实现,和 `practices/standard` 的图表工作台已经开始分叉。 +2. 以后只要 `standard` 的图表渲染壳层、空态、主题或交互细节发生变化,`standard-report` 都需要再补一遍。 +3. 当前编辑器通过 `Drawer width="100vw"` 打开,本质上仍是“侧边抽屉滑入”,不是“从当前图表卡片放大到全屏”,上下文连续性不够好。 +4. `practices/standard` 目前使用模块级 zustand store 单例,天然更偏“单个 demo app”,不适合被 `standard-report` 同时作为 preview surface 和 editor surface 复用。 + +本次目标不是在 `standard-report` 里继续补一套 preview/editor 壳子,而是把 `practices/standard` 收敛为唯一图表体验来源,让: + +1. `standard-report` 内图表预览使用 `practices/standard`。 +2. `standard-report` 内图表编辑也使用 `practices/standard`。 +3. `practices/standard` 只新增 `view mode` 和 `edit mode`,不再让 `standard-report` 自己拼装另一套图表界面。 +4. 点击编辑后,图表卡片能够以平滑动画放大到视口级全屏编辑态。 + +## Decision + +### 1. `practices/standard` 从“单 demo app”升级为“可嵌入的 mode-driven chart practice” + +接口约束: + +1. `APP` 新增 `mode?: 'view' | 'edit'`,默认值保持为 `edit`。 +2. `edit mode` 保持当前工作台定位,继续承载 `Toolbar + FieldsPanel + ShelfPanel + ChartPanel`。 +3. `view mode` 使用同一个 builder 初始化流程、同一套主题/语言配置和同一条 `buildVSeed()` 渲染链路,但只暴露只读图表展示壳层。 +4. `view mode` 不提供拖拽字段、切图表类型、撤销重做、筛选编辑等重交互入口。 +5. `practices/standard` 仍然是图表体验的唯一来源;`standard-report` 不再直接决定图表区内部结构。 + +原因: + +1. 需求已经明确“图表预览和编辑都是用 `practices/standard` 来实现”。 +2. preview 和 edit 的差异本质上是“同一图表体验的两种模式”,不是两个独立产品。 +3. 让 mode 收敛在 `standard` 内部,比让 `standard-report` 继续拼装一套 preview 壳层更符合 Single Source of Truth。 + +### 2. `practices/standard` 的状态管理改为实例级,而不是模块级单例 + +实现约束: + +1. 当前 `useVBIStore = create(...)` 的模块级单例要重构为 store factory + provider,或等价的实例级状态容器。 +2. 每次挂载 `StandardAPP` 都应拥有独立的 `loading / initialized / vseed` 等派生状态。 +3. 多个 `StandardAPP` 可以同时存在,互不覆盖 UI 状态和事件绑定。 +4. `VBIChartBuilder` 仍然是 chart DSL 的唯一事实来源;实例级 store 只负责派生 UI 状态,不复制 DSL。 + +原因: + +1. 报表页内 preview surface、放大全屏过程中的 transition surface、最终 editor surface 可能在一个交互过程中短暂共存。 +2. 如果继续沿用单例 store,多个 `StandardAPP` 会互相覆盖 `builder / vseed / initialized`,无法稳定复用。 +3. 只有先把 `standard` 变成真正可复用实例,`view mode` 和 `edit mode` 才是可靠架构,而不是表面 props 分支。 + +### 3. `standard-report` 的图表预览改为直接挂载 `standard` 的 `view mode` + +边界划分: + +1. `standard-report` 继续负责 page 壳层、翻页、页级 action、洞察文本等 report 语义。 +2. page 内“图表区域”统一改为 `StandardAPP mode="view" builder={pageBuilder.chart}` 或等价复用入口。 +3. `standard-report` 不再直接调用 `pageBuilder.chart.buildVSeed()` 去生成图表预览。 +4. `usePagePreview.ts`、`PagePreviewCanvas.tsx` 这类“report 自己拼的图表预览链路”应退出主路径。 + +原因: + +1. preview 既然要复用 `standard`,就不能继续让 `standard-report` 保留第二条图表渲染实现。 +2. 这能保证 view/edit 两态使用同一条图表构建链路、同一套空态和同一套主题逻辑。 +3. `standard-report` 应该聚焦 report 容器和转场,而不是再拥有一套 chart preview 子系统。 + +### 4. `standard-report` 的编辑态继续由 report 壳层调度,但内容统一来自 `standard` 的 `edit mode` + +实现约束: + +1. page 上的“编辑图表”动作仍由 `standard-report` 发起,因为当前上下文属于 report page。 +2. 全屏编辑区内部统一挂载 `StandardAPP mode="edit" builder={pageBuilder.chart}`。 +3. `standard-report` 只管理打开、关闭、当前 page、过渡动画与返回行为,不再负责图表编辑 UI 本身。 +4. `practices/standard` 原本的浏览器 Fullscreen API 能力保留给 standalone 使用;嵌入到 report 全屏层时,不再把它作为主交互模型。 + +原因: + +1. “我正在编辑哪一个 report page 的 chart” 是 report 语义;“如何编辑 chart” 是 standard 语义。 +2. 这样职责边界清晰,后续 chart editor 的演进只需要改 `practices/standard`。 +3. report 全屏态和浏览器 Fullscreen API 不是一回事,不应混为同一个交互概念。 + +### 5. 图表放大全屏采用 report 侧自定义 transition layer,不继续依赖 `Drawer` 作为主过渡 + +过渡方案: + +1. 点击编辑时,先记录当前图表预览容器的 `DOMRect`、圆角和阴影参数。 +2. `standard-report` 在 portal 中创建固定定位的 transition layer。 +3. transition layer 初始尺寸与被点击图表卡片一致,随后通过 CSS transform / size / radius 过渡扩展到 `100vw x 100vh`。 +4. 进入动画结束后,再切换到稳定的 `edit mode` 布局;关闭时反向执行同一路径。 +5. 如果拿不到源节点尺寸,则退化为居中淡入/缩放,不阻塞主流程。 + +明确不做: + +1. 不再把 `Drawer width="100vw"` 视为“放大到全屏”的实现方式。 +2. 不把浏览器 `requestFullscreen()` 作为 report 编辑态主入口。 +3. 不把浏览器专属 View Transition API 设为硬依赖。 + +原因: + +1. Ant Design `Drawer` 的默认语义是从边缘滑入,更适合上下文内子任务,不适合“从当前卡片放大到全屏”的共享元素过渡。 +2. 目标是提升从 report preview 到 chart edit 的空间连续性,过渡起点必须是当前图表卡片,而不是视口边缘。 +3. 采用 React + CSS 可控过渡层更稳妥,也更容易做关闭时的反向收拢动画。 + +### 6. `view mode` 与 `edit mode` 的切换只改变表现层,不改变 builder 归属 + +状态规则: + +1. preview、transition、editor 三个阶段始终绑定同一个 `pageBuilder.chart`。 +2. 不创建第二份 `draftChartBuilder`。 +3. 不新增“进入编辑复制一份,点击保存再覆盖”的提交模型。 +4. 关闭编辑器后,报表页看到的内容就是当前 builder 的最新结果。 + +原因: + +1. `page.chart` 已经是 page 的唯一图表配置源。 +2. 这次目标是统一 view/edit 体验,不是扩展 save/cancel 工作流。 +3. 如果为了动画再复制一份 chart draft,会把问题从“模式切换”升级成“双状态同步”,明显超出本次范围。 + +### 7. 对外兼容策略:`standard` 保持默认 `edit` 行为,避免破坏现有单图 practice + +兼容约束: + +1. `practices/standard` 独立运行时,`APP` 不传 `mode` 仍按当前完整编辑器表现。 +2. `view mode` 是新增能力,不改变 `standard` 作为单图表 practice 的默认体验。 +3. `standard-report` 是第一个消费 `view mode` 的外部场景,但不是通过 import 内部组件拼装,而是通过 `standard` 暴露的稳定入口接入。 + +原因: + +1. 这次改动目标是增强复用,不是重写 `practices/standard` 的产品定位。 +2. 先保持默认行为兼容,能降低 practice 演进风险。 +3. 让 `standard-report` 通过稳定接口复用 `standard`,后续更容易演进和测试。 + +### 8. 验证范围 + +必须覆盖以下验证: + +1. `practices/standard` 在 `mode="edit"` 下保持现有单图工作台能力。 +2. `practices/standard` 在 `mode="view"` 下可以独立基于 `builder` 渲染只读图表。 +3. `practices/standard` 多实例同时挂载时,不会互相覆盖 store 状态。 +4. `standard-report` 预览路径中不再直接调用 `pageBuilder.chart.buildVSeed()`。 +5. `standard-report` 点击编辑后,图表从当前卡片位置放大到全屏,而不是从侧边滑入。 +6. 关闭编辑器时,图表能反向收拢回当前 page 的图表区域。 +7. 编辑器内修改 chart 后,回到报表页看到的是同一 builder 的最新结果。 +8. page 切换、新增、删除后,当前 page 的 preview / edit 仍能正确绑定对应 `page.chart`。 + +## Reference + +- `practices/standard/src/App/App.tsx` +- `practices/standard/src/model/VBIStore.ts` +- `practices/standard-report/src/App/components/editor/ReportEditorDrawer.tsx` +- `practices/standard-report/src/App/components/page/PagePreviewCard.tsx` +- `practices/standard-report/src/App/components/page/PagePreviewCanvas.tsx` +- `practices/standard-report/src/App/hooks/usePagePreview.ts` +- Ant Design Modal / Drawer / Portal-related component docs: https://ant.design/llms-full.txt + +## 淘汰内容概述 + +本方案明确不采用以下做法: + +- 不继续让 `standard-report` 自己维护独立的 chart preview 渲染链路 +- 不在 `standard-report` 内部直接拼装 `standard` 的字段面板、shelf、图表组件 +- 不继续把 `Drawer width="100vw"` 当作“图表卡片放大全屏”的实现 +- 不为 preview 和 edit 维护两份 chart builder 或 draft +- 不把浏览器 `requestFullscreen()` 当作 report 编辑态的主入口 +- 不把浏览器 View Transition API 设为唯一依赖 diff --git a/practices/standard-report/docs/2026-03-24-view-edit-chart/goal.md b/practices/standard-report/docs/2026-03-24-view-edit-chart/goal.md new file mode 100644 index 0000000000..1c1edf649d --- /dev/null +++ b/practices/standard-report/docs/2026-03-24-view-edit-chart/goal.md @@ -0,0 +1,6 @@ +生成 architecture-decisions 文档, 命名为 ./adr.md + +1. practices/standard-report的图表预览和编辑都是用practices/standard来实现 +2. 只需为practices/standard增加view mode 和edit mode +3. practices/standard-report内点击编辑图表后,让其可以拥有动画放大到全屏幕,带来更加丝滑的编辑体验。 +4. 附录: AntDesign V6 组件库使用说明: https://ant.design/llms-full.txt diff --git a/practices/standard-report/docs/2026-03-24-view-edit-chart/plan.md b/practices/standard-report/docs/2026-03-24-view-edit-chart/plan.md new file mode 100644 index 0000000000..64b0210ff9 --- /dev/null +++ b/practices/standard-report/docs/2026-03-24-view-edit-chart/plan.md @@ -0,0 +1,235 @@ +# Plan: practices/standard-report 图表 view/edit 复用与全屏过渡落地 + +> 基于 ADR: `./adr.md` +> 开发顺序遵循仓库约定: 先补测试,再改实现,再做验证 + +## 范围 + +本计划覆盖两部分协同改造: + +1. `practices/standard` 从单一编辑 demo 收敛为可复用的 chart practice,新增 `view mode` / `edit mode`,并把状态管理改成实例级。 +2. `practices/standard-report` 停止维护独立 chart preview 链路,改为复用 `standard`,同时把当前 `Drawer` 侧滑编辑改成“从图表卡片放大全屏”的平滑过渡。 + +本计划不覆盖: + +1. page.chart draft / save / cancel 双状态模型 +2. report 多图布局、自由排版、富文本扩展 +3. 浏览器 Fullscreen API 方案重设计 +4. `@visactor/vbi` 的 report/chart DSL 变更 + +## Phase 1: 先锁定 `standard` 的复用目标与回归测试 + +目标:先把 `standard` 作为被复用方的对外行为锁定,避免后面重构 store 和 mode 时丢失现有能力。 + +### 1.1 `standard` 默认编辑态回归测试 + +**测试文件**: + +- `practices/standard/tests/index.test.tsx` +- 或拆出 `practices/standard/tests/app-modes.test.tsx`(新增) + +测试内容: + +1. `APP` 不传 `mode` 时仍渲染完整编辑工作台。 +2. 现有 toolbar、fields、shelves、chart panel 仍可见。 +3. 传入外部 `builder` 时仍能正常初始化。 + +### 1.2 `view mode` 行为测试 + +**测试文件**: + +- `practices/standard/tests/app-modes.test.tsx`(新增) + +测试内容: + +1. `mode="view"` 时只渲染只读图表壳层,不暴露编辑工具栏和字段面板。 +2. 空 chart 在 `view mode` 下能展示稳定空态。 +3. 非空 chart 在 `view mode` 下能完成 `buildVSeed()` 并渲染。 + +### 1.3 多实例隔离测试 + +**测试文件**: + +- `practices/standard/tests/app-store-isolation.test.tsx`(新增) + +测试内容: + +1. 同时挂载两个 `StandardAPP`,各自持有不同 `builder`。 +2. 一个实例更新后,不会覆盖另一个实例的 `loading / vseed / initialized`。 +3. 卸载一个实例时,不会解绑另一个实例的 builder 监听。 + +## Phase 2: 把 `standard` 的 store 改成实例级 + +目标:把当前模块级 zustand 单例改造成可复用实例,解决 `standard-report` 里 preview/editor 复用的根障碍。 + +**重点文件**: + +- `practices/standard/src/model/VBIStore.ts` +- `practices/standard/src/model/index.ts` +- `practices/standard/src/hooks/useVBIStore.ts` +- `practices/standard/src/App/App.tsx` + +任务: + +1. 抽出 `createVBIStore(...)` 或等价 store factory。 +2. 为 `standard` 增加 provider 层,保证每次挂载 `APP` 都创建独立 store 实例。 +3. 把现有 `useVBIStore(...)` / 相关 hooks 切到实例上下文读取,而不是直接读模块单例。 +4. 保持 `builder` 仍然是唯一 DSL 来源,store 只保存 UI 派生状态。 +5. 确保 `initialize()` / `bindEvent()` 生命周期在实例级下仍然可正确清理。 + +约束: + +1. 不为了实例化重构去复制 chart DSL。 +2. 不破坏 `standard` 作为独立 practice 的默认入口。 +3. 控制文件和函数体积,必要时拆小文件。 + +交付物: + +- 实例级 store factory +- `standard` provider / hooks 调整 +- 多实例可稳定共存 + +## Phase 3: 给 `standard` 增加 `view mode` / `edit mode` + +目标:让 `standard` 自己拥有 preview 和 editor 两种表现层,成为 report 侧唯一 chart 体验来源。 + +**重点文件**: + +- `practices/standard/src/App/App.tsx` +- `practices/standard/src/App/components/ChartPanel.tsx` +- `practices/standard/src/index.tsx` +- 视情况新增 `practices/standard/src/App/components/ViewPanel.tsx` + +任务: + +1. `APP` 新增 `mode?: 'view' | 'edit'`,默认保持 `edit`。 +2. 抽出 `edit mode` 的工作台骨架,保持当前 `Toolbar + FieldsPanel + ShelfPanel + ChartPanel`。 +3. 新增 `view mode` 图表壳层,复用同一套 `buildVSeed()` 渲染链路与主题/locale 初始化。 +4. `view mode` 补齐空态、加载态、只读图表容器样式。 +5. 保证 `standard` 对外仍通过稳定入口消费,不要求 `standard-report` import 内部子组件。 + +约束: + +1. `view mode` 不暴露字段拖拽、筛选编辑、切图表类型等交互。 +2. `edit mode` 继续保持现有单图编辑能力。 +3. 尽量复用现有渲染与配置逻辑,避免出现第二套 build/render 管线。 + +交付物: + +- `standard` mode API +- 只读图表展示面 +- 默认编辑态兼容 + +## Phase 4: 用 `standard view mode` 替换 `standard-report` 当前 preview 链路 + +目标:让 `standard-report` 不再自己 build preview,而是只负责 report page 壳层。 + +**重点文件**: + +- `practices/standard-report/src/App/components/page/PagePreviewCard.tsx` +- `practices/standard-report/src/App/components/page/PagePreviewCanvas.tsx` +- `practices/standard-report/src/App/hooks/usePagePreview.ts` +- `practices/standard-report/src/App/styles/page.css` + +任务: + +1. 将 page 内图表区域切换为 `StandardAPP mode="view" builder={pageBuilder.chart}` 或等价入口。 +2. 删除或下线 `usePagePreview(...) + buildVSeed()` 这条 report 私有预览路径。 +3. 保留 `PageHoverActions`、翻页按钮、洞察文案等 report 专属壳层。 +4. 调整 page 样式,确保嵌入 `standard view mode` 后尺寸、圆角和空态仍符合 report 视觉。 + +约束: + +1. `standard-report` 不再直接承担 chart preview 数据构建职责。 +2. page 仍然只绑定当前 `pageBuilder.chart`,不复制 builder。 +3. 替换后 preview 与后续 editor 必须走同一 chart source。 + +交付物: + +- `standard-report` 新 preview surface +- 旧 preview hook/组件退出主路径 + +## Phase 5: 把 `Drawer` 编辑改成图表卡片放大全屏过渡 + +目标:把当前“侧边抽屉进入编辑”改成“从当前图表位置放大全屏”的丝滑编辑体验。 + +**重点文件**: + +- `practices/standard-report/src/App/components/editor/ReportEditorDrawer.tsx` +- 视情况新增 `practices/standard-report/src/App/components/editor/ReportEditorTransition.tsx` +- 视情况新增 `practices/standard-report/src/App/hooks/useEditorTransition.ts` +- `practices/standard-report/src/App/styles/editor.css` +- `practices/standard-report/src/model/report-store.ts` + +任务: + +1. 为“打开编辑器”补充 transition 所需的源节点信息,例如 `DOMRect`、触发 pageId。 +2. 新增 portal/fixed overlay 过渡层,由 report 自己控制进入与退出动画。 +3. 过渡起始态对齐当前图表卡片,结束态扩展为视口级全屏容器。 +4. 进入动画完成后再稳定挂载 `StandardAPP mode="edit" builder={pageBuilder.chart}`。 +5. 关闭时执行反向收拢动画,回到当前 page 的图表区域。 +6. 对源节点缺失、切页后关闭、快速连续点击等场景提供降级和兜底。 + +约束: + +1. 不继续依赖 `Drawer width="100vw"` 作为主过渡。 +2. 不把浏览器 `requestFullscreen()` 作为 report 编辑主入口。 +3. 动画失败时允许退化为淡入/淡出,但不能阻塞编辑功能。 + +交付物: + +- 自定义全屏 transition layer +- `standard edit mode` 的报表内嵌入口 +- 反向收拢关闭行为 + +## Phase 6: 联调、边界修补与测试补全 + +目标:把 `standard` 与 `standard-report` 的联动行为跑通,补齐最容易出问题的边界。 + +### 6.1 `standard-report` 集成测试 + +**测试文件**: + +- `practices/standard-report/tests/page-hover-actions.test.tsx` +- 视情况新增 `practices/standard-report/tests/report-editor-transition.test.tsx` +- 视情况新增 `practices/standard-report/tests/report-preview-mode.test.tsx` + +测试内容: + +1. 点击编辑后会进入全屏编辑态,不再表现为侧边抽屉滑入。 +2. 关闭编辑器后能回到当前 page 的 preview。 +3. 编辑过程中 builder 更新后,返回报表页能看到最新内容。 +4. page 切换、新增、删除后,preview/editor 仍然绑定正确 `page.chart`。 + +### 6.2 验证命令 + +按仓库约定优先做子包验证,再做仓库级校验: + +```bash +pnpm --filter=standard run test +pnpm --filter=standard-report run test +pnpm run lint +pnpm run typecheck +``` + +如涉及格式或生成物,再补: + +```bash +pnpm run format +``` + +验收标准: + +1. `standard` 默认编辑态不回退。 +2. `standard view mode` 可被 `standard-report` 稳定复用。 +3. `standard-report` 不再直接维护独立 chart preview build 链路。 +4. 编辑打开/关闭具备从卡片到全屏、再回到卡片的空间连续性。 +5. 所有编辑仍直接落到当前 `page.chart`。 + +## 里程碑 + +1. M1: `standard` 完成实例级 store 改造,多实例可同时挂载。 +2. M2: `standard` 暴露稳定的 `view mode` / `edit mode`。 +3. M3: `standard-report` 预览路径切到 `standard view mode`。 +4. M4: 报表内编辑态切换为“图表卡片放大全屏”过渡。 +5. M5: `standard` 与 `standard-report` 的测试、lint、typecheck 通过。 diff --git a/practices/standard-report/docs/README.md b/practices/standard-report/docs/README.md new file mode 100644 index 0000000000..00a3f76add --- /dev/null +++ b/practices/standard-report/docs/README.md @@ -0,0 +1,7 @@ +# practices/standard-report/docs + +本目录存放 `practices/standard-report` 的目标、ADR 与执行计划。 + +- 一个主题一个目录:`YYYY-MM-DD-topic/` +- 文件按需创建:`goal.md`、`adr.md`、`plan.md` +- `src/` 放示例实现,`docs/` 放设计与演进记录 diff --git a/practices/demo/eslint.config.mjs b/practices/standard-report/eslint.config.mjs similarity index 100% rename from practices/demo/eslint.config.mjs rename to practices/standard-report/eslint.config.mjs diff --git a/practices/standard-report/package.json b/practices/standard-report/package.json new file mode 100644 index 0000000000..6057c569c5 --- /dev/null +++ b/practices/standard-report/package.json @@ -0,0 +1,56 @@ +{ + "name": "standard-report", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "source": "./src/index.tsx", + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "rslib build", + "dev": "rslib build --watch", + "format": "prettier --write .", + "lint": "eslint .", + "test": "rstest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@ant-design/icons": "6.1.0", + "@visactor/vbi": "workspace:*", + "@visactor/vquery": "workspace:*", + "@visactor/vseed": "workspace:*", + "antd": "6.1.3", + "motion": "12.38.0", + "react": "19.2.3", + "react-dom": "19.2.3", + "standard": "workspace:*", + "zustand": "5.0.6" + }, + "devDependencies": { + "@eslint/js": "9.39.1", + "@rsbuild/plugin-react": "1.4.2", + "@rslib/core": "0.18.6", + "@rstest/core": "0.8.3", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.1", + "@types/react": "19.2.7", + "eslint": "9.39.1", + "globals": "16.5.0", + "jsdom": "26.1.0", + "prettier": "3.7.3", + "typescript": "5.9.3", + "typescript-eslint": "8.48.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "private": true +} diff --git a/practices/demo/rslib.config.ts b/practices/standard-report/rslib.config.ts similarity index 100% rename from practices/demo/rslib.config.ts rename to practices/standard-report/rslib.config.ts diff --git a/practices/demo/rstest.config.ts b/practices/standard-report/rstest.config.ts similarity index 100% rename from practices/demo/rstest.config.ts rename to practices/standard-report/rstest.config.ts diff --git a/practices/standard-report/rstest.setup.ts b/practices/standard-report/rstest.setup.ts new file mode 100644 index 0000000000..ca55a45452 --- /dev/null +++ b/practices/standard-report/rstest.setup.ts @@ -0,0 +1,48 @@ +import { expect } from '@rstest/core'; +import * as jestDomMatchers from '@testing-library/jest-dom/matchers'; + +expect.extend(jestDomMatchers); + +const createCanvasContext = () => { + return { + fillStyle: '', + strokeStyle: '', + clearRect() {}, + fillRect() {}, + strokeRect() {}, + beginPath() {}, + closePath() {}, + moveTo() {}, + lineTo() {}, + arc() {}, + rect() {}, + fill() {}, + stroke() {}, + clip() {}, + save() {}, + restore() {}, + translate() {}, + scale() {}, + rotate() {}, + setTransform() {}, + transform() {}, + drawImage() {}, + fillText() {}, + strokeText() {}, + createImageData() { + return []; + }, + getImageData() { + return { data: new Uint8ClampedArray() }; + }, + putImageData() {}, + measureText() { + return { width: 0 }; + }, + }; +}; + +Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', { + configurable: true, + value: () => createCanvasContext(), +}); diff --git a/practices/standard-report/src/App/App.tsx b/practices/standard-report/src/App/App.tsx new file mode 100644 index 0000000000..9a1410c1f7 --- /dev/null +++ b/practices/standard-report/src/App/App.tsx @@ -0,0 +1,25 @@ +import type { VBIReportBuilder } from '@visactor/vbi'; +import { Spin } from 'antd'; +import { useEffect } from 'react'; +import { useReportStore } from 'src/model'; +import { ReportWorkbench } from './layout/ReportWorkbench'; +import './styles/index.css'; + +type AppProps = { + builder?: VBIReportBuilder; +}; + +export const APP = ({ builder }: AppProps) => { + const initialize = useReportStore((state) => state.initialize); + const initialized = useReportStore((state) => state.initialized); + + useEffect(() => { + return initialize(builder); + }, [builder, initialize]); + + if (!initialized) { + return ; + } + + return ; +}; diff --git a/practices/standard-report/src/App/components/editor/ReportEditorDrawer.tsx b/practices/standard-report/src/App/components/editor/ReportEditorDrawer.tsx new file mode 100644 index 0000000000..6a817e31b1 --- /dev/null +++ b/practices/standard-report/src/App/components/editor/ReportEditorDrawer.tsx @@ -0,0 +1,235 @@ +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { AnimatePresence, motion } from 'motion/react'; +import { APP as StandardAPP } from 'standard'; +import { Button } from 'antd'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { type EditorSourceRect, useReportStore } from 'src/model'; +import { useReportPageBuilder } from '../../hooks/useReportPageBuilder'; +import { + findPreviewElement, + getFallbackRect, + getFrameMotionValues, + getViewportRect, + readEditorSourceRect, +} from './transition-utils'; + +type TransitionPhase = 'closed' | 'opening' | 'open' | 'closing'; +const CLOSE_FALLBACK_MS = 420; + +const FRAME_TRANSITION = { + type: 'spring', + stiffness: 250, + damping: 30, + mass: 0.88, +} as const; + +const BACKDROP_TRANSITION = { + duration: 0.24, + ease: [0.22, 1, 0.36, 1], +} as const; + +const createSafeSourceRect = ( + pageId?: string, + sourceRect?: EditorSourceRect | null, +): EditorSourceRect | null => { + if (sourceRect) { + return sourceRect; + } + + if (pageId && typeof document !== 'undefined') { + return readEditorSourceRect(findPreviewElement(pageId)); + } + + return null; +}; + +export const ReportEditorDrawer = () => { + const open = useReportStore((state) => state.editorOpen); + const pageId = useReportStore((state) => state.activePageId); + const sourceRect = useReportStore((state) => state.editorSourceRect); + const report = useReportStore((state) => state.report); + const reportBuilder = useReportStore((state) => state.reportBuilder); + const closeEditor = useReportStore((state) => state.closeEditor); + const pageBuilder = useReportPageBuilder(reportBuilder, pageId); + const page = report.pages.find((item) => item.id === pageId); + const [phase, setPhase] = useState('closed'); + const [mounted, setMounted] = useState(false); + const [showEditor, setShowEditor] = useState(false); + const [frameRect, setFrameRect] = useState(() => + typeof window === 'undefined' + ? { top: 0, left: 0, width: 0, height: 0, borderRadius: 0 } + : getViewportRect(), + ); + const rafRef = useRef(null); + const closeTimeoutRef = useRef(null); + + const clearPending = useCallback(() => { + if (rafRef.current !== null) { + cancelAnimationFrame(rafRef.current); + rafRef.current = null; + } + + if (closeTimeoutRef.current !== null) { + window.clearTimeout(closeTimeoutRef.current); + closeTimeoutRef.current = null; + } + }, []); + + const finishClose = useCallback(() => { + clearPending(); + setMounted(false); + setShowEditor(false); + setPhase('closed'); + closeEditor(); + }, [clearPending, closeEditor]); + + const beginClose = useCallback(() => { + if (!mounted) { + closeEditor(); + return; + } + + clearPending(); + setShowEditor(false); + setPhase('closing'); + + const targetRect = + createSafeSourceRect( + pageId, + readEditorSourceRect(findPreviewElement(pageId)), + ) ?? + sourceRect ?? + getFallbackRect(); + + rafRef.current = requestAnimationFrame(() => { + setFrameRect(targetRect); + }); + closeTimeoutRef.current = window.setTimeout(() => { + finishClose(); + }, CLOSE_FALLBACK_MS); + }, [clearPending, closeEditor, finishClose, mounted, pageId, sourceRect]); + + useEffect(() => { + if (!open || !pageId || typeof window === 'undefined') { + return; + } + + clearPending(); + + const nextSourceRect = + createSafeSourceRect(pageId, sourceRect) ?? getFallbackRect(); + + setMounted(true); + setShowEditor(false); + setPhase('opening'); + setFrameRect(nextSourceRect); + + rafRef.current = requestAnimationFrame(() => { + rafRef.current = requestAnimationFrame(() => { + setFrameRect(getViewportRect()); + }); + }); + + return () => { + clearPending(); + }; + }, [clearPending, open, pageId, sourceRect]); + + useEffect(() => { + if (!mounted) { + return; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + beginClose(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + }, [beginClose, mounted]); + + useEffect(() => { + return () => { + clearPending(); + }; + }, [clearPending]); + + const handleFrameAnimationComplete = useCallback(() => { + if (phase === 'opening') { + setShowEditor(true); + setPhase('open'); + return; + } + + if (phase === 'closing') { + finishClose(); + } + }, [finishClose, phase]); + + if (!mounted || typeof document === 'undefined') { + return null; + } + + return createPortal( + +
+ +
+ +
+
+ +
+
+ + Chart Editor + +

+ {page?.title || '编辑图表'} +

+
+
+ +
+ {showEditor && pageBuilder ? ( + + ) : ( +
+ )} +
+ +
+
+ , + document.body, + ); +}; diff --git a/practices/standard-report/src/App/components/editor/transition-utils.ts b/practices/standard-report/src/App/components/editor/transition-utils.ts new file mode 100644 index 0000000000..0c848fd360 --- /dev/null +++ b/practices/standard-report/src/App/components/editor/transition-utils.ts @@ -0,0 +1,69 @@ +import type { EditorSourceRect } from 'src/model'; + +const toBorderRadius = (value: string) => { + const radius = Number.parseFloat(value); + return Number.isFinite(radius) ? radius : 28; +}; + +export const readEditorSourceRect = ( + element?: Element | null, +): EditorSourceRect | null => { + if (!(element instanceof HTMLElement)) { + return null; + } + + const rect = element.getBoundingClientRect(); + if (!rect.width || !rect.height) { + return null; + } + + const computedStyle = window.getComputedStyle(element); + + return { + top: rect.top, + left: rect.left, + width: rect.width, + height: rect.height, + borderRadius: toBorderRadius(computedStyle.borderRadius), + }; +}; + +export const getViewportRect = (): EditorSourceRect => { + return { + top: 0, + left: 0, + width: window.innerWidth, + height: window.innerHeight, + borderRadius: 0, + }; +}; + +export const getFallbackRect = (): EditorSourceRect => { + const width = Math.max(window.innerWidth * 0.88, 320); + const height = Math.max(window.innerHeight * 0.78, 320); + + return { + top: Math.max((window.innerHeight - height) / 2, 24), + left: Math.max((window.innerWidth - width) / 2, 24), + width: Math.min(width, window.innerWidth - 48), + height: Math.min(height, window.innerHeight - 48), + borderRadius: 28, + }; +}; + +export const getFrameMotionValues = (rect: EditorSourceRect) => { + const viewportWidth = Math.max(window.innerWidth, 1); + const viewportHeight = Math.max(window.innerHeight, 1); + + return { + x: rect.left, + y: rect.top, + scaleX: rect.width / viewportWidth, + scaleY: rect.height / viewportHeight, + borderRadius: rect.borderRadius, + }; +}; + +export const findPreviewElement = (pageId: string) => { + return document.querySelector(`[data-report-preview-page-id="${pageId}"]`); +}; diff --git a/practices/standard-report/src/App/components/page/PageHoverActions.tsx b/practices/standard-report/src/App/components/page/PageHoverActions.tsx new file mode 100644 index 0000000000..a1a95798cd --- /dev/null +++ b/practices/standard-report/src/App/components/page/PageHoverActions.tsx @@ -0,0 +1,69 @@ +import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Tooltip } from 'antd'; + +type PageHoverActionsProps = { + canRemove: boolean; + showPlaceholder: boolean; + onAddPage: () => void; + onEdit: () => void; + onRemovePage: () => void; +}; + +const getActions = ({ + canRemove, + onAddPage, + onEdit, + onRemovePage, + showPlaceholder, +}: PageHoverActionsProps) => { + return [ + { + icon: , + key: 'edit', + label: showPlaceholder ? '添加当前图表' : '编辑当前图表', + onClick: onEdit, + }, + { + icon: , + key: 'add', + label: '新增一页', + onClick: onAddPage, + }, + ...(canRemove + ? [ + { + icon: , + key: 'remove', + label: '删除当前页', + onClick: onRemovePage, + }, + ] + : []), + ]; +}; + +export const PageHoverActions = (props: PageHoverActionsProps) => { + const actions = getActions(props); + + return ( +
+ {actions.map((action) => ( + +
+ ); +}; diff --git a/practices/standard-report/src/App/components/page/PageInsight.tsx b/practices/standard-report/src/App/components/page/PageInsight.tsx new file mode 100644 index 0000000000..1e7172be9d --- /dev/null +++ b/practices/standard-report/src/App/components/page/PageInsight.tsx @@ -0,0 +1,10 @@ +export const PageInsight = () => { + return ( +