chore: import upstream snapshot with attribution
CI / build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:09:51 +08:00
commit 0232b4e2bb
3528 changed files with 291404 additions and 0 deletions
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FormModel } from '@flowgram.ai/form';
import { FlowDocumentContainerModule } from '@flowgram.ai/document';
import { loadPlugins, Playground, PlaygroundMockTools } from '@flowgram.ai/core';
import { createHistoryPlugin, HistoryService } from '@flowgram.ai/history';
import { attachFormValuesChange } from '../src/utils';
import { createHistoryNodePlugin } from '../src';
export const createContainer = () => {
const container = PlaygroundMockTools.createContainer([FlowDocumentContainerModule]);
const formModel = new FormModel();
const playground = container.get(Playground);
loadPlugins([createHistoryPlugin({ enable: true }), createHistoryNodePlugin({})], container);
playground.init();
const historyService = container.get(HistoryService);
historyService.context.source = container;
attachFormValuesChange(formModel as any, { id: 1 } as any, historyService);
return {
formModel,
container,
historyService,
};
};
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { beforeEach, describe, it, expect, vi } from 'vitest';
import { FieldArrayModel, FormModel } from '@flowgram.ai/form';
import { HistoryService } from '@flowgram.ai/history';
import * as utils from '../src/utils';
import { createContainer } from './create-container';
function delay(timeout: number) {}
describe('form', () => {
let formModel: FormModel;
let historyService: HistoryService;
beforeEach(() => {
const container = createContainer();
formModel = container.formModel;
historyService = container.historyService;
vi.spyOn(utils, 'getFormModelV2').mockImplementation(() => formModel as any);
vi.useFakeTimers();
});
it('object set', async () => {
const obj = formModel.createField('obj');
const fieldA = formModel.createField('obj.a');
fieldA.value = 1;
vi.advanceTimersByTime(500);
fieldA.value = 2;
obj.value = { a: 3 };
await historyService.undo();
expect(obj.value).toEqual({ a: 1 });
await historyService.redo();
expect(obj.value).toEqual({ a: 3 });
});
it('array delete', async () => {
formModel.createFieldArray('arr');
const arrField = formModel.getField<FieldArrayModel>('arr')!;
expect(arrField.value).toEqual(undefined);
arrField.value = ['a', 'b', 'c'];
expect(arrField.value).toEqual(['a', 'b', 'c']);
vi.advanceTimersByTime(500);
arrField.delete(1);
expect(arrField.value).toEqual(['a', 'c']);
await historyService.undo();
expect(arrField.value).toEqual(['a', 'b', 'c']);
await historyService.redo();
expect(arrField.value).toEqual(['a', 'c']);
});
it('array append', async () => {
formModel.createFieldArray('arr');
const arrField = formModel.getField<FieldArrayModel>('arr')!;
arrField.value = ['a'];
expect(arrField.value).toEqual(['a']);
await historyService.undo();
expect(arrField.value).toEqual(undefined);
await historyService.redo();
expect(arrField.value).toEqual(['a']);
vi.advanceTimersByTime(500);
arrField.append('b');
expect(arrField.value).toEqual(['a', 'b']);
await historyService.undo();
expect(arrField.value).toEqual(['a']);
await historyService.redo();
expect(arrField.value).toEqual(['a', 'b']);
});
it('array set', async () => {
formModel.createFieldArray('arr');
const arrField = formModel.getField<FieldArrayModel>('arr')!;
arrField.value = ['a'];
expect(arrField.value).toEqual(['a']);
vi.advanceTimersByTime(500);
formModel.setValueIn('arr.0', undefined);
expect(arrField.value).toEqual([undefined]);
await historyService.undo();
expect(arrField.value).toEqual(['a']);
await historyService.redo();
expect(arrField.value).toEqual([undefined]);
});
});
@@ -0,0 +1,11 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
const { defineFlatConfig } = require('@flowgram.ai/eslint-config');
module.exports = defineFlatConfig({
preset: 'web',
packageRoot: __dirname,
});
@@ -0,0 +1,62 @@
{
"name": "@flowgram.ai/history-node-plugin",
"version": "0.1.8",
"homepage": "https://flowgram.ai/",
"repository": "https://github.com/bytedance/flowgram.ai",
"license": "MIT",
"exports": {
"types": "./dist/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/index.js"
},
"main": "./dist/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "npm run build:fast -- --dts-resolve",
"build:fast": "tsup src/index.ts --format cjs,esm --sourcemap --legacy-output",
"build:watch": "npm run build:fast -- --dts-resolve",
"clean": "rimraf dist",
"test": "vitest run",
"test:cov": "vitest run --coverage",
"ts-check": "tsc --noEmit",
"watch": "npm run build:fast -- --dts-resolve --watch --ignore-watch dist"
},
"dependencies": {
"@flowgram.ai/core": "workspace:*",
"@flowgram.ai/document": "workspace:*",
"@flowgram.ai/form": "workspace:*",
"@flowgram.ai/form-core": "workspace:*",
"@flowgram.ai/history": "workspace:*",
"@flowgram.ai/node": "workspace:*",
"inversify": "^6.0.1",
"reflect-metadata": "~0.2.2",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@flowgram.ai/eslint-config": "workspace:*",
"@flowgram.ai/ts-config": "workspace:*",
"@types/bezier-js": "4.1.3",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitest/coverage-v8": "^3.2.4",
"eslint": "^9.0.0",
"react": "^18",
"react-dom": "^18",
"tsup": "^8.0.1",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { FlowDocument } from '@flowgram.ai/document';
import { bindContributions, definePluginCreator } from '@flowgram.ai/core';
import { HistoryContainerModule, HistoryService, OperationContribution } from '@flowgram.ai/history';
import { attachFormValuesChange, getFormModelV2 } from './utils';
import { HistoryNodeRegisters } from './history-node-registers';
/**
* 表单历史插件
*/
export const createHistoryNodePlugin = definePluginCreator({
onBind: ({ bind }) => {
bindContributions(bind, HistoryNodeRegisters, [OperationContribution]);
},
onInit: (ctx, _opts) => {
const document = ctx.get<FlowDocument>(FlowDocument);
const historyService = ctx.get<HistoryService>(HistoryService);
document.onNodeCreate(({ node }) => {
const formModel = getFormModelV2(node);
if (!formModel) {
return;
}
attachFormValuesChange(formModel, node, historyService);
});
},
containerModules: [HistoryContainerModule],
});
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { injectable } from 'inversify';
import { type OperationContribution, type OperationRegistry } from '@flowgram.ai/history';
import { operationMetas } from './operation-metas';
/**
* 表单历史操作
*/
@injectable()
export class HistoryNodeRegisters implements OperationContribution {
registerOperationMeta(operationRegistry: OperationRegistry): void {
operationMetas.forEach(operationMeta => {
operationRegistry.registerOperationMeta(operationMeta);
});
}
}
@@ -0,0 +1,6 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export * from './create-history-node-plugin';
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { type OperationMeta } from '@flowgram.ai/history';
import { FlowDocument } from '@flowgram.ai/document';
import { type PluginContext } from '@flowgram.ai/core';
import { getFormModelV2, shouldChangeFormValuesMerge } from '../utils';
import { ChangeFormValuesOperationValue, NodeOperationType } from '../types';
/**
* 表单修改操作
*/
export const changeFormValueOperationMeta: OperationMeta<
ChangeFormValuesOperationValue,
PluginContext,
void
> = {
type: NodeOperationType.changeFormValues,
inverse: (op) => ({
...op,
value: {
...op.value,
value: op.value.oldValue,
oldValue: op.value.value,
},
}),
apply: ({ value: { value, path, id } }, ctx: PluginContext) => {
const document = ctx.get<FlowDocument>(FlowDocument);
const formModel = getFormModelV2(document.getNode(id));
if (!formModel) {
return;
}
if (!path) {
formModel.updateFormValues(value);
} else {
formModel.setValueIn(path, value);
}
},
shouldMerge: shouldChangeFormValuesMerge as OperationMeta['shouldMerge'],
};
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { OperationMeta } from '@flowgram.ai/history';
import { changeFormValueOperationMeta } from './change-form-values';
export const operationMetas: OperationMeta[] = [changeFormValueOperationMeta];
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
export enum NodeOperationType {
changeFormValues = 'changeFormValues',
}
export interface ChangeFormValuesOperationValue {
id: string;
path: string;
value: unknown;
oldValue: unknown;
}
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import { get } from 'lodash-es';
import { FormModelV2, isFormModelV2 } from '@flowgram.ai/node';
import { HistoryService, Operation } from '@flowgram.ai/history';
import { StackOperation } from '@flowgram.ai/history';
import { FlowNodeFormData } from '@flowgram.ai/form-core';
import { FlowNodeEntity } from '@flowgram.ai/document';
import { ChangeFormValuesOperationValue, NodeOperationType } from '../types';
/**
* 获取v2版本的formModel
* @param node 节点
* @returns
*/
export function getFormModelV2(node: FlowNodeEntity | undefined): FormModelV2 | undefined {
if (!node) {
return undefined;
}
const formModel = node?.getData(FlowNodeFormData)?.getFormModel<FormModelV2>();
if (!formModel || !isFormModelV2(formModel)) {
return undefined;
}
return formModel;
}
/**
* 表单合并策略
* @param op 操作
* @param prev 上一个操作
* @param element 操作栈元素
* @returns
*/
export function shouldChangeFormValuesMerge(
op: Operation<ChangeFormValuesOperationValue | undefined>,
prev: Operation<ChangeFormValuesOperationValue | undefined>,
element: StackOperation
) {
if (!prev) {
return false;
}
if (Date.now() - element.getTimestamp() < 500) {
if (
op.type === prev.type && // 相同类型
op.value?.id === prev.value?.id && // 相同节点
op.value?.path === prev.value?.path // 相同路径
) {
return {
type: op.type,
value: {
...op.value,
value: op.value?.value,
oldValue: prev.value?.oldValue,
},
};
}
return true;
}
return false;
}
/**
* 监听表单值变化
* @param formModel 表单模型
* @param node 节点
* @param historyService 历史服务
*/
export function attachFormValuesChange(
formModel: FormModelV2,
node: FlowNodeEntity,
historyService: HistoryService
) {
formModel.onFormValuesChange((event) => {
historyService.pushOperation(
{
type: NodeOperationType.changeFormValues,
value: {
id: node.id,
path: event.name,
value: event.name ? get(event.values, event.name) : event.values,
oldValue: event.name ? get(event.prevValues, event.name) : event.prevValues,
},
},
{ noApply: true }
);
});
}
@@ -0,0 +1,7 @@
{
"extends": "@flowgram.ai/ts-config/tsconfig.flow.path.json",
"compilerOptions": {
},
"include": ["./src"],
"exclude": ["node_modules"]
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
const path = require('path');
import { defineConfig } from 'vitest/config';
export default defineConfig({
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
globals: true,
mockReset: false,
environment: 'jsdom',
setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],
include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],
exclude: [
'**/__mocks__**',
'**/node_modules/**',
'**/dist/**',
'**/lib/**', // lib 编译结果忽略掉
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
],
},
});
@@ -0,0 +1,6 @@
/**
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*/
import 'reflect-metadata';