mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-30 20:28:04 +00:00
将目标历史步骤的修改作为一次新操作反向应用,不破坏原有栈结构, page/dataSource/codeBlock 三类 service 均提供 revert 能力; 面板新增关闭按钮、步骤编号展示与合并组卡片样式优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
579 lines
19 KiB
TypeScript
579 lines
19 KiB
TypeScript
/*
|
||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||
*
|
||
* Copyright (C) 2025 Tencent. 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 { reactive } from 'vue';
|
||
import { cloneDeep, get, keys, pick } from 'lodash-es';
|
||
import type { Writable } from 'type-fest';
|
||
|
||
import type { CodeBlockContent, CodeBlockDSL, Id, MNode, TargetOptions } from '@tmagic/core';
|
||
import { Target, Watcher } from '@tmagic/core';
|
||
import type { TableColumnConfig } from '@tmagic/form';
|
||
import { getValueByKeyPath, setValueByKeyPath } from '@tmagic/utils';
|
||
|
||
import editorService from '@editor/services/editor';
|
||
import historyService from '@editor/services/history';
|
||
import storageService, { Protocol } from '@editor/services/storage';
|
||
import type {
|
||
AsyncHookPlugin,
|
||
CodeBlockStepValue,
|
||
CodeState,
|
||
HistoryOpOptions,
|
||
HistoryOpOptionsWithChangeRecords,
|
||
} from '@editor/type';
|
||
import { CODE_DRAFT_STORAGE_KEY } from '@editor/type';
|
||
import { getEditorConfig } from '@editor/utils/config';
|
||
import { COPY_CODE_STORAGE_KEY } from '@editor/utils/editor';
|
||
|
||
import BaseService from './BaseService';
|
||
|
||
const canUsePluginMethods = {
|
||
async: ['setCodeDslById', 'setEditStatus', 'setCombineIds', 'setUndeleteableList', 'deleteCodeDslByIds'] as const,
|
||
sync: ['setCodeDslByIdSync'],
|
||
};
|
||
|
||
type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
|
||
|
||
/**
|
||
* 「回滚」生成的新 step 简短描述。仅 service 层使用。
|
||
*/
|
||
const describeRevertCodeBlockStep = (step: CodeBlockStepValue): string => {
|
||
const { oldContent, newContent, changeRecords, id } = step;
|
||
if (oldContent === null && newContent) return `撤回新增 ${newContent.name || newContent.id || id}`;
|
||
if (oldContent && newContent === null) return `还原已删除的 ${oldContent.name || oldContent.id || id}`;
|
||
const name = newContent?.name || oldContent?.name || `${id}`;
|
||
const propPath = changeRecords?.[0]?.propPath;
|
||
return propPath ? `还原 ${name} · ${propPath}` : `还原 ${name}`;
|
||
};
|
||
|
||
class CodeBlock extends BaseService {
|
||
private state = reactive<CodeState>({
|
||
codeDsl: null,
|
||
editable: true,
|
||
combineIds: [],
|
||
undeletableList: [],
|
||
paramsColConfig: undefined,
|
||
});
|
||
|
||
constructor() {
|
||
super([
|
||
...canUsePluginMethods.async.map((methodName) => ({ name: methodName, isAsync: true })),
|
||
...canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false })),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 设置活动的代码块dsl数据源
|
||
* @param {CodeBlockDSL} codeDsl 代码DSL
|
||
* @returns {void}
|
||
*/
|
||
public async setCodeDsl(codeDsl: CodeBlockDSL): Promise<void> {
|
||
this.state.codeDsl = codeDsl;
|
||
this.emit('code-dsl-change', this.state.codeDsl);
|
||
}
|
||
|
||
/**
|
||
* 获取活动的代码块dsl数据源(默认从dsl中的codeBlocks字段读取)
|
||
* 方法要支持钩子添加扩展,会被重写为异步方法,因此这里显示写为异步以提醒调用者需以异步形式调用
|
||
* @param {boolean} forceRefresh 是否强制从活动dsl拉取刷新
|
||
* @returns {CodeBlockDSL | null}
|
||
*/
|
||
public getCodeDsl(): CodeBlockDSL | null {
|
||
return this.state.codeDsl;
|
||
}
|
||
|
||
/**
|
||
* 根据代码块id获取代码块内容
|
||
* @param {Id} id 代码块id
|
||
* @returns {CodeBlockContent | null}
|
||
*/
|
||
public getCodeContentById(id: Id): CodeBlockContent | null {
|
||
if (!id) return null;
|
||
const totalCodeDsl = this.getCodeDsl();
|
||
if (!totalCodeDsl) return null;
|
||
return totalCodeDsl[id] ?? null;
|
||
}
|
||
|
||
/**
|
||
* 设置代码块ID和代码内容到源dsl
|
||
* @param {Id} id 代码块id
|
||
* @param {CodeBlockContent} codeConfig 代码块内容配置信息
|
||
* @param options 可选配置
|
||
* @param options.changeRecords form 端 propPath/value 列表,用于历史记录的精细化撤销/重做
|
||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||
* @returns {void}
|
||
*/
|
||
public async setCodeDslById(
|
||
id: Id,
|
||
codeConfig: Partial<CodeBlockContent>,
|
||
{ changeRecords, doNotPushHistory = false }: HistoryOpOptionsWithChangeRecords = {},
|
||
): Promise<void> {
|
||
this.setCodeDslByIdSync(id, codeConfig, true, { changeRecords, doNotPushHistory });
|
||
}
|
||
|
||
/**
|
||
* 为了兼容历史原因
|
||
* 设置代码块ID和代码内容到源dsl
|
||
* @param {Id} id 代码块id
|
||
* @param {CodeBlockContent} codeConfig 代码块内容配置信息
|
||
* @param {boolean} force 是否强制写入,默认true
|
||
* @param options 可选配置
|
||
* @param options.changeRecords form 端 propPath/value 列表,用于历史记录的精细化撤销/重做
|
||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||
* @param options.historyDescription 入栈时附带的人类可读描述,用于历史面板展示
|
||
* @returns {void}
|
||
*/
|
||
public setCodeDslByIdSync(
|
||
id: Id,
|
||
codeConfig: Partial<CodeBlockContent>,
|
||
force = true,
|
||
{ changeRecords, doNotPushHistory = false, historyDescription }: HistoryOpOptionsWithChangeRecords = {},
|
||
): void {
|
||
const codeDsl = this.getCodeDsl();
|
||
|
||
if (!codeDsl) {
|
||
throw new Error('dsl中没有codeBlocks');
|
||
}
|
||
if (codeDsl[id] && !force) return;
|
||
|
||
const codeConfigProcessed = cloneDeep(codeConfig);
|
||
if (codeConfigProcessed.content) {
|
||
// 在保存的时候转换代码内容
|
||
const parseDSL = getEditorConfig('parseDSL');
|
||
if (typeof codeConfigProcessed.content === 'string') {
|
||
codeConfigProcessed.content = parseDSL<(...args: any[]) => any>(codeConfigProcessed.content);
|
||
}
|
||
}
|
||
|
||
// 历史记录:在写入前快照旧内容,区分新增/更新
|
||
const oldContent: CodeBlockContent | null = codeDsl[id] ? cloneDeep(codeDsl[id]) : null;
|
||
|
||
const existContent = codeDsl[id] || {};
|
||
|
||
codeDsl[id] = {
|
||
...existContent,
|
||
...codeConfigProcessed,
|
||
};
|
||
|
||
const newContent = cloneDeep(codeDsl[id]);
|
||
|
||
if (!doNotPushHistory) {
|
||
historyService.pushCodeBlock(id, { oldContent, newContent, changeRecords, historyDescription });
|
||
}
|
||
|
||
this.emit('addOrUpdate', id, codeDsl[id]);
|
||
}
|
||
|
||
/**
|
||
* 根据代码块id数组获取代码dsl
|
||
* @param {string[]} ids 代码块id数组
|
||
* @returns {CodeBlockDSL}
|
||
*/
|
||
public getCodeDslByIds(ids: string[]): CodeBlockDSL {
|
||
const codeDsl = this.getCodeDsl();
|
||
return pick(codeDsl, ids) as CodeBlockDSL;
|
||
}
|
||
|
||
/**
|
||
* 获取编辑状态
|
||
* @returns {boolean} 是否可编辑
|
||
*/
|
||
public getEditStatus(): boolean {
|
||
return this.state.editable;
|
||
}
|
||
|
||
/**
|
||
* 设置编辑状态
|
||
* @param {boolean} status 是否可编辑
|
||
* @returns {void}
|
||
*/
|
||
public async setEditStatus(status: boolean): Promise<void> {
|
||
this.state.editable = status;
|
||
}
|
||
|
||
/**
|
||
* 设置当前选中组件已关联绑定的代码块id数组
|
||
* @param {string[]} ids 代码块id数组
|
||
* @returns {void}
|
||
*/
|
||
public async setCombineIds(ids: string[]): Promise<void> {
|
||
this.state.combineIds = ids;
|
||
}
|
||
|
||
/**
|
||
* 获取当前选中组件已关联绑定的代码块id数组
|
||
* @returns {string[]}
|
||
*/
|
||
public getCombineIds(): string[] {
|
||
return this.state.combineIds;
|
||
}
|
||
|
||
/**
|
||
* 获取不可删除列表
|
||
* @returns {Id[]}
|
||
*/
|
||
public getUndeletableList(): Id[] {
|
||
return this.state.undeletableList;
|
||
}
|
||
|
||
/**
|
||
* 设置不可删除列表:为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除)
|
||
* @param {Id[]} codeIds 代码块id数组
|
||
* @returns {void}
|
||
*/
|
||
public async setUndeleteableList(codeIds: Id[]): Promise<void> {
|
||
this.state.undeletableList = codeIds;
|
||
}
|
||
|
||
/**
|
||
* 设置代码草稿
|
||
*/
|
||
public setCodeDraft(codeId: Id, content: string): void {
|
||
globalThis.localStorage.setItem(`${CODE_DRAFT_STORAGE_KEY}_${codeId}`, content);
|
||
}
|
||
|
||
/**
|
||
* 获取代码草稿
|
||
*/
|
||
public getCodeDraft(codeId: Id): string | null {
|
||
return globalThis.localStorage.getItem(`${CODE_DRAFT_STORAGE_KEY}_${codeId}`);
|
||
}
|
||
|
||
/**
|
||
* 删除代码草稿
|
||
*/
|
||
public removeCodeDraft(codeId: Id): void {
|
||
globalThis.localStorage.removeItem(`${CODE_DRAFT_STORAGE_KEY}_${codeId}`);
|
||
}
|
||
|
||
/**
|
||
* 在dsl数据源中删除指定id的代码块
|
||
* @param {Id[]} codeIds 需要删除的代码块id数组
|
||
* @param options 可选配置
|
||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||
*/
|
||
public async deleteCodeDslByIds(
|
||
codeIds: Id[],
|
||
{ doNotPushHistory = false, historyDescription }: HistoryOpOptions = {},
|
||
): Promise<void> {
|
||
const currentDsl = await this.getCodeDsl();
|
||
|
||
if (!currentDsl) return;
|
||
|
||
codeIds.forEach((id) => {
|
||
// 历史记录:删除前快照内容;不存在的 id 直接跳过历史推入
|
||
const oldContent: CodeBlockContent | null = currentDsl[id] ? cloneDeep(currentDsl[id]) : null;
|
||
|
||
delete currentDsl[id];
|
||
|
||
if (oldContent && !doNotPushHistory) {
|
||
historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription });
|
||
}
|
||
|
||
this.emit('remove', id);
|
||
});
|
||
}
|
||
|
||
public setParamsColConfig(config: TableColumnConfig): void {
|
||
this.state.paramsColConfig = config;
|
||
}
|
||
|
||
public getParamsColConfig(): TableColumnConfig | undefined {
|
||
return this.state.paramsColConfig;
|
||
}
|
||
|
||
/**
|
||
* 撤销指定代码块的最近一次变更。
|
||
*
|
||
* 内部走 setCodeDslByIdSync / deleteCodeDslByIds,因此会自动触发 codeBlockService 的
|
||
* `addOrUpdate` / `remove` 事件,由 initService 中的 handler 重新维护 dep target
|
||
* (DepTargetType.CODE_BLOCK 的 add / remove)。所有写回都带 `doNotPushHistory: true`,
|
||
* 确保不会在历史栈里产生新的记录。
|
||
*
|
||
* @param id 代码块 id
|
||
* @returns 撤销的 step;栈不存在或已无可撤销时返回 null
|
||
*/
|
||
public async undo(id: Id): Promise<CodeBlockStepValue | null> {
|
||
const step = historyService.undoCodeBlock(id);
|
||
if (!step) return null;
|
||
await this.applyHistoryStep(step, true);
|
||
return step;
|
||
}
|
||
|
||
/**
|
||
* 重做指定代码块的下一次变更。
|
||
* @param id 代码块 id
|
||
* @returns 重做的 step;栈不存在或已无可重做时返回 null
|
||
*/
|
||
public async redo(id: Id): Promise<CodeBlockStepValue | null> {
|
||
const step = historyService.redoCodeBlock(id);
|
||
if (!step) return null;
|
||
await this.applyHistoryStep(step, false);
|
||
return step;
|
||
}
|
||
|
||
/** 是否可对指定代码块撤销。 */
|
||
public canUndo(id: Id): boolean {
|
||
return historyService.canUndoCodeBlock(id);
|
||
}
|
||
|
||
/** 是否可对指定代码块重做。 */
|
||
public canRedo(id: Id): boolean {
|
||
return historyService.canRedoCodeBlock(id);
|
||
}
|
||
|
||
/**
|
||
* 跳转指定代码块的历史栈到目标游标。语义同 editor.gotoPageStep。
|
||
*
|
||
* @param id 代码块 id
|
||
* @param targetCursor 目标游标位置(已应用步骤数量)
|
||
* @returns 实际移动到的最终游标位置
|
||
*/
|
||
public async goto(id: Id, targetCursor: number): Promise<number> {
|
||
let cursor = historyService.getCodeBlockCursor(id);
|
||
const target = Math.max(0, targetCursor);
|
||
while (cursor > target) {
|
||
const step = await this.undo(id);
|
||
if (!step) break;
|
||
cursor -= 1;
|
||
}
|
||
while (cursor < target) {
|
||
const step = await this.redo(id);
|
||
if (!step) break;
|
||
cursor += 1;
|
||
}
|
||
return cursor;
|
||
}
|
||
|
||
/**
|
||
* 「回滚」指定代码块历史步骤(类 git revert 语义):
|
||
* - 不动原始栈结构(不移动 cursor、不丢弃任何步骤);
|
||
* - 把目标 step 的修改**反向应用**一次,并作为**新步骤**追加到栈顶;
|
||
* - 仅对已应用的步骤生效。
|
||
*
|
||
* @param id 代码块 id
|
||
* @param index 目标 step 在该栈中的索引(0 为最早),通常由历史面板传入
|
||
* @returns 反向后产生的新 step;目标不存在 / 未应用时返回 null
|
||
*/
|
||
public async revert(id: Id, index: number): Promise<CodeBlockStepValue | null> {
|
||
const list = historyService.getCodeBlockStepList(id);
|
||
const entry = list[index];
|
||
if (!entry?.applied) return null;
|
||
const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
|
||
return await this.applyRevertStep(entry.step, description);
|
||
}
|
||
|
||
/**
|
||
* 生成代码块唯一id
|
||
* @returns {Id} 代码块唯一id
|
||
*/
|
||
public async getUniqueId(): Promise<string> {
|
||
const newId = `code_${Math.random().toString(10).substring(2).substring(0, 4)}`;
|
||
// 判断是否重复
|
||
const dsl = await this.getCodeDsl();
|
||
const existedIds = keys(dsl);
|
||
if (!existedIds.includes(newId)) return newId;
|
||
return await this.getUniqueId();
|
||
}
|
||
|
||
/**
|
||
* 复制时会带上组件关联的代码块
|
||
* @param config 组件节点配置
|
||
* @returns
|
||
*/
|
||
public copyWithRelated(config: MNode | MNode[], collectorOptions?: TargetOptions): void {
|
||
const copyNodes: MNode[] = Array.isArray(config) ? config : [config];
|
||
const copyData: CodeBlockDSL = {};
|
||
|
||
if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
|
||
const customTarget = new Target({
|
||
...collectorOptions,
|
||
});
|
||
|
||
const coperWatcher = new Watcher();
|
||
|
||
coperWatcher.addTarget(customTarget);
|
||
|
||
coperWatcher.collect(copyNodes, {}, true, collectorOptions.type);
|
||
|
||
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
|
||
const node = editorService.getNodeById(nodeId);
|
||
if (!node) return;
|
||
customTarget!.deps[nodeId].keys.forEach((key) => {
|
||
const relateCodeId = get(node, key);
|
||
const isExist = Object.keys(copyData).find((codeId: Id) => codeId === relateCodeId);
|
||
if (!isExist) {
|
||
const relateCode = this.getCodeContentById(relateCodeId);
|
||
if (relateCode) {
|
||
copyData[relateCodeId] = relateCode;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
storageService.setItem(COPY_CODE_STORAGE_KEY, copyData, {
|
||
protocol: Protocol.OBJECT,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 粘贴代码块
|
||
* @returns
|
||
*/
|
||
public paste() {
|
||
const codeDsl: CodeBlockDSL = storageService.getItem(COPY_CODE_STORAGE_KEY);
|
||
Object.keys(codeDsl).forEach((codeId: Id) => {
|
||
// 不覆盖同样id的代码块
|
||
this.setCodeDslByIdSync(codeId, codeDsl[codeId], false);
|
||
});
|
||
}
|
||
|
||
public resetState() {
|
||
this.state.codeDsl = null;
|
||
this.state.editable = true;
|
||
this.state.combineIds = [];
|
||
this.state.undeletableList = [];
|
||
}
|
||
|
||
public destroy(): void {
|
||
this.resetState();
|
||
this.removeAllListeners();
|
||
this.removeAllPlugins();
|
||
}
|
||
|
||
public usePlugin(options: AsyncHookPlugin<AsyncMethodName, CodeBlock>): void {
|
||
super.usePlugin(options);
|
||
}
|
||
|
||
/**
|
||
* 反向应用一个 step 并以新 step 入栈。逻辑与 applyHistoryStep(reverse=true) 同构,
|
||
* 差异仅在于通过公开的 setCodeDslByIdSync / deleteCodeDslByIds 触发 push。
|
||
*/
|
||
private async applyRevertStep(
|
||
step: CodeBlockStepValue,
|
||
historyDescription: string,
|
||
): Promise<CodeBlockStepValue | null> {
|
||
const { id, oldContent, newContent, changeRecords } = step;
|
||
|
||
// 原本是新增 → revert 即删除
|
||
if (oldContent === null && newContent) {
|
||
await this.deleteCodeDslByIds([id], { historyDescription });
|
||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||
}
|
||
|
||
// 原本是删除 → revert 即写回
|
||
if (oldContent && newContent === null) {
|
||
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
|
||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||
}
|
||
|
||
if (!oldContent || !newContent) return null;
|
||
|
||
// 原本是更新 → 把 oldContent 写回;优先按 changeRecords 局部 patch
|
||
if (changeRecords?.length) {
|
||
const current = this.getCodeContentById(id);
|
||
if (!current) return null;
|
||
const patched = cloneDeep(current) as CodeBlockContent;
|
||
let fallbackToFullReplace = false;
|
||
for (const record of changeRecords) {
|
||
if (!record.propPath) {
|
||
fallbackToFullReplace = true;
|
||
break;
|
||
}
|
||
const value = cloneDeep(getValueByKeyPath(record.propPath, oldContent));
|
||
setValueByKeyPath(record.propPath, value, patched);
|
||
}
|
||
this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(oldContent) : patched, true, {
|
||
changeRecords,
|
||
historyDescription,
|
||
});
|
||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||
}
|
||
|
||
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
|
||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||
}
|
||
|
||
/**
|
||
* 把一条历史 step 应用到当前代码块服务上。
|
||
*
|
||
* 复用现有的 setCodeDslByIdSync / deleteCodeDslByIds,目的是借助它们发出的事件
|
||
* 触发 initService 中的 dep target 维护(CODE_BLOCK 的 add / remove)。
|
||
* 所有写回都带 `doNotPushHistory: true`,确保不会在历史栈里产生新的记录。
|
||
*
|
||
* - oldContent=null, newContent≠null:原始为新增 → undo 删除;redo 再次 setCodeDslByIdSync
|
||
* - oldContent≠null, newContent=null:原始为删除 → undo 还原写入;redo 再次删除
|
||
* - 两侧都有:原始为更新 → 按 changeRecords 局部 patch;缺省退化为整内容替换
|
||
*
|
||
* @param step 历史 step
|
||
* @param reverse true=撤销,false=重做
|
||
*/
|
||
private async applyHistoryStep(step: CodeBlockStepValue, reverse: boolean): Promise<void> {
|
||
const { id, oldContent, newContent, changeRecords } = step;
|
||
|
||
// 新增 / 删除:直接 set 或 delete,不走 patch 逻辑
|
||
if (oldContent === null && newContent) {
|
||
if (reverse) {
|
||
await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
|
||
} else {
|
||
this.setCodeDslByIdSync(id, cloneDeep(newContent), true, { doNotPushHistory: true });
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (oldContent && newContent === null) {
|
||
if (reverse) {
|
||
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { doNotPushHistory: true });
|
||
} else {
|
||
await this.deleteCodeDslByIds([id], { doNotPushHistory: true });
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!oldContent || !newContent) return;
|
||
|
||
// 更新场景:优先按 changeRecords 局部 patch;缺省退化为整内容替换
|
||
const sourceForValues = reverse ? oldContent : newContent;
|
||
|
||
if (changeRecords?.length) {
|
||
const current = this.getCodeContentById(id);
|
||
if (!current) return;
|
||
const patched = cloneDeep(current) as CodeBlockContent;
|
||
let fallbackToFullReplace = false;
|
||
for (const record of changeRecords) {
|
||
if (!record.propPath) {
|
||
fallbackToFullReplace = true;
|
||
break;
|
||
}
|
||
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
||
setValueByKeyPath(record.propPath, value, patched);
|
||
}
|
||
this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(sourceForValues) : patched, true, {
|
||
changeRecords,
|
||
doNotPushHistory: true,
|
||
});
|
||
return;
|
||
}
|
||
|
||
this.setCodeDslByIdSync(id, cloneDeep(sourceForValues), true, { doNotPushHistory: true });
|
||
}
|
||
}
|
||
|
||
export type CodeBlockService = CodeBlock;
|
||
|
||
export default new CodeBlock();
|