mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-07 22:49:26 +08:00
feat: add initial design documentation
This commit is contained in:
parent
31d936c228
commit
d237a8c49e
27
.cursor/rules/000-global.mdc
Normal file
27
.cursor/rules/000-global.mdc
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Instructions
|
||||
|
||||
During your interaction with the user, if you find anything reusable in this project (e.g. version of a library, model name), especially about a fix to a mistake you made or a correction you received, you should take note in the `Lessons` section in the `.cursorrules` file so you will not make the same mistake again.
|
||||
|
||||
You should also use the `.cursorrules` file as a Scratchpad to organize your thoughts. Especially when you receive a new task, you should first review the content of the Scratchpad, clear old different task if necessary, first explain the task, and plan the steps you need to take to complete the task. You can use todo markers to indicate the progress, e.g.
|
||||
[X] Task 1
|
||||
[ ] Task 2
|
||||
|
||||
Also update the progress of the task in the Scratchpad when you finish a subtask.
|
||||
Especially when you finished a milestone, it will help to improve your depth of task accomplishment to use the Scratchpad to reflect and plan.
|
||||
The goal is to help you maintain a big picture as well as the progress of the task. Always refer to the Scratchpad when you plan the next step.
|
||||
|
||||
# Lessons
|
||||
|
||||
## User Specified Lessons
|
||||
|
||||
- Due to Cursor's limit, when you use `git` and `gh` and need to submit a multiline commit message, first write the message in a file, and then use `git commit -F <filename>` or similar command to commit. And then remove the file. Include "[Cursor] " in the commit message and PR title.
|
||||
|
||||
## Cursor learned
|
||||
- When searching for recent news, use the current year (2025) instead of previous years, or simply use the "recent" keyword to get the latest information
|
||||
|
||||
# Scratchpad
|
31
.cursor/rules/001-global-guidelines.mdc
Normal file
31
.cursor/rules/001-global-guidelines.mdc
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
---
|
||||
description: Global coding standards and best practices for the project.
|
||||
globs: "**/*"
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Global Project Guidelines
|
||||
|
||||
- **TypeScript Practices**:
|
||||
- Enforce strict typing with explicit type definitions.
|
||||
- Prefer interfaces over type aliases for object shapes.
|
||||
- Avoid the use of `any`; strive for precise types.
|
||||
|
||||
- **React Guidelines**:
|
||||
- Employ functional components exclusively.
|
||||
- Utilize React hooks for state and side effects management.
|
||||
- Ensure components are reusable and adhere to the Single Responsibility Principle.
|
||||
|
||||
- **Ant Design Integration**:
|
||||
- Import Ant Design components as needed to optimize bundle size.
|
||||
- Customize Ant Design themes to align with project branding.
|
||||
|
||||
- **CSS-in-JS Styling**:
|
||||
- Use a CSS-in-JS solution (e.g., styled-components or @emotion/styled) for component styling.
|
||||
- Define styles within the same file as the component for better cohesion.
|
||||
- Avoid inline styles; encapsulate styles within styled components or equivalent.
|
23
.cursor/rules/002-component-guidelines.mdc
Normal file
23
.cursor/rules/002-component-guidelines.mdc
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
---
|
||||
description: Standards and practices for React components.
|
||||
globs: "src/components/**/*.tsx"
|
||||
---
|
||||
|
||||
# Component Guidelines
|
||||
|
||||
- **File Structure**:
|
||||
- Each component should reside in its own folder within `src/components/`.
|
||||
- Include related files such as styles and tests within the same folder.
|
||||
|
||||
- **Naming Conventions**:
|
||||
- Use PascalCase for component names.
|
||||
- Suffix component files with `.tsx`.
|
||||
|
||||
- **Testing**:
|
||||
- Write unit tests for each component using Jest and React Testing Library.
|
||||
- Ensure tests cover various states and interactions of the component.
|
23
.cursor/rules/003-documentation-guidelines.mdc
Normal file
23
.cursor/rules/003-documentation-guidelines.mdc
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
---
|
||||
description: Guidelines for writing and maintaining project documentation using Dumi.
|
||||
globs: "docs/**/*.md"
|
||||
---
|
||||
|
||||
# Documentation Guidelines
|
||||
|
||||
- **Structure**:
|
||||
- Organize documentation files within the `docs` directory.
|
||||
- Use a clear and consistent hierarchy to facilitate navigation.
|
||||
|
||||
- **Content**:
|
||||
- Provide comprehensive explanations and examples for components and features.
|
||||
- Keep language clear and concise, suitable for both new and experienced developers.
|
||||
|
||||
- **Dumi Integration**:
|
||||
- Utilize Dumi's features to create interactive and visually appealing documentation.
|
||||
- Ensure that documentation is updated alongside code changes to maintain accuracy.
|
775
packages/plugins/@nocobase/plugin-event-filter-system/design.md
Normal file
775
packages/plugins/@nocobase/plugin-event-filter-system/design.md
Normal file
@ -0,0 +1,775 @@
|
||||
# Event Filter System Design
|
||||
|
||||
## Introduction
|
||||
|
||||
The Event Filter System plugin integrates two core systems in NocoBase:
|
||||
|
||||
1. **Event System**: Manages event publishing and subscriptions throughout the application.
|
||||
2. **Filter System**: Provides data transformation capabilities across various data types.
|
||||
|
||||
This document outlines the design principles, architecture, and implementation details of these systems.
|
||||
|
||||
## System Flow Diagrams
|
||||
|
||||
### Event System Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Component Event Trigger] -->|dispatches| B[EventManager]
|
||||
B -->|routes to| C[Event Listeners]
|
||||
C -->|filter by| D{Filter Function}
|
||||
D -->|not matching| E[Skip Listener]
|
||||
D -->|matching| F[Execute Listener]
|
||||
F -->|may trigger| G[Subsequent Events]
|
||||
F -->|returns| H[Result Collection]
|
||||
H -->|resolves| I[Original Promise]
|
||||
|
||||
subgraph Options
|
||||
J[priority]
|
||||
K[once]
|
||||
L[blocking]
|
||||
M[uischema]
|
||||
N[filter function]
|
||||
end
|
||||
|
||||
C -.-> Options
|
||||
```
|
||||
|
||||
### Filter System Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Component] -->|calls| B[applyFilter]
|
||||
B -->|finds matching| C[Registered Filters]
|
||||
C -->|sorted by| D[Priority]
|
||||
D -->|processes| E[Input Data]
|
||||
E -->|transforms through| F[Filter Chain]
|
||||
F -->|returns| G[Modified Data]
|
||||
G -->|back to| A
|
||||
|
||||
subgraph Registration
|
||||
H[addFilter] -->|registers| I[New Filter]
|
||||
I -->|with| J[Name]
|
||||
I -->|with| K[Filter Function]
|
||||
I -->|with| L[Options]
|
||||
end
|
||||
```
|
||||
|
||||
### Component-Event-Filter Integration
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[UI Component] -->|user action| B[Event Trigger]
|
||||
B -->|dispatches| C[EventManager]
|
||||
C -->|routes to| D[Event Handlers]
|
||||
D -->|may call| E[FilterManager]
|
||||
E -->|transforms| F[Data]
|
||||
F -->|returns to| D
|
||||
D -->|updates| A
|
||||
|
||||
G[Other Components] -->|register| H[Event Listeners]
|
||||
H -.->|connect to| D
|
||||
|
||||
I[Plugin] -->|registers| J[Filters]
|
||||
J -.->|connect to| E
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### Filter API
|
||||
|
||||
The Filter API provides mechanisms for data transformation with enhanced customization capabilities through `addFilter`.
|
||||
|
||||
#### FilterManager
|
||||
|
||||
```typescript
|
||||
class FilterManager { // 单例, 可以挂载到app上
|
||||
addFilter(name: string, filter: FilterFunction, options?: FilterOptions); // options目前只有prority
|
||||
removeFilter(name: string, filter?: FilterFunction);
|
||||
applyFilter(name: string, ...inputs: any): any;
|
||||
}
|
||||
```
|
||||
|
||||
#### Filter Naming Convention
|
||||
|
||||
Filter names use a modular structure: `[module]:[attribute]` format. Registration requires using this namespace convention.
|
||||
|
||||
#### Methods
|
||||
|
||||
##### addFilter
|
||||
|
||||
```typescript
|
||||
// 注册 Filter
|
||||
app.filterManager.addFilter(name: string, filter: FilterFunction, options?: FilterOptions): Function
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `name`: Filter name, format should be `module:attribute`, does not support wildcards!
|
||||
- `filter`: Filter function, receives values and returns modified values, uses applyFilter internally
|
||||
- `options`: Configuration options, including priority settings
|
||||
|
||||
**Returns**:
|
||||
- A function to unregister the filter
|
||||
|
||||
##### removeFilter
|
||||
|
||||
```typescript
|
||||
app.filterManager.removeFilter(name: string, filter: FilterFunction, options?: FilterOptions): Function
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `name`: Filter name, format should be `module:attribute`
|
||||
- `filter`: Filter function; if provided, removes only that specific filter
|
||||
|
||||
##### applyFilter
|
||||
|
||||
```typescript
|
||||
// 应用 Filter
|
||||
app.filterManager.applyFilter(name: string, input: InputType[name]): any
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `name`: Filter name
|
||||
- `value`: Original value to be processed by the filter
|
||||
|
||||
**Returns**:
|
||||
- The value after being processed by all matching filters
|
||||
|
||||
#### Examples
|
||||
|
||||
**Height Filter Example**:
|
||||
```typescript
|
||||
// 注册表格高度 filter
|
||||
app.filterManager.addFilter('table:props:height', (height, options) => {
|
||||
// 根据选项是否需要高度
|
||||
if (options.compact) {
|
||||
return Math.min(height, 300);
|
||||
}
|
||||
return height;
|
||||
}, { priority: 10 });
|
||||
|
||||
// 链接多个filter
|
||||
app.filterManager.addFilter('table:props', (props) => {
|
||||
const height = app.filterManager.applyFilter('table:props:height', props);
|
||||
const width = app.filterManager.applyFilter('table:props:width', props);
|
||||
const collection = app.filterManager.applyFilter('table:props:collection', props);
|
||||
return {
|
||||
...props,
|
||||
height,
|
||||
width,
|
||||
collection
|
||||
};
|
||||
});
|
||||
|
||||
<Table props={app.filterManager.applyFilter('table:props')}/>
|
||||
```
|
||||
|
||||
**Complex Filter Examples**:
|
||||
```typescript
|
||||
const schema = useFieldSchema();
|
||||
|
||||
// 应用单个属性 filter
|
||||
const height = useMemo(() =>
|
||||
app.filterManager.applyFilter('table:default:height', {
|
||||
compact: isCompactMode,
|
||||
height: 600
|
||||
}),
|
||||
[isCompactMode]
|
||||
);
|
||||
|
||||
// 应用多个属性的综合 filter
|
||||
const props = useMemo(() =>
|
||||
app.filterManager.applyFilter('table:props', { ...schema, height }, {
|
||||
mode: 'default',
|
||||
context: { currentUser }
|
||||
}),
|
||||
[schema, height, currentUser]
|
||||
);
|
||||
|
||||
return <Table {...props} />;
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
#### useAddFilter
|
||||
|
||||
```typescript
|
||||
const useAddFilter = (name:string, filterFunction, options) => {
|
||||
useEffect(() => {
|
||||
return app.filterManager.addFilter(name, filterFunction, options);
|
||||
}, [...]);
|
||||
}
|
||||
```
|
||||
|
||||
#### useApplyFilter
|
||||
|
||||
```typescript
|
||||
const useApplyFilter = (name: string) => {
|
||||
return useCallback((props) => {
|
||||
return app.filterManager.applyFilter(name, props);
|
||||
}, [app]);
|
||||
}
|
||||
```
|
||||
|
||||
### Event API
|
||||
|
||||
The Event API manages event subscriptions and publications throughout the application.
|
||||
|
||||
#### EventManager
|
||||
|
||||
```typescript
|
||||
class EventManager { // 单例, 可以挂载到app上
|
||||
// 注册事件监听器
|
||||
on(eventName, listener, options = {}) { ... }
|
||||
|
||||
// 注册一次性事件监听器
|
||||
once(eventName, listener, options = {}) { ... }
|
||||
|
||||
// 取消事件监听器
|
||||
off(eventName, listener) { ... }
|
||||
|
||||
// 触发事件
|
||||
async dispatchEvent(eventName, context = {}) { ... }
|
||||
|
||||
// 获取特定事件的所有监听器, 主要用于测试
|
||||
getListeners(eventName) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### Event Naming Convention
|
||||
|
||||
Event names follow the format: `[module]:[action]` such as:
|
||||
|
||||
- `table:row:select` // 表格行选择事件
|
||||
- `modal:confirm:click` // 确认模态框按钮事件
|
||||
- `form:submit` // 表单提交
|
||||
|
||||
Where:
|
||||
- `module(Module)`: Represents the component's context, can span multiple layers
|
||||
- `action(Action)`: Indicates the specific action performed
|
||||
|
||||
#### EventContext
|
||||
|
||||
```typescript
|
||||
interface EventContext<T = any> {
|
||||
// 事件源 (dispatchEvent调用者) 信息
|
||||
source?: {
|
||||
id?: string;
|
||||
type?: string; // 'user-interaction', 'system', 'workflow'
|
||||
[key: string]: any // any extra information
|
||||
};
|
||||
|
||||
// 用于指定接收者信息, 主要用于精准触发事件监听
|
||||
target?: {
|
||||
id?: string; // id
|
||||
uischema?: ISchema; // ui schema
|
||||
}
|
||||
|
||||
// 事件相关数据
|
||||
payload?: T; // 事件特定的数据, TODO: 列出常用的事件相关数据
|
||||
|
||||
// 元数据
|
||||
meta?: {
|
||||
timestamp?: number; // 事件触发的时间
|
||||
userId?: string; // 当前用户ID (可选)
|
||||
event?: string | string[]; // 事件名, 可以通配符匹配 table:*, 可支持多个eventName组成的数组
|
||||
[table:refresh, form:refresh]
|
||||
};
|
||||
|
||||
// TODO: 是否需要支持事件中断执行?
|
||||
// propagation: true; // 可能改成propagation.stop()是否停止继续执行
|
||||
}
|
||||
```
|
||||
|
||||
#### Listener Options
|
||||
|
||||
```typescript
|
||||
interface EventListenerOptions {
|
||||
priority?: number; // 监听器优先级,数值越大优先级越高,默认为 0
|
||||
once?: boolean; // 是否只执行一次,默认为 false
|
||||
blocking?: boolean; // 是否为阻塞监听器,默认false,即默认行为是不需要当前监听器结束再执行后续监听器
|
||||
uischema?: ISchema; // Json Schema,主要用于精准触发事件
|
||||
filter?: (ctx: EventContext, options: EventListenerOptions) => boolean; // 过滤函数,允许根据上下文来确定当前监听器是否执行,默认行为是:处理多播(dispatch时未指定target)和单播(dispatch时指定当前组件为target)事件。
|
||||
}
|
||||
```
|
||||
|
||||
Default filter function:
|
||||
```typescript
|
||||
function filter(ctx: EventContext, options: EventListenerOptions) {
|
||||
if (ctx.target) {
|
||||
return true;
|
||||
}
|
||||
if (ctx.target.uischema === options.uischema) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
##### on
|
||||
|
||||
```typescript
|
||||
on(event: string | string[], listener: EventListener, options?: EventListenerOptions): Unsubscriber
|
||||
```
|
||||
|
||||
Used to register event listeners. When the specified event is triggered, the listener will execute.
|
||||
|
||||
##### once
|
||||
|
||||
```typescript
|
||||
once(event: string | string[], listener: EventListener, options?: OmitEventListenerOptions, 'once'): Unsubscriber
|
||||
```
|
||||
|
||||
Similar to `on()` but the listener is automatically removed after the first execution.
|
||||
|
||||
##### off
|
||||
|
||||
```typescript
|
||||
off(eventName) // 取消事件所有的监听器
|
||||
off(eventName, listener) // 取消事件指定监听器
|
||||
```
|
||||
|
||||
Removes event listeners.
|
||||
|
||||
##### dispatchEvent
|
||||
|
||||
```typescript
|
||||
app.eventManager.dispatchEvent(eventName, ctx: EventContext, timeout: number): Promise<void>;
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `eventName`: Event name
|
||||
- `ctx`: Event context
|
||||
- `timeout`: Event timeout in milliseconds, default 2 minutes
|
||||
|
||||
**Returns**:
|
||||
- A Promise that resolves when all listeners have completed processing
|
||||
|
||||
## Form Submission Flow Example
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Form
|
||||
participant EventManager
|
||||
participant BeforeSubmitListeners
|
||||
participant SubmitListeners
|
||||
participant AfterSubmitListeners
|
||||
|
||||
User->>Form: Click Submit
|
||||
Form->>EventManager: dispatches :beforeSubmit
|
||||
EventManager->>BeforeSubmitListeners: route to handlers
|
||||
BeforeSubmitListeners-->>EventManager: return (may include stop flag)
|
||||
alt if beforeSubmitCtx.stop is true
|
||||
EventManager-->>Form: stop submission
|
||||
Form-->>User: Show feedback
|
||||
else proceed with submission
|
||||
EventManager-->>Form: continue
|
||||
Form->>EventManager: dispatches form:submit
|
||||
EventManager->>SubmitListeners: route to handlers
|
||||
SubmitListeners-->>EventManager: return results
|
||||
EventManager-->>Form: continue
|
||||
Form->>EventManager: dispatches form:afterSubmit
|
||||
EventManager->>AfterSubmitListeners: route to handlers
|
||||
AfterSubmitListeners-->>EventManager: return results
|
||||
EventManager-->>Form: complete
|
||||
Form-->>User: Show success feedback
|
||||
end
|
||||
```
|
||||
|
||||
## Design Considerations
|
||||
|
||||
### Current Implementation Challenges
|
||||
|
||||
1. **Object Immutability**: Currently `source`, `target`, `meta` properties can only be frozen using `Object.freeze`, but payload modification is not restricted. Should more attributes be protected?
|
||||
|
||||
2. **Async Event Collection**: Determining whether `dispatchEvent` should collect and return values from handlers. If return values are needed, should they be from all handlers or specific ones?
|
||||
|
||||
3. **Performance Optimization**:
|
||||
- Preventing event storms
|
||||
- Implementing debounce mechanisms
|
||||
|
||||
4. **Error Handling**: Considering how to handle errors in event listeners reliably
|
||||
|
||||
5. **Event Chaining**: Supporting complex event flows and preventing unintended circular dependencies
|
||||
|
||||
### Integration Points
|
||||
|
||||
1. **Component Actions**:
|
||||
```jsx
|
||||
<Button onClick={() => app.dispatchEvent('form:record:create', { ... })}>
|
||||
新建
|
||||
</Button>
|
||||
|
||||
// 内核已经监听form:record:create事件
|
||||
app.on('form:record:create', (ctx) => {
|
||||
openCreationModal(ctx);
|
||||
});
|
||||
```
|
||||
|
||||
2. **Form Submission Flow**:
|
||||
```jsx
|
||||
// block-form.ts
|
||||
async function submit() {
|
||||
const beforeSubmitCtx = {
|
||||
source: {
|
||||
id: actionId
|
||||
}
|
||||
...
|
||||
};
|
||||
await app.eventManager.dispatchEvent(':beforeSubmit', beforeSubmitCtx);
|
||||
if (beforeSubmitCtx.stop) {
|
||||
return;
|
||||
}
|
||||
// 继续提交
|
||||
await app.eventManager.dispatchEvent('form:submit', ctx);
|
||||
await app.eventManager.dispatchEvent('form:afterSubmit', ctx);
|
||||
}
|
||||
|
||||
// event-flow.ts
|
||||
app.eventManager.on('form.beforeSubmit', (ctx) => {
|
||||
await openDialog(ctx);
|
||||
await openDialog(ctx);
|
||||
}, {
|
||||
filter: (eventCtx, listenerOptions) => {
|
||||
return eventCtx.source.id === listenerOptions.id;
|
||||
},
|
||||
id: actionId
|
||||
});
|
||||
```
|
||||
|
||||
3. **Table Component Integration**:
|
||||
```jsx
|
||||
<Table1 />
|
||||
const refresh = () => {
|
||||
console.log('refresh table1 data');
|
||||
};
|
||||
app.eventManager.on('table:data:refresh', ()=> {
|
||||
refresh();
|
||||
}, {
|
||||
target: {
|
||||
uischema: { 'x-uid': 'table1' }
|
||||
}
|
||||
});
|
||||
|
||||
<Table2 />
|
||||
const refresh = () => {
|
||||
console.log('refresh table1 data');
|
||||
};
|
||||
app.eventManager.on('table:data:refresh', ()=> {
|
||||
refresh();
|
||||
}, {
|
||||
target: {
|
||||
uischema: { 'x-uid': 'table2' }
|
||||
}
|
||||
});
|
||||
|
||||
<Button onClick={() => {
|
||||
app.eventManager.dispatchEvent('table:data:refresh', ctx); // this will refresh both table1 and table2
|
||||
}}>
|
||||
|
||||
<Button2 onClick={() => {
|
||||
app.eventManager.dispatchEvent('table:data:refresh', {
|
||||
target: {
|
||||
uischema: {'x-uid': 'tablue2'}
|
||||
}
|
||||
}); // refresh just tablue2
|
||||
}} />
|
||||
```
|
||||
|
||||
## Architecture Goals
|
||||
|
||||
1. **Decouple UI and Business Logic**: Clean separation between event triggering and handling
|
||||
```jsx
|
||||
<Button onClick={onClick}/>
|
||||
|
||||
function onClick() {
|
||||
// 这个action实现方式不适合编排,因为需要写多个上面的代码
|
||||
openDialog(ctx);
|
||||
refresh(ctx);
|
||||
}
|
||||
|
||||
function anotherAction(ctx) {
|
||||
// 这个action实现方式就适合编排,只有保存需要触发的事件及顺序即可
|
||||
app.dispatchEvent('dialog.open', ctx);
|
||||
app.dispatchEvent('data.refresh', ctx);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Improve Testability**: Business logic in event handlers is easier to test independently
|
||||
```jsx
|
||||
// 事件处理器可单独测试
|
||||
test('record create event handler', () => {
|
||||
const mockContext = { ... };
|
||||
const handler = app.getEventHandler('form:record:create');
|
||||
|
||||
handler(mockContext);
|
||||
|
||||
expect(modalOpenSpy).toHaveBeenCalledWith({...});
|
||||
});
|
||||
```
|
||||
|
||||
3. **Enhance Extensibility**: Through filter API, allowing extensions without modifying core code
|
||||
```jsx
|
||||
// 通过事件系统可以在不修改原始组件的情况下添加新功能
|
||||
app.on('table:record:create', async (ctx) => {
|
||||
await app.dispatchEvent('data:validation', ctx); // 后续只要检测监听返回,即可在不改变区块内的前提下增加验证功能
|
||||
openModal(ctx);
|
||||
});
|
||||
```
|
||||
|
||||
4. **Simplify Workflow**: Support for event chains and workflows
|
||||
```jsx
|
||||
// 简化复杂流程
|
||||
app.on('table:record:create', (ctx) => {
|
||||
// 可触发后续事件
|
||||
openForm(ctx).then((formData) => {
|
||||
app.dispatchEvent('form:data:refresh', { ...ctx, payload: formData });
|
||||
app.dispatchEvent('table:data:refresh', {...});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
5. **Method Encapsulation**: Better organize code through functional modules
|
||||
```jsx
|
||||
function openDialog(ctx) {
|
||||
// 打开弹出
|
||||
}
|
||||
|
||||
function refresh(ctx) {
|
||||
// 区块刷新
|
||||
}
|
||||
|
||||
app.on('dialog.open', openDialog);
|
||||
app.on('data.refresh', refresh);
|
||||
```
|
||||
|
||||
6. **Consistency**: Maintaining predictable behavior across implementation
|
||||
```jsx
|
||||
// 方便编排: 支持通过配置动态改变运行时逻辑,为事件流做准备
|
||||
function openDialog(ctx) {
|
||||
// 这个action就不适合编排,因为需要写入上面的函数
|
||||
openDialog(ctx);
|
||||
refresh(ctx);
|
||||
}
|
||||
|
||||
function anotherAction(ctx) {
|
||||
// 这个action实现方式就适合编排,只有保存需要触发的事件及顺序即可
|
||||
app.dispatchEvent('dialog.open', ctx);
|
||||
app.dispatchEvent('data.refresh', ctx);
|
||||
}
|
||||
```
|
||||
|
||||
## Menu Configuration Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[SettingMenu] -->|click| B[openSettingDialog]
|
||||
B -->|dispatches| C[block:setting:update event]
|
||||
|
||||
subgraph Filter Flow
|
||||
D[applyFilter] -->|settings.menu.handle| E[applyFilter]
|
||||
E -->|settings.menu.text| F[applyFilter]
|
||||
F -->|settings.menu.props| G[text + handling]
|
||||
end
|
||||
|
||||
subgraph Event Flow
|
||||
H[onClick] -->|dispatches| I[eventFlow:add]
|
||||
I -->|dispatches| J[dialog:open]
|
||||
J -->|dispatches| K[block:setting:update]
|
||||
end
|
||||
|
||||
C --> L[Registered Listeners]
|
||||
L --> M[Update Block Settings]
|
||||
```
|
||||
|
||||
## Implementation Examples
|
||||
|
||||
### Component Usage
|
||||
|
||||
```jsx
|
||||
function Component (props) {
|
||||
const [actionSettings, setActionSettings] = useActionSettings();
|
||||
const [filterSettings, setFilterSettings] = useFilterSettings();
|
||||
// TODO: 如何避免不必要的运行逻辑,例如filter执行时,能否有些提示?
|
||||
const [newProps, done] = useFilter('xxx:props', props, filterSettings);
|
||||
|
||||
if(!done || newProps['hidden']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClick = async () => {
|
||||
// do something before dispatchEvent
|
||||
const eventCtx = {
|
||||
payload: xxx,
|
||||
settings: actionSettings
|
||||
};
|
||||
if (actionSettings.secondConfirm) {
|
||||
await app.dispatchEvent('system:dialog:open', eventCtx)
|
||||
}
|
||||
const done = await app.dispatchEvent('action:xxx:click', eventCtx);
|
||||
if (done && actionSettings.refreshAfterDone) {
|
||||
await app.dispatchEvent('block:yyy:refresh', eventCtx);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<SomeInternalComponent onClick={handleClick} ...newProps />
|
||||
<SettingMenus {newProps.menus}/>
|
||||
</>
|
||||
}
|
||||
|
||||
function MenuItem(props) {
|
||||
const [actionSettings, setActionSettings] = useActionSettings();
|
||||
const [filterSettings, setFilterSettings] = useFilterSettings();
|
||||
const [newProps, done] = useFilter('menuItem:props', props, filterSettings);
|
||||
if (!done) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
app.dispatchEvent('xxx:settings', {
|
||||
actionSettings,
|
||||
props
|
||||
});
|
||||
};
|
||||
|
||||
return <MenuItemInner onClick={handleClick}>
|
||||
|
||||
</MenuItemInner>
|
||||
}
|
||||
|
||||
// some global filters.ts file
|
||||
app.filterManager.on('xxx:props', (...inputs) => {
|
||||
const newProps = {};
|
||||
newProps['text'] = app.filterManager.applyFilter('xxx:props:text', ...inputs);
|
||||
// applyFilter组合了很多个单里filter...
|
||||
return newProps;
|
||||
});
|
||||
```
|
||||
|
||||
### Table Block Example
|
||||
|
||||
```typescript
|
||||
// table-filters.ts
|
||||
const { addFilter, applyFilter } = app.filterManager;
|
||||
|
||||
addFilter('block:table', async(ctx: {payload, settings}) => {
|
||||
// props属性
|
||||
const tableProps = applyFilter('block:table:props', ctx);
|
||||
|
||||
// linkages 配置, 后续data, fields, actions的生成可用其结果
|
||||
const linkageRules = applyFilter('block:table:linkages', ctx);
|
||||
|
||||
// data数据, 可以根据settings来过滤一些数据
|
||||
const data = applyFilter('block:table:data', ctx);
|
||||
|
||||
// 用来渲染字段的
|
||||
const fields = applyFilter('block:table:fields', ctx);
|
||||
|
||||
// 用来渲染actions的
|
||||
const actions = applyFilter('block:table:actions', ctx);
|
||||
|
||||
// 用来渲染配置项的
|
||||
const settingOptions = applyFilter('block:table:settingOptions', ctx);
|
||||
|
||||
return { props: tableProps, linkageRules, data, fields, actions, settingOptions }
|
||||
});
|
||||
|
||||
addFilter('block:table:props', async (ctx: {props, settings, previousResult}) => {
|
||||
// 包含 title, description, height 等props的处理
|
||||
const blockCommonProps = await applyFilter('block:common:props', ctx);
|
||||
|
||||
// 包含
|
||||
const tableProps = convertTableProps(props); // table block区域已处理的
|
||||
|
||||
// 大部分配置项都是filter配置, 公用的配置项命名为 block:xxx, table自己的配置项为 block:table:setting
|
||||
return { ...blockCommonProps, tableProps };
|
||||
});
|
||||
|
||||
addFilter('block:table:fields', async (ctx) => {
|
||||
const dataFields = applyFilter('block:fields', ctx);
|
||||
// some logical related to table block
|
||||
const tableFields = convertToTableFields(dataFields);
|
||||
return tableFields;
|
||||
});
|
||||
addFilter('block:table:actions', async (ctx) => {
|
||||
const actions = applyFilter('block:actions', ctx);
|
||||
// some logical related to table actions
|
||||
const tableActions = convertToTableActions(actions);
|
||||
return tableActions;
|
||||
});
|
||||
|
||||
addFilter('block:table:datas', async (ctx) => {
|
||||
const data = applyFilter('block:data', ctx);
|
||||
const tableData = convertToTableData(data);
|
||||
return tableData;
|
||||
});
|
||||
```
|
||||
|
||||
## Action Component Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Action Component
|
||||
A[Action Button] -->|click| B[handleClick]
|
||||
B -->|dispatches| C[action:xxx:click]
|
||||
end
|
||||
|
||||
subgraph Event Listeners
|
||||
C --> D[Registered Handlers]
|
||||
D --> E{Has Listeners?}
|
||||
E -->|No| F[Default Behavior]
|
||||
E -->|Yes| G[Execute Handlers]
|
||||
G -->|May dispatch| H[Secondary Events]
|
||||
end
|
||||
|
||||
subgraph Data Flow
|
||||
I[Initial Data] -->|through| J[Filter Chain]
|
||||
J -->|modified by| K[Various Filters]
|
||||
K -->|returns| L[Transformed Data]
|
||||
end
|
||||
|
||||
H -.->|may trigger| I
|
||||
L -.->|used by| A
|
||||
```
|
||||
|
||||
## Improvement Points
|
||||
|
||||
### Current Issues
|
||||
|
||||
1. **Global State Complexity**: Both Event & Filter being global increases complexity, making code harder to trace. Not knowing how many filters have been processed in a single operation can lead to unexpected behavior.
|
||||
- **Solution**: Add consistent conventions and constraints.
|
||||
|
||||
2. **Resource Management Risk**: Direct use of global state can lead to memory leaks.
|
||||
- **Solution**: Provide hooks for automatic resource cleanup.
|
||||
|
||||
3. **Simple vs. Complex Scenarios**: For simple data transformations, using Filter API might be overengineered.
|
||||
- **Solution**: Document use cases where direct code is preferable over the Filter API.
|
||||
|
||||
4. **Filter Settings & Action Settings**: Unclear storage strategy - should they be stored in UI schema or separately?
|
||||
- **Solution**: Consider using a dedicated filter like `applyFilter('deprecated:props')` to collect old settings.
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
1. **Type Safety**: Improve TypeScript integration to provide better autocompletion and type checking for event names and payloads.
|
||||
|
||||
2. **Documentation**: Create comprehensive documentation with real-world examples for both systems.
|
||||
|
||||
3. **Performance Monitoring**: Add tools to analyze event propagation and filter execution performance.
|
||||
|
||||
4. **Testing Utilities**: Develop specialized testing utilities for simulating events and validating filter chains.
|
||||
|
||||
5. **Event Visualization**: Create developer tools to visualize event flows and filter chains for debugging.
|
||||
|
||||
6. **Middleware Support**: Add middleware capabilities for cross-cutting concerns like logging, validation, and error handling.
|
||||
|
||||
7. **Standardized Event Schema**: Further refine the event schema to ensure consistency across the application.
|
||||
|
||||
8. **Filter Composition**: Improve ways to compose and reuse filters for complex transformations.
|
||||
|
||||
9. **Conditional Event Processing**: Enhanced support for conditional event processing based on application state.
|
||||
|
||||
10. **Error Recovery**: Implement robust error recovery mechanisms for failed event handlers and filters.
|
26
repos
Normal file
26
repos
Normal file
@ -0,0 +1,26 @@
|
||||
packages/ | git@github.com:nocobase/pro-plugins.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-file-storage-s3-pro.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-backups.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-workflow-approval.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-auth-wecom-huaxiabank.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-auth-ldap.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-departments.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-audit-logger.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-auth-dingtalk.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-data-visualization-echarts.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-data-source-external-oracle.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-auth-wecom.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-block-multi-step-form.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-auth-oidc.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-email-manager.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-action-template-print.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-workflow-javascript.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-workflow-json-variable-mapping.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-workflow-subflow.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-workflow-date-calculation.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-block-tree.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-workflow-webhook.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-migration-manager.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-qr-uploader.git
|
||||
packages/plugins/@nocobase/ | git@github.com:nocobase/plugin-migration-manager.git
|
||||
packages/plugins/@youchaoyun/ | git@github.com:nocobase/youchaoyun.git
|
Loading…
x
Reference in New Issue
Block a user