From d3777b236d85c091a2ab1e83d7dc3616e72a465e Mon Sep 17 00:00:00 2001 From: roymondchen Date: Tue, 2 Jan 2024 20:57:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(data-source,utils,runtime):=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=BA=90setData=E6=94=AF=E6=8C=81=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/App.ts | 17 ++--- packages/data-source/src/DataSourceManager.ts | 12 ++-- .../src/createDataSourceManager.ts | 5 +- packages/data-source/src/data-sources/Base.ts | 22 ++++-- packages/data-source/src/types.ts | 5 ++ packages/data-source/tests/DataSource.spec.ts | 71 +++++++++++++++++++ packages/editor/src/services/props.ts | 13 ++-- packages/utils/package.json | 4 +- packages/utils/src/index.ts | 59 ++++++--------- packages/utils/tests/unit/index.spec.ts | 56 ++++++++++----- pnpm-lock.yaml | 14 ++-- runtime/react/page/App.tsx | 14 +++- runtime/react/page/main.tsx | 2 +- runtime/react/playground/main.tsx | 2 +- runtime/vue2/page/App.vue | 13 +++- runtime/vue2/page/main.ts | 2 +- runtime/vue3/page/App.vue | 13 +++- runtime/vue3/page/main.ts | 2 +- 18 files changed, 229 insertions(+), 97 deletions(-) diff --git a/packages/core/src/App.ts b/packages/core/src/App.ts index b6c4d12e..27c491f4 100644 --- a/packages/core/src/App.ts +++ b/packages/core/src/App.ts @@ -38,7 +38,7 @@ import { import Env from './Env'; import { bindCommonEventListener, isCommonMethod, triggerCommonMethod } from './events'; -import type Node from './Node'; +import Node from './Node'; import Page from './Page'; import { fillBackgroundImage, isNumber, style2Obj } from './utils'; @@ -265,20 +265,21 @@ class App extends EventEmitter implements AppCore { for (const [, value] of this.page.nodes) { value.events?.forEach((event) => { const eventName = `${event.name}_${value.data.id}`; - const eventHanlder = (fromCpt: Node, ...args: any[]) => { + const eventHandler = (fromCpt: Node, ...args: any[]) => { this.eventHandler(event, fromCpt, args); }; - this.eventList.set(eventHanlder, eventName); - this.on(eventName, eventHanlder); + this.eventList.set(eventHandler, eventName); + this.on(eventName, eventHandler); }); } } - public emit(name: string | symbol, node: any, ...args: any[]): boolean { - if (node?.data?.id) { - return super.emit(`${String(name)}_${node.data.id}`, node, ...args); + public emit(name: string | symbol, ...args: any[]): boolean { + const [node, ...otherArgs] = args; + if (node instanceof Node && node?.data?.id) { + return super.emit(`${String(name)}_${node.data.id}`, node, ...otherArgs); } - return super.emit(name, node, ...args); + return super.emit(name, ...args); } /** diff --git a/packages/data-source/src/DataSourceManager.ts b/packages/data-source/src/DataSourceManager.ts index 791f0b3a..199f769d 100644 --- a/packages/data-source/src/DataSourceManager.ts +++ b/packages/data-source/src/DataSourceManager.ts @@ -24,7 +24,7 @@ import type { AppCore, DataSourceSchema, Id, MNode } from '@tmagic/schema'; import { compiledCond, compiledNode, DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, isObject } from '@tmagic/utils'; import { DataSource, HttpDataSource } from './data-sources'; -import type { DataSourceManagerData, DataSourceManagerOptions } from './types'; +import type { ChangeEvent, DataSourceManagerData, DataSourceManagerOptions } from './types'; class DataSourceManager extends EventEmitter { private static dataSourceClassMap = new Map(); @@ -123,14 +123,14 @@ class DataSourceManager extends EventEmitter { this.data[ds.id] = ds.data; - ds.on('change', () => { - this.setData(ds); + ds.on('change', (changeEvent: ChangeEvent) => { + this.setData(ds, changeEvent); }); } - public setData(ds: DataSource) { - Object.assign(this.data[ds.id], ds.data); - this.emit('change', ds.id); + public setData(ds: DataSource, changeEvent: ChangeEvent) { + this.data[ds.id] = ds.data; + this.emit('change', ds.id, changeEvent); } public removeDataSource(id: string) { diff --git a/packages/data-source/src/createDataSourceManager.ts b/packages/data-source/src/createDataSourceManager.ts index 0dd25408..879bfa5b 100644 --- a/packages/data-source/src/createDataSourceManager.ts +++ b/packages/data-source/src/createDataSourceManager.ts @@ -21,7 +21,7 @@ import type { AppCore } from '@tmagic/schema'; import { getDepNodeIds, getNodes, replaceChildNode } from '@tmagic/utils'; import DataSourceManager from './DataSourceManager'; -import { DataSourceManagerData } from './types'; +import type { ChangeEvent, DataSourceManagerData } from './types'; /** * 创建数据源管理器 @@ -52,7 +52,7 @@ export const createDataSourceManager = (app: AppCore, useMock?: boolean, initial // ssr环境下,数据应该是提前准备好的(放到initialData中),不应该发生变化,无需监听 // 有initialData不一定是在ssr环境下 if (app.jsEngine !== 'nodejs') { - dataSourceManager.on('change', (sourceId: string) => { + dataSourceManager.on('change', (sourceId: string, changeEvent: ChangeEvent) => { const dep = dsl.dataSourceDeps?.[sourceId] || {}; const condDep = dsl.dataSourceCondDeps?.[sourceId] || {}; @@ -66,6 +66,7 @@ export const createDataSourceManager = (app: AppCore, useMock?: boolean, initial return dataSourceManager.compiledNode(newNode); }), sourceId, + changeEvent, ); }); } diff --git a/packages/data-source/src/data-sources/Base.ts b/packages/data-source/src/data-sources/Base.ts index d89a8b76..3b2e81ae 100644 --- a/packages/data-source/src/data-sources/Base.ts +++ b/packages/data-source/src/data-sources/Base.ts @@ -18,9 +18,9 @@ import EventEmitter from 'events'; import type { AppCore, CodeBlockContent, DataSchema, DataSourceSchema } from '@tmagic/schema'; -import { getDefaultValueFromFields } from '@tmagic/utils'; +import { getDefaultValueFromFields, setValueByKeyPath } from '@tmagic/utils'; -import type { DataSourceOptions } from '@data-source/types'; +import type { ChangeEvent, DataSourceOptions } from '@data-source/types'; /** * 静态数据源 @@ -101,10 +101,20 @@ export default class DataSource e this.#methods = methods; } - public setData(data: Record) { - // todo: 校验数据,看是否符合 schema - this.data = data; - this.emit('change'); + public setData(data: any, path?: string) { + if (path) { + setValueByKeyPath(path, data, this.data); + } else { + // todo: 校验数据,看是否符合 schema + this.data = data; + } + + const changeEvent: ChangeEvent = { + updateData: data, + path, + }; + + this.emit('change', changeEvent); } public getDefaultData() { diff --git a/packages/data-source/src/types.ts b/packages/data-source/src/types.ts index e585c3aa..a3c473d7 100644 --- a/packages/data-source/src/types.ts +++ b/packages/data-source/src/types.ts @@ -37,3 +37,8 @@ export interface DataSourceManagerOptions { export interface DataSourceManagerData { [key: string]: Record; } + +export interface ChangeEvent { + path?: string; + updateData: any; +} diff --git a/packages/data-source/tests/DataSource.spec.ts b/packages/data-source/tests/DataSource.spec.ts index 7bcc2cb6..b3f55e85 100644 --- a/packages/data-source/tests/DataSource.spec.ts +++ b/packages/data-source/tests/DataSource.spec.ts @@ -34,3 +34,74 @@ describe('DataSource', () => { expect(ds.isInit).toBeTruthy(); }); }); + +describe('DataSource setData', () => { + test('setData', () => { + const ds = new DataSource({ + schema: { + type: 'base', + id: '1', + fields: [{ name: 'name', defaultValue: 'name' }], + methods: [], + }, + app: {}, + }); + + ds.init(); + + expect(ds.data.name).toBe('name'); + + ds.setData({ name: 'name2' }); + + expect(ds.data.name).toBe('name2'); + + ds.setData('name3', 'name'); + + expect(ds.data.name).toBe('name3'); + }); + + test('setDataByPath', () => { + const ds = new DataSource({ + schema: { + type: 'base', + id: '1', + fields: [ + { name: 'name', defaultValue: 'name' }, + { + name: 'obj', + type: 'object', + fields: [{ name: 'a' }, { name: 'b', type: 'array', fields: [{ name: 'c' }] }], + }, + ], + methods: [], + }, + app: {}, + }); + + ds.init(); + + expect(ds.data.name).toBe('name'); + expect(ds.data.obj.b).toHaveLength(0); + + ds.setData({ + name: 'name', + obj: { + a: 'a', + b: [ + { + c: 'c', + }, + ], + }, + }); + + expect(ds.data.obj.b).toHaveLength(1); + expect(ds.data.obj.b[0].c).toBe('c'); + + ds.setData('c1', 'obj.b.0.c'); + expect(ds.data.obj.b[0].c).toBe('c1'); + expect(ds.data.obj.a).toBe('a'); + ds.setData('a1', 'obj.a'); + expect(ds.data.obj.a).toBe('a1'); + }); +}); diff --git a/packages/editor/src/services/props.ts b/packages/editor/src/services/props.ts index 627c2ce7..7bdfe01e 100644 --- a/packages/editor/src/services/props.ts +++ b/packages/editor/src/services/props.ts @@ -17,12 +17,12 @@ */ import { reactive } from 'vue'; -import { cloneDeep, get, mergeWith, set } from 'lodash-es'; +import { cloneDeep, mergeWith } from 'lodash-es'; import { DepTargetType } from '@tmagic/dep'; import type { FormConfig } from '@tmagic/form'; import type { Id, MComponent, MNode } from '@tmagic/schema'; -import { guid, toLine } from '@tmagic/utils'; +import { getValueByKeyPath, guid, setValueByKeyPath, toLine } from '@tmagic/utils'; import depService from '@editor/services/dep'; import editorService from '@editor/services/editor'; @@ -194,17 +194,22 @@ class Props extends BaseService { public replaceRelateId(originConfigs: MNode[], targetConfigs: MNode[]) { const relateIdMap = this.getRelateIdMap(); if (Object.keys(relateIdMap).length === 0) return; + const target = depService.getTarget(DepTargetType.RELATED_COMP_WHEN_COPY, DepTargetType.RELATED_COMP_WHEN_COPY); if (!target) return; + originConfigs.forEach((config: MNode) => { const newId = relateIdMap[config.id]; const targetConfig = targetConfigs.find((targetConfig) => targetConfig.id === newId); + if (!targetConfig) return; + target.deps[config.id]?.keys?.forEach((fullKey) => { - const relateOriginId = get(config, fullKey); + const relateOriginId = getValueByKeyPath(fullKey, config); const relateTargetId = relateIdMap[relateOriginId]; if (!relateTargetId) return; - set(targetConfig, fullKey, relateTargetId); + + setValueByKeyPath(fullKey, relateTargetId, targetConfig); }); }); } diff --git a/packages/utils/package.json b/packages/utils/package.json index a1d6e3e3..eff9e5a5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -31,12 +31,14 @@ }, "dependencies": { "@tmagic/schema": "1.3.8", - "dayjs": "^1.11.4" + "dayjs": "^1.11.4", + "lodash-es": "^4.17.21" }, "peerDependencies": { "dayjs": "^1.11.4" }, "devDependencies": { + "@types/lodash-es": "^4.17.4", "@types/node": "^18.19.0", "rimraf": "^3.0.2", "typescript": "^5.0.4", diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 00fce860..7400c6ff 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -18,6 +18,7 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; +import { cloneDeep, get as objectGet, set as objectSet } from 'lodash-es'; import type { DataSchema, DataSourceDeps, Id, MComponent, MNode } from '@tmagic/schema'; import { NodeType } from '@tmagic/schema'; @@ -164,22 +165,10 @@ export const guid = (digit = 8): string => return v.toString(16); }); -export const getValueByKeyPath: any = (keys: string, value: Record = {}) => { - const path = keys.split('.'); - const pathLength = path.length; +export const getValueByKeyPath: any = (keys: string, data: Record = {}) => objectGet(data, keys); - return path.reduce((accumulator, currentValue: any, currentIndex: number) => { - if (Object.prototype.toString.call(accumulator) === '[object Object]' || Array.isArray(accumulator)) { - return accumulator[currentValue]; - } - - if (pathLength - 1 === currentIndex) { - return undefined; - } - - return {}; - }, value); -}; +export const setValueByKeyPath: any = (keys: string, value: any, data: Record = {}) => + objectSet(data, keys, value); export const getNodes = (ids: Id[], data: MNode[] = []): MNode[] => { const nodes: MNode[] = []; @@ -266,32 +255,25 @@ export const compiledNode = ( const keyPrefix = '__magic__'; keys.forEach((key) => { - const keyPath = `${key}`.split('.'); - const keyPathLength = keyPath.length; - keyPath.reduce((accumulator, currentValue: any, currentIndex) => { - if (keyPathLength - 1 === currentIndex) { - const cacheKey = `${keyPrefix}${currentValue}`; + const cacheKey = `${keyPrefix}${key}`; - if (typeof accumulator[cacheKey] === 'undefined') { - accumulator[cacheKey] = accumulator[currentValue]; - } + const value = getValueByKeyPath(key, node); + let templateValue = getValueByKeyPath(cacheKey, node); - try { - accumulator[currentValue] = compile(accumulator[cacheKey]); - } catch (e) { - console.error(e); - accumulator[currentValue] = ''; - } + if (typeof templateValue === 'undefined') { + setValueByKeyPath(cacheKey, value, node); + templateValue = value; + } - return accumulator; - } + let newValue; + try { + newValue = compile(templateValue); + } catch (e) { + console.error(e); + newValue = ''; + } - if (isObject(accumulator) || Array.isArray(accumulator)) { - return accumulator[currentValue]; - } - - return {}; - }, node); + setValueByKeyPath(key, newValue, node); }); if (Array.isArray(node.items)) { @@ -368,7 +350,8 @@ export const getDefaultValueFromFields = (fields: DataSchema[]) => { return; } - data[field.name] = field.defaultValue; + data[field.name] = cloneDeep(field.defaultValue); + return; } diff --git a/packages/utils/tests/unit/index.spec.ts b/packages/utils/tests/unit/index.spec.ts index 981f58d9..7f9cacc7 100644 --- a/packages/utils/tests/unit/index.spec.ts +++ b/packages/utils/tests/unit/index.spec.ts @@ -331,6 +331,28 @@ describe('getValueByKeyPath', () => { expect(value).toBe(1); }); + test('array', () => { + const value = util.getValueByKeyPath('a.0.b', { + a: [ + { + b: 1, + }, + ], + }); + + expect(value).toBe(1); + + const value1 = util.getValueByKeyPath('a[0].b', { + a: [ + { + b: 1, + }, + ], + }); + + expect(value1).toBe(1); + }); + test('error', () => { const value = util.getValueByKeyPath('a.b.c.d', { a: {}, @@ -475,7 +497,7 @@ describe('replaceChildNode', () => { expect(root[1].items[0].items[0].text).toBe('文本'); }); - test('replace whith parent', () => { + test('replace with parent', () => { const root = [ { id: 1, @@ -532,19 +554,21 @@ describe('compiledNode', () => { { id: 61705611, type: 'text', - text: '456', + text: { + value: '456', + }, }, { ds_bebcb2d5: { 61705611: { name: '文本', - keys: ['text'], + keys: ['text.value'], }, }, }, ); - expect(node.text).toBe('123'); + expect(node.text.value).toBe('123'); }); test('compile with source id', () => { @@ -595,61 +619,61 @@ describe('compiledNode', () => { describe('getDefaultValueFromFields', () => { test('最简单', () => { - const fileds = [ + const fields = [ { name: 'name', }, ]; - const data = util.getDefaultValueFromFields(fileds); + const data = util.getDefaultValueFromFields(fields); expect(data).toHaveProperty('name'); }); test('默认值为string', () => { - const fileds = [ + const fields = [ { name: 'name', defaultValue: 'name', }, ]; - const data = util.getDefaultValueFromFields(fileds); + const data = util.getDefaultValueFromFields(fields); expect(data.name).toBe('name'); }); test('type 为 object', () => { - const fileds: DataSchema[] = [ + const fields: DataSchema[] = [ { type: 'object', name: 'name', }, ]; - const data = util.getDefaultValueFromFields(fileds); + const data = util.getDefaultValueFromFields(fields); expect(data.name).toEqual({}); }); test('type 为 array', () => { - const fileds: DataSchema[] = [ + const fields: DataSchema[] = [ { type: 'array', name: 'name', }, ]; - const data = util.getDefaultValueFromFields(fileds); + const data = util.getDefaultValueFromFields(fields); expect(data.name).toEqual([]); }); test('type 为 null', () => { - const fileds: DataSchema[] = [ + const fields: DataSchema[] = [ { type: 'null', name: 'name', }, ]; - const data = util.getDefaultValueFromFields(fileds); + const data = util.getDefaultValueFromFields(fields); expect(data.name).toBeNull(); }); test('object 嵌套', () => { - const fileds: DataSchema[] = [ + const fields: DataSchema[] = [ { type: 'object', name: 'name', @@ -661,7 +685,7 @@ describe('getDefaultValueFromFields', () => { ], }, ]; - const data = util.getDefaultValueFromFields(fileds); + const data = util.getDefaultValueFromFields(fields); expect(data.name.key).toBe('key'); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05a39178..5d89ce47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -711,7 +715,13 @@ importers: dayjs: specifier: ^1.11.4 version: 1.11.4 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 devDependencies: + '@types/lodash-es': + specifier: ^4.17.4 + version: 4.17.7 '@types/node': specifier: ^18.19.0 version: 18.19.3 @@ -11263,7 +11273,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/runtime/react/page/App.tsx b/runtime/react/page/App.tsx index 7e6c7ead..fbc4decd 100644 --- a/runtime/react/page/App.tsx +++ b/runtime/react/page/App.tsx @@ -20,6 +20,7 @@ import React, { useContext, useState } from 'react'; import { cloneDeep } from 'lodash-es'; import Core from '@tmagic/core'; +import type { ChangeEvent } from '@tmagic/data-source'; import type { MNode } from '@tmagic/schema'; import { AppContent } from '@tmagic/ui-react'; import { replaceChildNode } from '@tmagic/utils'; @@ -31,11 +32,20 @@ function App() { const [config, setConfig] = useState(app.page.data); - app.dataSourceManager?.on('update-data', (nodes: MNode[]) => { + app.dataSourceManager?.on('update-data', (nodes: MNode[], sourceId: string, event: ChangeEvent) => { nodes.forEach((node) => { replaceChildNode(node, [config]); - setConfig(cloneDeep(config)); }); + + setConfig(cloneDeep(config)); + + setTimeout(() => { + app.emit('replaced-node', { + ...event, + nodes, + sourceId, + }); + }, 0); }); const MagicUiPage = app.resolveComponent('page'); diff --git a/runtime/react/page/main.tsx b/runtime/react/page/main.tsx index 3a895ea1..7adec9f8 100644 --- a/runtime/react/page/main.tsx +++ b/runtime/react/page/main.tsx @@ -54,7 +54,7 @@ const getLocalConfig = (): MApp[] => { window.magicDSL = []; Object.entries(datasources).forEach(([type, ds]: [string, any]) => { - DataSourceManager.registe(type, ds); + DataSourceManager.register(type, ds); }); const app = new Core({ diff --git a/runtime/react/playground/main.tsx b/runtime/react/playground/main.tsx index 174372c0..5eece53f 100644 --- a/runtime/react/playground/main.tsx +++ b/runtime/react/playground/main.tsx @@ -59,7 +59,7 @@ window.appInstance = app; let curPageId = ''; const updateConfig = (root: MApp) => { - app?.setConfig(root,curPageId); + app?.setConfig(root, curPageId); renderDom(); }; diff --git a/runtime/vue2/page/App.vue b/runtime/vue2/page/App.vue index a98b54cd..b04b36db 100644 --- a/runtime/vue2/page/App.vue +++ b/runtime/vue2/page/App.vue @@ -3,10 +3,11 @@