refactor: 依赖收集相关代码抽出独立成库

This commit is contained in:
roymondchen 2023-11-16 19:34:50 +08:00
parent 2b10e7eda9
commit 94641bad76
32 changed files with 947 additions and 710 deletions

View File

@ -54,6 +54,7 @@
"chalk": "^4.1.0",
"commitizen": "^4.3.0",
"conventional-changelog-cli": "^4.1.0",
"cosmiconfig": "^8.3.6",
"cz-conventional-changelog": "^3.3.0",
"element-plus": "^2.2.32",
"enquirer": "^2.3.6",
@ -79,7 +80,7 @@
"vite": "^4.4.4",
"vitepress": "1.0.0-rc.24",
"vitest": "^0.31.1",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"config": {
"commitizen": {

42
packages/dep/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"version": "1.3.1",
"name": "@tmagic/dep",
"type": "module",
"sideEffects": [
"dist/*"
],
"main": "dist/tmagic-dep.umd.cjs",
"module": "dist/tmagic-dep.js",
"types": "types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
"import": "./dist/tmagic-dep.js",
"require": "./dist/tmagic-dep.umd.cjs"
},
"./*": "./*"
},
"license": "Apache-2.0",
"scripts": {
"build": "npm run build:type && vite build",
"build:type": "npm run clear:type && tsc --declaration --emitDeclarationOnly --project tsconfig.build.json",
"clear:type": "rimraf ./types"
},
"engines": {
"node": ">=14"
},
"repository": {
"type": "git",
"url": "https://github.com/Tencent/tmagic-editor.git"
},
"dependencies": {
"@tmagic/schema": "1.3.1",
"@tmagic/utils": "1.3.1"
},
"devDependencies": {
"@types/node": "^15.12.4",
"rimraf": "^3.0.2",
"typescript": "^5.0.4",
"vite": "^4.4.4"
}
}

112
packages/dep/src/Target.ts Normal file
View File

@ -0,0 +1,112 @@
import type { DepData } from '@tmagic/schema';
import { DepTargetType, type IsTarget, type TargetOptions } from './types';
/**
*
*
*/
export default class Target {
/**
*
*/
public isTarget: IsTarget;
/**
* id
* id
*/
public id: string | number;
/**
*
*/
public name?: string;
/**
* type
*/
public type: string = DepTargetType.DEFAULT;
/**
*
* { 'node_id': { name: 'node_name', keys: [ created, mounted ] } }
*/
public deps: DepData = {};
constructor(options: TargetOptions) {
this.isTarget = options.isTarget;
this.id = options.id;
this.name = options.name;
if (options.type) {
this.type = options.type;
}
if (options.initialDeps) {
this.deps = options.initialDeps;
}
}
/**
*
* @param node
* @param key key配置了这个目标的id
*/
public updateDep(node: Record<string | number, any>, key: string | number) {
const dep = this.deps[node.id] || {
name: node.name,
keys: [],
};
if (node.name) {
dep.name = node.name;
}
this.deps[node.id] = dep;
if (dep.keys.indexOf(key) === -1) {
dep.keys.push(key);
}
}
/**
*
* @param node
* @param key key需要移除key
* @returns void
*/
public removeDep(node?: Record<string | number, any>, key?: string | number) {
if (!node) {
Object.keys(this.deps).forEach((depKey) => {
delete this.deps[depKey];
});
return;
}
const dep = this.deps[node.id];
if (!dep) return;
if (key) {
const index = dep.keys.indexOf(key);
dep.keys.splice(index, 1);
if (dep.keys.length === 0) {
delete this.deps[node.id];
}
} else {
delete this.deps[node.id];
}
}
/**
* key是否存在在依赖列表中
* @param node
* @param key key
* @returns boolean
*/
public hasDep(node: Record<string | number, any>, key: string | number) {
const dep = this.deps[node.id];
return Boolean(dep?.keys.find((d) => d === key));
}
public destroy() {
this.deps = {};
}
}

175
packages/dep/src/Watcher.ts Normal file
View File

@ -0,0 +1,175 @@
import { isObject } from '@tmagic/utils';
import type Target from './Target';
import { DepTargetType, type TargetList } from './types';
export default class Watcher {
private targetsList: TargetList = {};
constructor(options?: { initialTargets?: TargetList }) {
if (options?.initialTargets) {
this.targetsList = options.initialTargets;
}
}
public getTargetsList() {
return this.targetsList;
}
/**
* target
* @param type
* @returns Target[]
*/
public getTargets(type: string = DepTargetType.DEFAULT) {
return this.targetsList[type] || {};
}
/**
*
* @param target Target
*/
public addTarget(target: Target) {
const targets = this.getTargets(target.type) || {};
this.targetsList[target.type] = targets;
targets[target.id] = target;
}
/**
* id的target
* @param id target id
* @returns Target
*/
public getTarget(id: string | number) {
const allTargets = Object.values(this.targetsList);
for (const targets of allTargets) {
if (targets[id]) {
return targets[id];
}
}
}
/**
* id的target
* @param id target id
* @returns boolean
*/
public hasTarget(id: string | number) {
return Boolean(this.getTarget(id));
}
/**
* id的target
* @param id target id
*/
public removeTarget(id: string | number) {
const allTargets = Object.values(this.targetsList);
for (const targets of allTargets) {
if (targets[id]) {
targets[id].destroy();
delete targets[id];
}
}
}
/**
* target
* @param type
* @returns void
*/
public removeTargets(type: string = DepTargetType.DEFAULT) {
const targets = this.targetsList[type];
if (!targets) return;
for (const target of Object.values(targets)) {
target.destroy();
}
delete this.targetsList[type];
}
/**
* target
*/
public clearTargets() {
Object.keys(this.targetsList).forEach((key) => {
delete this.targetsList[key];
});
}
/**
*
* @param nodes
* @param deep
*/
public collect(nodes: Record<string | number, any>[], deep = false) {
Object.values(this.targetsList).forEach((targets) => {
Object.values(targets).forEach((target) => {
nodes.forEach((node) => {
// 先删除原有依赖,重新收集
target.removeDep(node);
this.collectItem(node, target, deep);
});
});
});
}
/**
*
* @param nodes
*/
public clear(nodes?: Record<string | number, any>[]) {
const clearedItemsNodeIds: (string | number)[] = [];
Object.values(this.targetsList).forEach((targets) => {
Object.values(targets).forEach((target) => {
if (nodes) {
nodes.forEach((node) => {
target.removeDep(node);
if (Array.isArray(node.items) && node.items.length && !clearedItemsNodeIds.includes(node.id)) {
clearedItemsNodeIds.push(node.id);
this.clear(node.items);
}
});
} else {
target.removeDep();
}
});
});
}
private collectItem(node: Record<string | number, any>, target: Target, deep = false) {
const collectTarget = (config: Record<string | number, any>, prop = '') => {
const doCollect = (key: string, value: any) => {
const keyIsItems = key === 'items';
const fullKey = prop ? `${prop}.${key}` : key;
if (target.isTarget(fullKey, value)) {
target.updateDep(node, fullKey);
} else if (!keyIsItems && Array.isArray(value)) {
value.forEach((item, index) => {
if (isObject(item)) {
collectTarget(item, `${fullKey}.${index}`);
}
});
} else if (isObject(value)) {
collectTarget(value, fullKey);
}
if (keyIsItems && deep && Array.isArray(value)) {
value.forEach((child) => {
this.collectItem(child, target, deep);
});
}
};
Object.entries(config).forEach(([key, value]) => {
if (typeof value === 'undefined' || value === '') return;
doCollect(key, value);
});
};
collectTarget(node);
}
}

View File

@ -0,0 +1,4 @@
export { default as Target } from './Target';
export { default as Watcher } from './Watcher';
export * from './utils';
export * from './types';

33
packages/dep/src/types.ts Normal file
View File

@ -0,0 +1,33 @@
import { DepData } from '@tmagic/schema';
import type Target from './Target';
/** 依赖收集的目标类型 */
export enum DepTargetType {
DEFAULT = 'default',
/** 代码块 */
CODE_BLOCK = 'code-block',
/** 数据源 */
DATA_SOURCE = 'data-source',
/** 数据源方法 */
DATA_SOURCE_METHOD = 'data-source-method',
/** 数据源条件 */
DATA_SOURCE_COND = 'data-source-cond',
}
export type IsTarget = (key: string | number, value: any) => boolean;
export interface TargetOptions {
isTarget: IsTarget;
id: string | number;
/** 类型,数据源、代码块或其他 */
type?: string;
name?: string;
initialDeps?: DepData;
}
export interface TargetList {
[type: string]: {
[targetId: string | number]: Target;
};
}

View File

@ -1,23 +1,28 @@
import { isEmpty } from 'lodash-es';
import { type CodeBlockContent, HookType, type Id } from '@tmagic/schema';
import {
type CodeBlockContent,
type DataSourceSchema,
type DepData,
type HookData,
HookType,
type Id,
} from '@tmagic/schema';
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils';
import dataSourceService from '@editor/services/dataSource';
import { Target } from '@editor/services/dep';
import { DepTargetType, type HookData } from '@editor/type';
import Target from './Target';
import { DepTargetType } from './types';
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent) =>
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) =>
new Target({
type: DepTargetType.CODE_BLOCK,
id,
initialDeps,
name: codeBlock.name,
isTarget: (key: string | number, value: any) => {
if (id === value) {
return true;
}
if (value?.hookType === HookType.CODE && !isEmpty(value.hookData)) {
if (value?.hookType === HookType.CODE && Array.isArray(value.hookData)) {
const index = value.hookData.findIndex((item: HookData) => item.codeId === id);
return Boolean(index > -1);
}
@ -26,13 +31,23 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent) =>
},
});
export const createDataSourceTarget = (id: Id) =>
export const createDataSourceTarget = (ds: DataSourceSchema, initialDeps: DepData = {}) =>
new Target({
type: DepTargetType.DATA_SOURCE,
id,
id: ds.id,
initialDeps,
isTarget: (key: string | number, value: any) => {
// 关联数据源对象或者在模板在使用数据源
if ((value?.isBindDataSource && value.dataSourceId) || (typeof value === 'string' && value.includes(`${id}`))) {
// 关联数据源对象,如:{ isBindDataSource: true, dataSourceId: 'xxx'}
// 或者在模板在使用数据源,如:`xxx${id.field}xxx`
if (
(value?.isBindDataSource && value.dataSourceId) ||
(typeof value === 'string' && value.includes(`${ds.id}`))
) {
return true;
}
// 指定数据源的字符串模板,如:{ isBindDataSourceField: true, dataSourceId: 'id', template: `xxx${field}xxx`}
if (value?.isBindDataSourceField && value.dataSourceId && typeof value.template === 'string') {
return true;
}
@ -50,31 +65,28 @@ export const createDataSourceTarget = (id: Id) =>
const dsId = prefixId.substring(prefixIndex + DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX.length);
return dsId === id && Boolean(dataSourceService.getDataSourceById(id));
return dsId === ds.id;
},
});
export const createDataSourceCondTarget = (id: string) =>
export const createDataSourceCondTarget = (ds: DataSourceSchema, initialDeps: DepData = {}) =>
new Target({
type: DepTargetType.DATA_SOURCE_COND,
id,
id: ds.id,
initialDeps,
isTarget: (key: string | number, value: any) => {
if (!Array.isArray(value) || value[0] !== id || !`${key}`.startsWith('displayConds')) return false;
const ds = dataSourceService.getDataSourceById(id);
if (!Array.isArray(value) || value[0] !== ds.id || !`${key}`.startsWith('displayConds')) return false;
return Boolean(ds?.fields?.find((field) => field.name === value[1]));
},
});
export const createDataSourceMethodTarget = (id: string) =>
export const createDataSourceMethodTarget = (ds: DataSourceSchema, initialDeps: DepData = {}) =>
new Target({
type: DepTargetType.DATA_SOURCE_METHOD,
id,
id: ds.id,
initialDeps,
isTarget: (key: string | number, value: any) => {
if (!Array.isArray(value) || value[0] !== id) return false;
const ds = dataSourceService.getDataSourceById(id);
if (!Array.isArray(value) || value[0] !== ds.id) return false;
return Boolean(ds?.methods?.find((method) => method.name === value[1]));
},

View File

@ -0,0 +1,28 @@
import { describe, expect, test } from 'vitest';
import Target from '../src/Target';
describe('Target', () => {
test('instance', () => {
const target = new Target({
isTarget: () => true,
id: 'target',
});
expect(target).toBeInstanceOf(Target);
});
test('default target type', () => {
const defaultTarget = new Target({
isTarget: () => true,
id: 'default',
type: 'default',
});
const target = new Target({
isTarget: () => true,
id: 'target',
type: 'target',
});
expect(defaultTarget.type).toBe('default');
expect(target.type).toBe('target');
});
});

View File

@ -1,88 +1,110 @@
import { describe, expect, test } from 'vitest';
import depService, { Target, Watcher } from '@editor/services/dep';
import Target from '../src/Target';
import Watcher from '../src/Watcher';
describe('Watcher', () => {
test('instance', () => {
const watcher = new Watcher();
expect(watcher).toBeInstanceOf(Watcher);
});
});
describe('depService', () => {
const defaultTarget = new Target({
id: 1,
name: 'test',
isTarget: () => true,
});
const target = new Target({
type: 'target',
id: 2,
name: 'test',
isTarget: () => true,
});
test('default target type', () => {
expect(defaultTarget.type).toBe('default');
expect(target.type).toBe('target');
});
test('addTarget', () => {
depService.addTarget(target);
const watcher = new Watcher();
const target = new Target({
isTarget: () => true,
id: 'target',
type: 'target',
});
expect(depService.getTarget(1)).toBeUndefined();
expect(depService.getTarget(2)?.id).toBe(2);
expect(Object.keys(depService.getTargets())).toHaveLength(0);
expect(Object.keys(depService.getTargets('target'))).toHaveLength(1);
watcher.addTarget(target);
expect(watcher.getTarget(1)).toBeUndefined();
expect(watcher.getTarget('target')?.id).toBe('target');
expect(Object.keys(watcher.getTargets())).toHaveLength(0);
expect(Object.keys(watcher.getTargets('target'))).toHaveLength(1);
});
test('clearTargets', () => {
depService.clearTargets();
const watcher = new Watcher();
watcher.clearTargets();
depService.addTarget(target);
const target = new Target({
isTarget: () => true,
id: 'target',
});
expect(depService.hasTarget(2)).toBeTruthy();
watcher.addTarget(target);
depService.clearTargets();
expect(watcher.hasTarget('target')).toBeTruthy();
expect(depService.hasTarget(2)).toBeFalsy();
watcher.clearTargets();
expect(watcher.hasTarget('target')).toBeFalsy();
});
test('hasTarget', () => {
depService.clearTargets();
const watcher = new Watcher();
depService.addTarget(target);
const target = new Target({
isTarget: () => true,
id: 'target2',
});
expect(depService.hasTarget(1)).toBeFalsy();
expect(depService.hasTarget(2)).toBeTruthy();
watcher.clearTargets();
watcher.addTarget(target);
expect(watcher.hasTarget('target')).toBeFalsy();
expect(watcher.hasTarget('target2')).toBeTruthy();
});
test('removeTarget', () => {
depService.clearTargets();
depService.addTarget(target);
expect(depService.hasTarget(2)).toBeTruthy();
depService.removeTarget(2);
expect(depService.hasTarget(2)).toBeFalsy();
const watcher = new Watcher();
const target = new Target({
isTarget: () => true,
id: 'target',
});
watcher.clearTargets();
watcher.addTarget(target);
expect(watcher.hasTarget('target')).toBeTruthy();
watcher.removeTarget('target');
expect(watcher.hasTarget('target')).toBeFalsy();
});
test('removeTargets', () => {
depService.clearTargets();
depService.addTarget(defaultTarget);
depService.addTarget(target);
expect(depService.hasTarget(1)).toBeTruthy();
expect(depService.hasTarget(2)).toBeTruthy();
depService.removeTargets('target');
expect(depService.hasTarget(1)).toBeTruthy();
expect(depService.hasTarget(2)).toBeFalsy();
const watcher = new Watcher();
depService.removeTargets('target1');
const defaultTarget = new Target({
isTarget: () => true,
id: 'defaultTarget',
});
const target = new Target({
isTarget: () => true,
type: 'targetType',
id: 'target',
});
watcher.clearTargets();
watcher.addTarget(defaultTarget);
watcher.addTarget(target);
expect(watcher.hasTarget('defaultTarget')).toBeTruthy();
expect(watcher.hasTarget('target')).toBeTruthy();
watcher.removeTargets('targetType');
expect(watcher.hasTarget('defaultTarget')).toBeTruthy();
expect(watcher.hasTarget('target')).toBeFalsy();
});
test('collect', () => {
depService.clearTargets();
const watcher = new Watcher();
depService.addTarget(
watcher.clearTargets();
watcher.addTarget(
new Target({
type: 'target',
id: 'collect_1',
@ -91,7 +113,7 @@ describe('depService', () => {
}),
);
depService.addTarget(
watcher.addTarget(
new Target({
type: 'target',
id: 'collect_2',
@ -100,7 +122,7 @@ describe('depService', () => {
}),
);
depService.collect([
watcher.collect([
{
id: 'node_1',
name: 'node',
@ -119,15 +141,15 @@ describe('depService', () => {
},
]);
const target1 = depService.getTarget('collect_1');
const target2 = depService.getTarget('collect_2');
const target1 = watcher.getTarget('collect_1');
const target2 = watcher.getTarget('collect_2');
expect(target1?.deps?.node_1.name).toBe('node');
expect(target2?.deps?.node_1.name).toBe('node');
expect(target1?.deps?.node_1.keys).toHaveLength(1);
expect(target2?.deps?.node_1.keys).toHaveLength(3);
depService.collect([
watcher.collect([
{
id: 'node_1',
name: 'node',
@ -149,7 +171,7 @@ describe('depService', () => {
expect(target1?.deps?.node_1).toBeUndefined();
expect(target2?.deps?.node_1.keys).toHaveLength(1);
depService.collect([
watcher.collect([
{
id: 'node_1',
name: 'node',
@ -161,7 +183,7 @@ describe('depService', () => {
expect(target1?.deps?.node_1).toBeUndefined();
expect(target2?.deps?.node_1.keys[0]).toBe('text1');
depService.clear([
watcher.clear([
{
id: 'node_1',
name: 'node',
@ -173,9 +195,11 @@ describe('depService', () => {
});
test('collect deep', () => {
depService.clearTargets();
const watcher = new Watcher();
depService.addTarget(
watcher.clearTargets();
watcher.addTarget(
new Target({
type: 'target',
id: 'collect_1',
@ -184,7 +208,7 @@ describe('depService', () => {
}),
);
depService.collect(
watcher.collect(
[
{
id: 'node_1',
@ -202,12 +226,12 @@ describe('depService', () => {
true,
);
const target1 = depService.getTarget('collect_1');
const target1 = watcher.getTarget('collect_1');
expect(target1?.deps?.node_1.name).toBe('node');
expect(target1?.deps?.node_2.name).toBe('node2');
depService.clear([
watcher.clear([
{
id: 'node_1',
name: 'node',

View File

@ -1,10 +1,10 @@
import { describe, expect, test } from 'vitest';
import * as dep from '@editor/utils/dep';
import * as utils from '../src/utils';
describe('dep', () => {
describe('utils', () => {
test('createCodeBlockTarget', () => {
const target = dep.createCodeBlockTarget('code_5316', {
const target = utils.createCodeBlockTarget('code_5316', {
name: 'code',
content: () => false,
params: [],
@ -33,7 +33,7 @@ describe('dep', () => {
expect(isTarget).toBeTruthy();
const target1 = dep.createCodeBlockTarget('1', {
const target1 = utils.createCodeBlockTarget('1', {
name: 'code',
content: () => false,
params: [],

View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"declarationDir": "types",
"forceConsistentCasingInFileNames": true,
"paths": {},
},
"include": [
"src"
],
}

View File

@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "../..",
},
}

View File

@ -0,0 +1,51 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2023 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'path';
import { defineConfig } from 'vite';
import pkg from './package.json';
export default defineConfig({
resolve: {
alias:
process.env.NODE_ENV === 'production'
? []
: [{ find: /^@tmagic\/schema/, replacement: path.join(__dirname, '../schema/src/index.ts') }],
},
build: {
sourcemap: true,
minify: false,
target: 'esnext',
lib: {
entry: 'src/index.ts',
name: 'TMagicDep',
fileName: 'tmagic-dep',
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external(id: string) {
return Object.keys(pkg.dependencies).some((k) => new RegExp(`^${k}`).test(id));
},
},
},
});

View File

@ -37,15 +37,15 @@
"typescript"
],
"dependencies": {
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"peerDependencies": {
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@types/node": "^15.12.4",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.3.2",
"rimraf": "^3.0.2",
"typescript": "^5.0.4",

View File

@ -48,6 +48,7 @@
"@babel/core": "^7.18.0",
"@element-plus/icons-vue": "^2.0.9",
"@tmagic/core": "1.3.1",
"@tmagic/dep": "1.3.1",
"@tmagic/design": "1.3.1",
"@tmagic/form": "1.3.1",
"@tmagic/schema": "1.3.1",
@ -63,13 +64,13 @@
"monaco-editor": "^0.41.0",
"moveable": "^0.51.1",
"serialize-javascript": "^6.0.0",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"peerDependencies": {
"@tmagic/design": "1.3.1",
"@tmagic/form": "1.3.1",
"monaco-editor": "^0.41.0",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@types/events": "^3.0.0",
@ -77,7 +78,7 @@
"@types/node": "^15.12.4",
"@types/serialize-javascript": "^5.0.1",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.3.2",
"rimraf": "^3.0.2",
"sass": "^1.35.1",

View File

@ -40,6 +40,7 @@ import './theme/index.scss';
export type { OnDrag } from 'gesto';
export type { DepTargetType } from '@tmagic/dep';
export type { MoveableOptions } from '@tmagic/stage';
export * from './type';
export * from './hooks';

View File

@ -1,20 +1,21 @@
import { onUnmounted, toRaw, watch } from 'vue';
import { onUnmounted, reactive, toRaw, watch } from 'vue';
import { cloneDeep } from 'lodash-es';
import type { EventOption } from '@tmagic/core';
import type { CodeBlockContent, DataSourceSchema, Id, MApp, MNode, MPage } from '@tmagic/schema';
import { getNodes } from '@tmagic/utils';
import PropsPanel from './layouts/PropsPanel.vue';
import type { Target } from './services/dep';
import type { Target } from '@tmagic/dep';
import {
createCodeBlockTarget,
createDataSourceCondTarget,
createDataSourceMethodTarget,
createDataSourceTarget,
} from './utils/dep';
DepTargetType,
} from '@tmagic/dep';
import type { CodeBlockContent, DataSourceSchema, Id, MApp, MNode, MPage } from '@tmagic/schema';
import { getNodes } from '@tmagic/utils';
import PropsPanel from './layouts/PropsPanel.vue';
import { EditorProps } from './editorProps';
import { DepTargetType, Services } from './type';
import { Services } from './type';
export declare type LooseRequired<T> = {
[P in string & keyof T]: T[P];
@ -270,9 +271,9 @@ export const initServiceEvents = (
depService.on('collected', collectedHandler);
const initDataSourceDepTarget = (ds: DataSourceSchema) => {
depService.addTarget(createDataSourceTarget(ds.id));
depService.addTarget(createDataSourceMethodTarget(ds.id));
depService.addTarget(createDataSourceCondTarget(ds.id));
depService.addTarget(createDataSourceTarget(ds, reactive({})));
depService.addTarget(createDataSourceMethodTarget(ds, reactive({})));
depService.addTarget(createDataSourceCondTarget(ds, reactive({})));
};
const rootChangeHandler = async (value: MApp | null, preValue?: MApp | null) => {

View File

@ -44,13 +44,14 @@
import { computed, inject, ref } from 'vue';
import { Close, Edit, View } from '@element-plus/icons-vue';
import { DepTargetType } from '@tmagic/dep';
import { tMagicMessage, tMagicMessageBox, TMagicTooltip, TMagicTree } from '@tmagic/design';
import type { Id } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import AppManageIcon from '@editor/icons/AppManageIcon.vue';
import CodeIcon from '@editor/icons/CodeIcon.vue';
import { CodeBlockListSlots, CodeDeleteErrorType, CodeDslItem, DepTargetType, Services } from '@editor/type';
import { type CodeBlockListSlots, CodeDeleteErrorType, type CodeDslItem, type Services } from '@editor/type';
defineSlots<CodeBlockListSlots>();

View File

@ -38,11 +38,12 @@
import { computed, inject, ref } from 'vue';
import { Aim, Close, Coin, Edit, View } from '@element-plus/icons-vue';
import { DepTargetType } from '@tmagic/dep';
import { tMagicMessageBox, TMagicTooltip, TMagicTree } from '@tmagic/design';
import { Dep, Id } from '@tmagic/schema';
import { DepData, Id } from '@tmagic/schema';
import Icon from '@editor/components/Icon.vue';
import { type DataSourceListSlots, DepTargetType, type Services } from '@editor/type';
import type { DataSourceListSlots, Services } from '@editor/type';
defineSlots<DataSourceListSlots>();
@ -65,10 +66,10 @@ const dsDep = computed(() => depService?.getTargets(DepTargetType.DATA_SOURCE) |
const dsMethodDep = computed(() => depService?.getTargets(DepTargetType.DATA_SOURCE_METHOD) || {});
const dsCondDep = computed(() => depService?.getTargets(DepTargetType.DATA_SOURCE_COND) || {});
const getKeyTreeConfig = (dep: Dep[string], type?: string) =>
const getKeyTreeConfig = (dep: DepData[string], type?: string) =>
dep.keys.map((key) => ({ name: key, id: key, type: 'key', isMethod: type === 'method', isCond: type === 'cond' }));
const getNodeTreeConfig = (id: string, dep: Dep[string], type?: string) => ({
const getNodeTreeConfig = (id: string, dep: DepData[string], type?: string) => ({
name: dep.name,
type: 'node',
id,
@ -81,7 +82,7 @@ const getNodeTreeConfig = (id: string, dep: Dep[string], type?: string) => ({
* @param deps 依赖
* @param type 依赖类型
*/
const mergeChildren = (children: any[], deps: Dep, type?: string) => {
const mergeChildren = (children: any[], deps: DepData, type?: string) => {
Object.entries(deps).forEach(([id, dep]) => {
//
const nodeItem = children.find((item) => item.id === id);
@ -97,9 +98,9 @@ const mergeChildren = (children: any[], deps: Dep, type?: string) => {
const list = computed(() =>
dataSources.value.map((ds) => {
const dsDeps = dsDep.value[ds.id].deps;
const dsMethodDeps = dsMethodDep.value[ds.id].deps;
const dsCondDeps = dsCondDep.value[ds.id].deps;
const dsDeps = dsDep.value[ds.id]?.deps || {};
const dsMethodDeps = dsMethodDep.value[ds.id]?.deps || {};
const dsCondDeps = dsCondDep.value[ds.id]?.deps || {};
const children: any[] = [];
// key/nodemethodcond

View File

@ -15,320 +15,58 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import { reactive } from 'vue';
import type { Dep, Id, MNode } from '@tmagic/schema';
import { isObject } from '@tmagic/utils';
import { DepTargetType, type Target, Watcher } from '@tmagic/dep';
import type { Id, MNode } from '@tmagic/schema';
import { DepTargetType } from '@editor/type';
import BaseService from './BaseService';
type IsTarget = (key: string | number, value: any) => boolean;
class Dep extends BaseService {
private watcher = new Watcher({ initialTargets: reactive({}) });
interface TargetOptions {
isTarget: IsTarget;
id: string | number;
/** 类型,数据源、代码块或其他 */
type?: DepTargetType | string;
name?: string;
}
public removeTargets(type: string = DepTargetType.DEFAULT) {
this.watcher.removeTargets(type);
interface TargetList {
[type: DepTargetType | string]: {
[targetId: string | number]: Target;
};
}
/**
*
*
*/
export class Target extends EventEmitter {
/**
*
*/
public isTarget: IsTarget;
/**
* id
* id
*/
public id: string | number;
/**
*
*/
public name?: string;
/**
* type
*/
public type: DepTargetType | string = DepTargetType.DEFAULT;
/**
*
* { 'node_id': { name: 'node_name', keys: [ created, mounted ] } }
*/
public deps = reactive<Dep>({});
constructor(options: TargetOptions) {
super();
this.isTarget = options.isTarget;
this.id = options.id;
this.name = options.name;
if (options.type) {
this.type = options.type;
}
this.emit('remove-target');
}
/**
*
* @param node
* @param key key配置了这个目标的id
*/
public updateDep(node: MNode, key: string | number) {
const dep = this.deps[node.id] || {
name: node.name,
keys: [],
};
if (node.name) {
dep.name = node.name;
}
this.deps[node.id] = dep;
if (dep.keys.indexOf(key) === -1) {
dep.keys.push(key);
}
this.emit('change');
public getTargets(type: string = DepTargetType.DEFAULT) {
return this.watcher.getTargets(type);
}
/**
*
* @param node
* @param key key需要移除key
* @returns void
*/
public removeDep(node?: MNode, key?: string | number) {
if (!node) {
Object.keys(this.deps).forEach((depKey) => {
delete this.deps[depKey];
});
this.emit('change');
return;
}
const dep = this.deps[node.id];
if (!dep) return;
if (key) {
const index = dep.keys.indexOf(key);
dep.keys.splice(index, 1);
if (dep.keys.length === 0) {
delete this.deps[node.id];
}
} else {
delete this.deps[node.id];
}
this.emit('change');
public getTarget(id: Id) {
return this.watcher.getTarget(id);
}
/**
* key是否存在在依赖列表中
* @param node
* @param key key
* @returns boolean
*/
public hasDep(node: MNode, key: string | number) {
const dep = this.deps[node.id];
return Boolean(dep?.keys.find((d) => d === key));
}
public destroy() {
this.removeAllListeners();
}
}
export class Watcher extends EventEmitter {
private targets = reactive<TargetList>({});
/**
* target
* @param type
* @returns Target[]
*/
public getTargets(type: DepTargetType | string = DepTargetType.DEFAULT) {
return this.targets[type] || {};
}
/**
*
* @param target Target
*/
public addTarget(target: Target) {
const targets = this.getTargets(target.type) || {};
this.targets[target.type] = targets;
targets[target.id] = target;
this.watcher.addTarget(target);
this.emit('add-target', target);
}
/**
* id的target
* @param id target id
* @returns Target
*/
public getTarget(id: string | number) {
const allTargets = Object.values(this.targets);
for (const targets of allTargets) {
if (targets[id]) {
return targets[id];
}
}
}
/**
* id的target
* @param id target id
* @returns boolean
*/
public hasTarget(id: string | number) {
const allTargets = Object.values(this.targets);
for (const targets of allTargets) {
if (targets[id]) {
return true;
}
}
return false;
}
/**
* id的target
* @param id target id
*/
public removeTarget(id: string | number) {
const allTargets = Object.values(this.targets);
for (const targets of allTargets) {
if (targets[id]) {
targets[id].destroy();
delete targets[id];
}
}
public removeTarget(id: Id) {
this.watcher.removeTarget(id);
this.emit('remove-target');
}
/**
* target
* @param type
* @returns void
*/
public removeTargets(type: DepTargetType | string = DepTargetType.DEFAULT) {
const targets = this.targets[type];
if (!targets) return;
for (const target of Object.values(targets)) {
target.destroy();
}
delete this.targets[type];
this.emit('remove-target');
}
/**
* target
*/
public clearTargets() {
Object.keys(this.targets).forEach((key) => {
delete this.targets[key];
});
this.watcher.clearTargets();
}
/**
*
* @param nodes
* @param deep
*/
public collect(nodes: MNode[], deep = false) {
Object.values(this.targets).forEach((targets) => {
Object.values(targets).forEach((target) => {
nodes.forEach((node) => {
// 先删除原有依赖,重新收集
target.removeDep(node);
this.collectItem(node, target, deep);
});
});
});
this.watcher.collect(nodes, deep);
this.emit('collected', nodes, deep);
}
/**
*
* @param nodes
*/
public clear(nodes?: MNode[]) {
const clearedItemsNodeIds: Id[] = [];
Object.values(this.targets).forEach((targets) => {
Object.values(targets).forEach((target) => {
if (nodes) {
nodes.forEach((node) => {
target.removeDep(node);
if (Array.isArray(node.items) && node.items.length && !clearedItemsNodeIds.includes(node.id)) {
clearedItemsNodeIds.push(node.id);
this.clear(node.items);
}
});
} else {
target.removeDep();
}
});
});
return this.watcher.clear(nodes);
}
private collectItem(node: MNode, target: Target, deep = false) {
const collectTarget = (config: Record<string | number, any>, prop = '') => {
const doCollect = (key: string, value: any) => {
const keyIsItems = key === 'items';
const fullKey = prop ? `${prop}.${key}` : key;
if (target.isTarget(fullKey, value)) {
target.updateDep(node, fullKey);
this.emit('update-dep', node, fullKey);
} else if (!keyIsItems && Array.isArray(value)) {
value.forEach((item, index) => {
if (isObject(item)) {
collectTarget(item, `${fullKey}.${index}`);
}
});
} else if (isObject(value)) {
collectTarget(value, fullKey);
}
if (keyIsItems && deep && Array.isArray(value)) {
value.forEach((child) => {
this.collectItem(child, target, deep);
});
}
};
Object.entries(config).forEach(([key, value]) => {
if (typeof value === 'undefined' || value === '') return;
doCollect(key, value);
});
};
collectTarget(node);
public hasTarget(id: Id) {
return this.watcher.hasTarget(id);
}
}
export type DepService = Watcher;
export type DepService = Dep;
export default new Watcher();
export default new Dep();

View File

@ -425,13 +425,6 @@ export type CodeState = {
paramsColConfig?: ColumnConfig;
};
export type HookData = {
/** 代码块id */
codeId: Id;
/** 参数 */
params?: object;
};
export type CodeRelation = {
/** 组件id:[代码id1代码id2] */
[compId: Id]: Id[];
@ -594,19 +587,6 @@ export interface DataSourceFieldSelectConfig {
display?: boolean | FilterFunction;
}
/** 依赖收集的目标类型 */
export enum DepTargetType {
DEFAULT = 'default',
/** 代码块 */
CODE_BLOCK = 'code-block',
/** 数据源 */
DATA_SOURCE = 'data-source',
/** 数据源方法 */
DATA_SOURCE_METHOD = 'data-source-method',
/** 数据源条件 */
DATA_SOURCE_COND = 'data-source-cond',
}
/** 可新增的数据源类型选项 */
export interface DatasourceTypeOption {
/** 数据源类型 */

View File

@ -39,12 +39,12 @@
"dependencies": {
"@tmagic/design": "1.3.1",
"element-plus": "^2.2.32",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"peerDependencies": {
"@tmagic/design": "1.3.1",
"element-plus": "^2.2.32",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@types/node": "^15.12.4",

View File

@ -41,10 +41,10 @@
"@tmagic/utils": "1.3.1",
"lodash-es": "^4.17.21",
"sortablejs": "^1.14.0",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"peerDependencies": {
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@babel/core": "^7.18.0",
@ -52,7 +52,7 @@
"@types/node": "^15.12.4",
"@types/sortablejs": "^1.10.7",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.3.2",
"rimraf": "^3.0.2",
"sass": "^1.35.1",

View File

@ -64,7 +64,7 @@ export enum ActionType {
}
export interface DataSourceDeps {
[dataSourceId: string | number]: Dep;
[dataSourceId: string | number]: DepData;
}
/** 事件类型(已废弃,后续不建议继续使用) */
@ -241,10 +241,17 @@ export interface DataSourceSchema {
[key: string]: any;
}
export interface Dep {
export interface DepData {
[nodeId: Id]: {
/** 组件名称 */
name: string;
keys: (string | number)[];
};
}
export type HookData = {
/** 代码块id */
codeId: Id;
/** 参数 */
params?: object;
};

View File

@ -38,18 +38,18 @@
"@tmagic/form": "1.3.1",
"@tmagic/utils": "1.3.1",
"lodash-es": "^4.17.21",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"peerDependencies": {
"@tmagic/form": "1.3.1",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@types/color": "^3.0.1",
"@types/lodash-es": "^4.17.4",
"@types/node": "^15.12.4",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.3.2",
"rimraf": "^3.0.2",
"sass": "^1.35.1",

View File

@ -39,16 +39,16 @@
"dependencies": {
"@tmagic/design": "1.3.1",
"tdesign-vue-next": "^1.3.4",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"peerDependencies": {
"@tmagic/design": "1.3.1",
"tdesign-vue-next": "^1.3.4",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"@types/node": "^15.12.4",
"rimraf": "^3.0.2",
"typescript": "^5.0.4",

View File

@ -18,16 +18,16 @@
"delegate": "^3.2.0",
"qrcode": "^1.5.0",
"tiny-emitter": "^2.1.0",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"peerDependencies": {
"qrcode": "^1.5.0",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@testing-library/vue": "^6.4.2",
"@types/qrcode": "^1.4.2",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.3.2"
}
}

View File

@ -24,7 +24,7 @@
"element-plus": "^2.2.32",
"monaco-editor": "^0.41.0",
"serialize-javascript": "^6.0.0",
"vue": "^3.3.4",
"vue": "^3.3.8",
"vue-router": "^4.0.10"
},
"devDependencies": {
@ -34,7 +34,7 @@
"@vitejs/plugin-legacy": "^4.1.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"sass": "^1.35.1",
"terser": "^5.14.2",
"typescript": "^5.0.4",

View File

@ -59,6 +59,7 @@ export default defineConfig({
{ find: /^@tmagic\/utils/, replacement: path.join(__dirname, '../packages/utils/src/index.ts') },
{ find: /^@tmagic\/design/, replacement: path.join(__dirname, '../packages/design/src/index.ts') },
{ find: /^@tmagic\/data-source/, replacement: path.join(__dirname, '../packages/data-source/src/index.ts') },
{ find: /^@tmagic\/dep/, replacement: path.join(__dirname, '../packages/dep/src/index.ts') },
{ find: /^@data-source/, replacement: path.join(__dirname, '../packages/data-source/src') },
{
find: /^@tmagic\/element-plus-adapter/,

540
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@
"@tmagic/stage": "1.3.1",
"@tmagic/utils": "1.3.1",
"axios": "^0.25.0",
"vue": "^3.3.4"
"vue": "^3.3.8"
},
"devDependencies": {
"@babel/preset-env": "^7.21.4",
@ -34,7 +34,7 @@
"@vitejs/plugin-legacy": "^4.1.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/compiler-sfc": "^3.3.4",
"@vue/compiler-sfc": "^3.3.8",
"@vue/test-utils": "^2.3.2",
"recast": "^0.20.4",
"rollup": "^2.25.0",

View File

@ -22,6 +22,7 @@ export default defineConfig({
'./packages/stage/tests/**',
'./packages/utils/tests/**',
'./packages/data-source/tests/**',
'./packages/dep/tests/**',
],
environment: 'jsdom',
},