feat: add initial design documentation

This commit is contained in:
gchust 2025-03-29 19:19:33 +08:00
parent 31d936c228
commit d237a8c49e
6 changed files with 905 additions and 0 deletions

View 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

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

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

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

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