Compare commits

...

25 Commits

Author SHA1 Message Date
roymondchen
4ffda3ddd5 chore: update lockfile v1.8.0-beta.17 2026-07-27 17:40:24 +08:00
roymondchen
a0b5702330 chore: release v1.8.0-beta.17 2026-07-27 17:39:05 +08:00
roymondchen
e6cf694a03 feat(editor,stage): 新增 containerHighlightAddOnly 配置
开启后仅新增组件(从组件列表拖入)时才识别容器,画布中拖动已有组件不再被加入其他容器,避免误操作
2026-07-27 17:33:29 +08:00
manmanyu
be7b38f97e style(design,editor,form): 事件模块处理 & 卡片样式调整 2026-07-27 08:55:36 +00:00
manmanyu
dec4ebfa20 fix(form,table): groupList分界线、空状态、actions只存在一个
TAPD: --story=134235103
2026-07-27 06:52:18 +00:00
roymondchen
596d86f2ab fix(table): 列操作点击后关闭popover 2026-07-27 12:51:08 +08:00
roymondchen
b564c9e4aa fix(editor): 切换动作类型时清空关联配置字段 2026-07-24 17:17:55 +08:00
roymondchen
b5abf31066 fix(editor): 修复 collectIdle 批次结算与中断时 Promise 挂起问题
引入批次级 Promise 结算,避免快速连续触发或 clearIdleTasks 后 collecting 卡死。
collectIdle 返回 boolean 表示是否完整完成,initService 据此跳过被中断的 stage 更新。
2026-07-24 17:17:55 +08:00
roymondchen
bf8df74864 fix(form): 完善 mountFormInstance 清理逻辑,避免临时表单实例泄漏
补充 mount 全流程异常清理、非正数 timeout 兜底,并支持 AbortSignal 主动中断 debug 弹层。
2026-07-24 16:51:42 +08:00
roymondchen
a37e0487c6 chore: update lockfile v1.8.0-beta.16 2026-07-23 22:12:42 +08:00
roymondchen
329cd947e1 chore: release v1.8.0-beta.16 2026-07-23 22:11:18 +08:00
roymondchen
0e1986c819 feat: 参考建议可选值最多展示个数由 5 调整为 20 2026-07-23 22:07:51 +08:00
roymondchen
d0a22315c0 refactor(form): 抽离表单校验逻辑至 validateForm 并复用于 props 面板
- 将 editor 的 validatePropsForm 逻辑迁移到 @tmagic/form 的 validateForm
- FormPanel 源码保存改为复用 validateForm 静默校验
- 补充 form 校验与 applyExtendState 相关单元测试
2026-07-23 22:02:43 +08:00
roymondchen
fe7815cf3d chore: update lockfile v1.8.0-beta.15 2026-07-23 20:25:53 +08:00
roymondchen
bf9078a458 chore: release v1.8.0-beta.15 2026-07-23 20:24:47 +08:00
roymondchen
3b9d5da0bc feat(form,editor): submitForm/validateForm 静默挂载时注入静默标记,vs-code 字段跳过 monaco 渲染 2026-07-23 20:22:47 +08:00
roymondchen
2e031cc134 feat(design): 优化 FormItem 和 colorPicker 样式 2026-07-23 19:26:54 +08:00
roymondchen
1f389ff9fd feat(form): text 类型校验允许数字值,兼容数据源字段配置 2026-07-23 19:04:04 +08:00
roymondchen
fe9e754b50 feat(editor): 数据源字段选择器校验提示细化并支持 fieldConfig 类型校验 2026-07-23 19:03:28 +08:00
roymondchen
745f449396 feat: 新增 page-structure-change 事件,支持页面增删统一通知 2026-07-23 19:00:12 +08:00
manmanyu
48abab7051 feat(form,design): 一行多个表单默认右靠齐label 2026-07-23 08:44:10 +00:00
roymondchen
11bf037054 fix: 跳过无字段名称配置的类型校验 2026-07-23 16:32:07 +08:00
roymondchen
97e0cac44a fix(core): 为背景图片 URL 添加引号 2026-07-23 15:40:58 +08:00
roymondchen
cde0805292 fix(editor): 使用属性面板中的源码删除样式无效 2026-07-23 15:15:19 +08:00
roymondchen
6919a0a0b1 fix(editor): 右键菜单动态变化后修复菜单位置防止截断 2026-07-23 15:13:26 +08:00
77 changed files with 1456 additions and 529 deletions

View File

@ -1,3 +1,43 @@
# [1.8.0-beta.17](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.16...v1.8.0-beta.17) (2026-07-27)
### Bug Fixes
* **editor:** 修复 collectIdle 批次结算与中断时 Promise 挂起问题 ([b5abf31](https://github.com/Tencent/tmagic-editor/commit/b5abf310663f0bc34bd2c51c2e1a28de0bdf6ab9))
* **editor:** 切换动作类型时清空关联配置字段 ([b564c9e](https://github.com/Tencent/tmagic-editor/commit/b564c9e4aacd215dd46c4b839cf0033ceedc8047))
* **form,table:** groupList分界线、空状态、actions只存在一个 ([dec4ebf](https://github.com/Tencent/tmagic-editor/commit/dec4ebfa20483f699a676e41e0541d23ebe24d81))
* **form:** 完善 mountFormInstance 清理逻辑,避免临时表单实例泄漏 ([bf8df74](https://github.com/Tencent/tmagic-editor/commit/bf8df74864b78891a8820462062624702a1d33d3))
* **table:** 列操作点击后关闭popover ([596d86f](https://github.com/Tencent/tmagic-editor/commit/596d86f2ab08d3d4e6548b5e4bd5759d18e4493d))
### Features
* **editor,stage:** 新增 containerHighlightAddOnly 配置 ([e6cf694](https://github.com/Tencent/tmagic-editor/commit/e6cf694a032652e4f2c05efbab4a07a252e2ffa1))
# [1.8.0-beta.16](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.15...v1.8.0-beta.16) (2026-07-23)
### Features
* 参考建议可选值最多展示个数由 5 调整为 20 ([0e1986c](https://github.com/Tencent/tmagic-editor/commit/0e1986c81998e50461c294c1a27a8139e8ae9a13))
# [1.8.0-beta.15](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.14...v1.8.0-beta.15) (2026-07-23)
### Bug Fixes
* **core:** 为背景图片 URL 添加引号 ([97e0cac](https://github.com/Tencent/tmagic-editor/commit/97e0cac44a2d6ceafaf95fbc4ae716bfe5d47f70))
* **editor:** 使用属性面板中的源码删除样式无效 ([cde0805](https://github.com/Tencent/tmagic-editor/commit/cde08052924ddef1c51faeb68ce15d3195827adb))
* **editor:** 右键菜单动态变化后修复菜单位置防止截断 ([6919a0a](https://github.com/Tencent/tmagic-editor/commit/6919a0a0b19efff92821fc8d52b61b2f7382a817))
* 跳过无字段名称配置的类型校验 ([11bf037](https://github.com/Tencent/tmagic-editor/commit/11bf037054774937132743a40006364e49ee8fd5))
### Features
* **design:** 优化 FormItem 和 colorPicker 样式 ([2e031cc](https://github.com/Tencent/tmagic-editor/commit/2e031cc1346fb6ea739f0d45a89213b6577dc7ee))
* **editor:** 数据源字段选择器校验提示细化并支持 fieldConfig 类型校验 ([fe9e754](https://github.com/Tencent/tmagic-editor/commit/fe9e754b50f14e6034170b2284553ec1c4536150))
* **form,design:** 一行多个表单默认右靠齐label ([48abab7](https://github.com/Tencent/tmagic-editor/commit/48abab7051b93b7ed336172c21ab3b908733f2e7))
* **form,editor:** submitForm/validateForm 静默挂载时注入静默标记vs-code 字段跳过 monaco 渲染 ([3b9d5da](https://github.com/Tencent/tmagic-editor/commit/3b9d5da0bc9f323e944c0e2adb4fc88642ce1915))
* **form:** text 类型校验允许数字值,兼容数据源字段配置 ([1f389ff](https://github.com/Tencent/tmagic-editor/commit/1f389ff9fd77fb93ad749f3fdbbc8aa0fe8725ac))
* 新增 page-structure-change 事件,支持页面增删统一通知 ([745f449](https://github.com/Tencent/tmagic-editor/commit/745f44939605528e8e2ccd0639478db98293a441))
# [1.8.0-beta.14](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.13...v1.8.0-beta.14) (2026-07-23) # [1.8.0-beta.14](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.13...v1.8.0-beta.14) (2026-07-23)

View File

@ -1020,6 +1020,18 @@ alt: 按住alt键启动识别
- **类型:** `'default' | 'alt' | ''` - **类型:** `'default' | 'alt' | ''`
## containerHighlightAddOnly
- **详情:**
是否只有新增组件(从组件列表拖入新组件)时才启用识别容器
开启后,在画布中拖动已有组件时不再识别容器,即已有组件不能通过拖动加入其他容器;新增组件仍然按 [containerHighlightType](#containerHighlightType) 配置的方式识别
- **默认值:** `false`
- **类型:** `boolean`
## stageRect ## stageRect
- **详情:** - **详情:**

View File

@ -69,10 +69,11 @@
## delayedMarkContainer ## delayedMarkContainer
- **类型**`(event: MouseEvent, excludeElList?: Element[]) => NodeJS.Timeout | undefined` - **类型**`(event: MouseEvent, excludeElList?: Element[], isAdd?: boolean) => NodeJS.Timeout | undefined`
- **参数** - **参数**
- `event`:鼠标事件 - `event`:鼠标事件
- `excludeElList`:计算鼠标所在容器时要排除的元素列表 - `excludeElList`:计算鼠标所在容器时要排除的元素列表
- `isAdd`:当前操作是否为新增组件(从组件列表拖入新组件),`containerHighlightType``addOnly` 时只有新增才会标记容器
- **详情** - **详情**
鼠标拖拽着元素在容器上方悬停延迟一段时间后对容器进行标记如果悬停时间够长将标记成功悬停时间短调用方通过返回的timeoutId取消标记 鼠标拖拽着元素在容器上方悬停延迟一段时间后对容器进行标记如果悬停时间够长将标记成功悬停时间短调用方通过返回的timeoutId取消标记

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "tmagic", "name": "tmagic",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/cli", "name": "@tmagic/cli",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/core", "name": "@tmagic/core",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -51,7 +51,7 @@ export const style2Obj = (style: string) => {
export const fillBackgroundImage = (value: string) => { export const fillBackgroundImage = (value: string) => {
if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) { if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) {
return `url(${value})`; return `url("${value}")`;
} }
return value; return value;
}; };

View File

@ -50,7 +50,7 @@ describe('style2Obj', () => {
describe('fillBackgroundImage', () => { describe('fillBackgroundImage', () => {
test('裸路径会包裹 url()', () => { test('裸路径会包裹 url()', () => {
expect(fillBackgroundImage('a.png')).toBe('url(a.png)'); expect(fillBackgroundImage('a.png')).toBe('url("a.png")');
}); });
test('已经是 url() 不重复包裹', () => { test('已经是 url() 不重复包裹', () => {
@ -127,7 +127,7 @@ describe('transformStyle', () => {
test('backgroundImage: browser 下补全 url()', () => { test('backgroundImage: browser 下补全 url()', () => {
expect(transformStyle({ backgroundImage: 'a.png' }, 'browser')).toEqual({ expect(transformStyle({ backgroundImage: 'a.png' }, 'browser')).toEqual({
backgroundImage: 'url(a.png)', backgroundImage: 'url("a.png")',
}); });
}); });

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/data-source", "name": "@tmagic/data-source",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/dep", "name": "@tmagic/dep",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/design", "name": "@tmagic/design",
"type": "module", "type": "module",
"sideEffects": [ "sideEffects": [

View File

@ -1,5 +1,9 @@
<template> <template>
<component :is="uiComponent" :class="['tmagic-design-card', { 'tmagic-design-card--flat': isFlat }]" v-bind="uiProps"> <component
:is="uiComponent"
:class="['tmagic-design-card', { 'tmagic-design-card--flat': isFlat, 'm-flat-hide-body': isHideBody }]"
v-bind="uiProps"
>
<template #header v-if="$slots.header"> <template #header v-if="$slots.header">
<slot name="header" class="header"></slot> <slot name="header" class="header"></slot>
</template> </template>
@ -30,7 +34,7 @@ const uiComponent = ui?.component || 'el-card';
// `<MEditor>` / `<MForm>` theme `magic-admin` // `<MEditor>` / `<MForm>` theme `magic-admin`
// `flat=true` `flat` // `flat=true` `flat`
const isFlat = computed(() => !!props.flat || isGlobalFlat.value); const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
const isHideBody = computed(() => isFlat.value && props.bodyStyle?.display === 'none');
// `flat` UI el-card / t-card props // `flat` UI el-card / t-card props
// / DOM // / DOM
const uiProps = computed<CardProps>(() => { const uiProps = computed<CardProps>(() => {

View File

@ -47,12 +47,12 @@ const uiComponent = ui?.component || 'el-form-item';
const adapterType = getDesignConfig('adapterType'); const adapterType = getDesignConfig('adapterType');
const formInline = inject<boolean>('formInline'); const formInline = inject<boolean>('formInline', false);
const formInRow = inject<boolean>('isInRow', false);
const uiProps = computed<FormItemProps>(() => { const uiProps = computed<FormItemProps>(() => {
const { extra, extraTips, ...rest } = ui?.props(props) || props; const { extra, extraTips, ...rest } = ui?.props(props) || props;
if (isGlobalFlat.value && rest.labelPosition === undefined) { if (isGlobalFlat.value && rest.labelPosition === undefined) {
return { ...rest, labelPosition: formInline ? 'right' : 'left' }; return { ...rest, labelPosition: formInline || formInRow ? 'right' : 'left' };
} }
return rest; return rest;
}); });

View File

@ -11,7 +11,11 @@
border-right: 0; border-right: 0;
border-radius: 0; border-radius: 0;
border-bottom: 0; border-bottom: 0;
&.m-flat-hide-body {
.el-card__header {
border-bottom: 0 !important;
}
}
&.m-fields-group-list-item { &.m-fields-group-list-item {
margin-bottom: 0; margin-bottom: 0;
} }
@ -21,7 +25,7 @@
.el-card__header { .el-card__header {
border-bottom: none; border-bottom: none;
padding: 16px; padding: 8px 16px;
&:hover { &:hover {
background: transparent; background: transparent;
box-shadow: none; box-shadow: none;
@ -29,11 +33,15 @@
} }
} }
.m-fields-group-list-footer {
margin-bottom: 4px;
margin-top: 4px;
}
.el-card__body { .el-card__body {
padding: 0 16px 0 16px; padding: 0 16px 0 16px;
// 这里每个form-item应该要有一个16px间距但是除了tbody中的form-item // 这里每个form-item应该要有一个16px间距但是除了tbody中的form-item
.tmagic-design-form-item:not(tbody .tmagic-design-form-item) { .tmagic-design-form-item:not(tbody .tmagic-design-form-item) {
margin-bottom: 16px; margin-bottom: 8px;
} }
> .m-container-row { > .m-container-row {
overflow-x: hidden; overflow-x: hidden;
@ -61,7 +69,7 @@
} }
> .el-card__body { > .el-card__body {
background-color: #fff; background-color: #fff;
padding-top: 16px; padding-top: 8px;
} }
} }
} }

View File

@ -2,3 +2,7 @@
margin-left: 8px; margin-left: 8px;
width: calc(var(--el-input-width, 100%) - 40px); width: calc(var(--el-input-width, 100%) - 40px);
} }
.tmagic-design-color-picker {
flex: 0 0 auto;
}

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/editor", "name": "@tmagic/editor",
"type": "module", "type": "module",
"sideEffects": [ "sideEffects": [

View File

@ -218,6 +218,7 @@ const stageOptions: StageOptions = {
containerHighlightClassName: props.containerHighlightClassName, containerHighlightClassName: props.containerHighlightClassName,
containerHighlightDuration: props.containerHighlightDuration, containerHighlightDuration: props.containerHighlightDuration,
containerHighlightType: props.containerHighlightType, containerHighlightType: props.containerHighlightType,
containerHighlightAddOnly: props.containerHighlightAddOnly,
disabledDragStart: props.disabledDragStart, disabledDragStart: props.disabledDragStart,
renderType: props.renderType, renderType: props.renderType,
guidesOptions: props.guidesOptions, guidesOptions: props.guidesOptions,

View File

@ -128,20 +128,42 @@ const outsideClickHideHandler = (e: MouseEvent) => {
hide(); hide();
}; };
const setPosition = (e: { clientY: number; clientX: number }) => { //
const menuHeight = menuEl.value?.clientHeight || 0; const fixPosition = () => {
const menu = menuEl.value;
if (!menu || !visible.value) return;
let top = e.clientY; const menuHeight = menu.clientHeight;
if (menuHeight + e.clientY > document.body.clientHeight) { const menuWidth = menu.clientWidth;
top = document.body.clientHeight - menuHeight;
let { top, left } = menuPosition.value;
if (top + menuHeight > document.body.clientHeight) {
top = Math.max(0, document.body.clientHeight - menuHeight);
} }
if (left + menuWidth > document.body.clientWidth) {
left = Math.max(0, document.body.clientWidth - menuWidth);
}
if (top !== menuPosition.value.top || left !== menuPosition.value.left) {
menuPosition.value = { top, left };
}
};
const setPosition = (e: { clientY: number; clientX: number }) => {
menuPosition.value = { menuPosition.value = {
top, top: e.clientY,
left: e.clientX, left: e.clientX,
}; };
fixPosition();
}; };
//
const resizeObserver = new ResizeObserver(() => {
fixPosition();
});
const show = (e?: { clientY: number; clientX: number }) => { const show = (e?: { clientY: number; clientX: number }) => {
visible.value = true; visible.value = true;
@ -186,12 +208,18 @@ const mouseenterHandler = () => {
}; };
onMounted(() => { onMounted(() => {
if (menuEl.value) {
resizeObserver.observe(menuEl.value);
}
if (props.isSubMenu) return; if (props.isSubMenu) return;
globalThis.addEventListener('mousedown', outsideClickHideHandler, true); globalThis.addEventListener('mousedown', outsideClickHideHandler, true);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
resizeObserver.disconnect();
if (props.isSubMenu) return; if (props.isSubMenu) return;
globalThis.removeEventListener('mousedown', outsideClickHideHandler, true); globalThis.removeEventListener('mousedown', outsideClickHideHandler, true);

View File

@ -82,6 +82,11 @@ export interface EditorProps {
containerHighlightDuration?: number; containerHighlightDuration?: number;
/** 拖入画布中容器时,识别容器的操作类型 */ /** 拖入画布中容器时,识别容器的操作类型 */
containerHighlightType?: ContainerHighlightType; containerHighlightType?: ContainerHighlightType;
/**
*
* false
*/
containerHighlightAddOnly?: boolean;
/** 画布大小 */ /** 画布大小 */
stageRect?: StageRect; stageRect?: StageRect;
/** monaco editor 的配置 */ /** monaco editor 的配置 */
@ -163,6 +168,7 @@ export const defaultEditorProps = {
containerHighlightClassName: CONTAINER_HIGHLIGHT_CLASS_NAME, containerHighlightClassName: CONTAINER_HIGHLIGHT_CLASS_NAME,
containerHighlightDuration: 800, containerHighlightDuration: 800,
containerHighlightType: ContainerHighlightType.DEFAULT, containerHighlightType: ContainerHighlightType.DEFAULT,
containerHighlightAddOnly: false,
disabledShowSrc: false, disabledShowSrc: false,
disabledDataSource: false, disabledDataSource: false,
disabledCodeBlock: false, disabledCodeBlock: false,

View File

@ -1,5 +1,6 @@
<template> <template>
<MagicCodeEditor <MagicCodeEditor
v-if="!silentMode"
:height="config.height" :height="config.height"
:type="diffMode ? 'diff' : undefined" :type="diffMode ? 'diff' : undefined"
:init-values="diffMode ? (lastValues || {})[name] : model[name]" :init-values="diffMode ? (lastValues || {})[name] : model[name]"
@ -17,9 +18,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, inject } from 'vue';
import type { CodeConfig, FieldProps } from '@tmagic/form'; import { type CodeConfig, type FieldProps, FORM_SILENT_MODE_KEY } from '@tmagic/form';
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue'; import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
@ -27,6 +28,13 @@ defineOptions({
name: 'MFieldsVsCode', name: 'MFieldsVsCode',
}); });
/**
* 静默模式submitForm/validateForm 隐藏挂载下跳过 monaco 渲染
* 校验由 FormItem 针对 model 中的值完成与编辑器实例无关本组件挂载无值副作用
* save 仅由用户操作/编辑器内容变更触发跳过可省去 monaco worker/model 的无谓实例化
*/
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
const emit = defineEmits<{ const emit = defineEmits<{
change: [value: string | any]; change: [value: string | any];
}>(); }>();

View File

@ -37,13 +37,12 @@
:model="entry.cardItem" :model="entry.cardItem"
:last-values="entry.lastCardItem" :last-values="entry.lastCardItem"
:is-compare="isCompareMode" :is-compare="isCompareMode"
:hide-expand="true" :hide-expand="false"
:label-width="config.labelWidth || '100px'" :label-width="config.labelWidth || '100px'"
@change="onChangeHandler" @change="onChangeHandler"
> >
<template #header> <template #header>
<div class="event-item-header"> <div class="event-item-header">
<div class="event-item-title">事件{{ Number(entry.index) + 1 }}</div>
<MFormContainer <MFormContainer
class="fullWidth" class="fullWidth"
:config="eventNameConfig" :config="eventNameConfig"
@ -210,6 +209,12 @@ const actionTypeConfig = computed(() => {
trigger: 'blur', trigger: 'blur',
}, },
], ],
onChange: (_mForm: FormState, _v: string, { setModel }: any) => {
setModel('to', '');
setModel('method', '');
setModel('codeId', '');
setModel('dataSourceMethod', []);
},
}; };
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig }; return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
}); });
@ -222,7 +227,7 @@ const targetCompConfig = computed(() => {
type: 'ui-select', type: 'ui-select',
labelPosition: 'left', labelPosition: 'left',
display: (_mForm, { model }) => model.actionType === ActionType.COMP, display: (_mForm, { model }) => model.actionType === ActionType.COMP,
onChange: (_MForm, _v, { setModel }) => { onChange: (_mForm, _v, { setModel }) => {
setModel('method', ''); setModel('method', '');
}, },
rules: [ rules: [

View File

@ -28,6 +28,7 @@ export const useStage = (stageOptions: StageOptions) => {
containerHighlightClassName: stageOptions.containerHighlightClassName, containerHighlightClassName: stageOptions.containerHighlightClassName,
containerHighlightDuration: stageOptions.containerHighlightDuration, containerHighlightDuration: stageOptions.containerHighlightDuration,
containerHighlightType: stageOptions.containerHighlightType, containerHighlightType: stageOptions.containerHighlightType,
containerHighlightAddOnly: stageOptions.containerHighlightAddOnly,
disabledDragStart: stageOptions.disabledDragStart, disabledDragStart: stageOptions.disabledDragStart,
renderType: stageOptions.renderType, renderType: stageOptions.renderType,
canSelect: (el, event, stop) => { canSelect: (el, event, stop) => {

View File

@ -333,11 +333,11 @@ export const initServiceEvents = (
Promise.all( Promise.all(
nodes.map((node) => { nodes.map((node) => {
if (node.type === NodeType.ROOT) { if (node.type === NodeType.ROOT) {
return Promise.resolve(); return Promise.resolve(true);
} }
return depService.collectIdle([node], { pageId: getPageIdByNode(node) }, deep, type); return depService.collectIdle([node], { pageId: getPageIdByNode(node) }, deep, type);
}), }),
); ).then((results) => results.every(Boolean));
watch( watch(
() => editorService.get('stage'), () => editorService.get('stage'),
@ -351,7 +351,7 @@ export const initServiceEvents = (
if (!node) return; if (!node) return;
await collectIdle([node], true, DepTargetType.DATA_SOURCE); if (!(await collectIdle([node], true, DepTargetType.DATA_SOURCE))) return;
updateStageNode(node); updateStageNode(node);
}); });
}, },
@ -494,8 +494,10 @@ export const initServiceEvents = (
// 新增节点,收集依赖 // 新增节点,收集依赖
const nodeAddHandler = (nodes: MComponent[]) => { const nodeAddHandler = (nodes: MComponent[]) => {
collectIdle(nodes, true).then(() => { collectIdle(nodes, true).then((completed) => {
updateStageNodes(nodes); if (completed) {
updateStageNodes(nodes);
}
}); });
}; };
@ -549,8 +551,8 @@ export const initServiceEvents = (
if (needRecollectNodes.length) { if (needRecollectNodes.length) {
// 有数据源依赖需要等依赖重新收集完才更新stage // 有数据源依赖需要等依赖重新收集完才更新stage
const handler = async () => { const handler = async () => {
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE); if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE))) return;
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND); if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND))) return;
updateStageNodes(needRecollectNodes); updateStageNodes(needRecollectNodes);
}; };
handler(); handler();
@ -572,8 +574,10 @@ export const initServiceEvents = (
// 历史记录变化时,需要重新收集依赖 // 历史记录变化时,需要重新收集依赖
const historyChangeHandler = (page: MPage | MPageFragment) => { const historyChangeHandler = (page: MPage | MPageFragment) => {
collectIdle([page], true).then(() => { collectIdle([page], true).then((completed) => {
updateStageNode(page); if (completed) {
updateStageNode(page);
}
}); });
}; };
@ -654,7 +658,7 @@ export const initServiceEvents = (
if (Array.isArray(root?.items)) { if (Array.isArray(root?.items)) {
depService.clearIdleTasks(); depService.clearIdleTasks();
let collectIdlePromises: Promise<void[]>[] = []; let collectIdlePromises: Promise<boolean>[] = [];
if (isModifyField) { if (isModifyField) {
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE); depService.removeTarget(config.id, DepTargetType.DATA_SOURCE);
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE_COND); depService.removeTarget(config.id, DepTargetType.DATA_SOURCE_COND);
@ -679,10 +683,15 @@ export const initServiceEvents = (
collectIdlePromises = [collectIdle(root.items, true, DepTargetType.DATA_SOURCE_METHOD)]; collectIdlePromises = [collectIdle(root.items, true, DepTargetType.DATA_SOURCE_METHOD)];
} }
Promise.all(collectIdlePromises) const handler = async () => {
.then(() => updateDataSourceSchema()) const results = await Promise.all(collectIdlePromises);
.then(() => updateDsData()) if (!results.every(Boolean)) return;
.then(() => updateStageNodes(root.items));
updateDataSourceSchema();
await updateDsData();
updateStageNodes(root.items);
};
handler();
} }
} else if (root?.dataSources) { } else if (root?.dataSources) {
updateDsData(); updateDsData();
@ -706,11 +715,12 @@ export const initServiceEvents = (
const nodeIds = Object.keys(root.dataSourceDeps?.[id] || {}); const nodeIds = Object.keys(root.dataSourceDeps?.[id] || {});
const nodes = getNodes(nodeIds, root.items); const nodes = getNodes(nodeIds, root.items);
await Promise.all([ const results = await Promise.all([
collectIdle(nodes, false, DepTargetType.DATA_SOURCE), collectIdle(nodes, false, DepTargetType.DATA_SOURCE),
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_COND), collectIdle(nodes, false, DepTargetType.DATA_SOURCE_COND),
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_METHOD), collectIdle(nodes, false, DepTargetType.DATA_SOURCE_METHOD),
]); ]);
if (!results.every(Boolean)) return;
updateDataSourceSchema(); updateDataSourceSchema();

View File

@ -49,14 +49,13 @@ import { Document as DocumentIcon } from '@element-plus/icons-vue';
import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design'; import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design';
import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form'; import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
import { MForm } from '@tmagic/form'; import { MForm, validateForm } from '@tmagic/form';
import { filterXSS } from '@tmagic/utils'; import { filterXSS } from '@tmagic/utils';
import MIcon from '@editor/components/Icon.vue'; import MIcon from '@editor/components/Icon.vue';
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps'; import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height'; import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
import { useServices } from '@editor/hooks/use-services'; import { useServices } from '@editor/hooks/use-services';
import { validatePropsForm } from '@editor/utils/props';
import CodeEditor from '../CodeEditor.vue'; import CodeEditor from '../CodeEditor.vue';
@ -162,14 +161,19 @@ const saveCode = async (values: any) => {
// //
// 使 // 使
try { try {
const error = await validatePropsForm({ const error = await validateForm({
config: props.config, config: props.config,
values: newValues,
appContext: internalInstance?.appContext ?? null,
services,
stage: stage.value,
typeMatchValid: true, typeMatchValid: true,
extendState: props.extendState, initValues: newValues,
// provides Editor services / codeOptions provide
// appContext使 MForm DataSourceInput inject
appContext: internalInstance?.appContext ? { ...internalInstance?.appContext, provides: { services } } : null,
extendState: (state) => {
if (configFormRef.value?.formState) {
return { ...(configFormRef.value?.formState || {}) };
}
return props.extendState?.(state) || {};
},
}); });
if (error) { if (error) {

View File

@ -164,17 +164,25 @@ const submit = async (
}; };
if (v.style) { if (v.style) {
Object.entries(v.style).forEach(([key, value]) => { // doUpdate mergeWith
if (value !== '' && newValue.style) { if (eventData) {
newValue.style[key] = value; // DSL
} // changeRecords
}); Object.entries(v.style).forEach(([key, value]) => {
if (value !== '' && newValue.style) {
newValue.style[key] = value;
}
});
eventData?.changeRecords?.forEach((record) => { eventData.changeRecords?.forEach((record) => {
if (record.propPath?.startsWith('style') && record.value === '') { if (record.propPath?.startsWith('style') && record.value === '') {
setValueByKeyPath(record.propPath, record.value, newValue); setValueByKeyPath(record.propPath, record.value, newValue);
} }
}); });
} else {
// eventDatastyle
newValue.style = { ...v.style };
}
} }
// MForm @change eventData changeRecords // MForm @change eventData changeRecords

View File

@ -145,6 +145,6 @@ const dragHandler = (e: DragEvent) => {
if (timeout || !stage.value) return; if (timeout || !stage.value) return;
timeout = stage.value.delayedMarkContainer(e); timeout = stage.value.delayedMarkContainer(e, [], true);
}; };
</script> </script>

View File

@ -42,6 +42,21 @@ interface State {
type StateKey = keyof State; type StateKey = keyof State;
/**
* collectIdle /
* Promise idleTask finish resolve
* clearTasks Promise resolvecollecting
*/
interface CollectBatch {
nodes: MNode[];
deep: boolean;
pending: number;
dsPending: number;
collectedEmitted: boolean;
dsSettled: boolean;
resolve: (completed: boolean) => void;
}
class Dep extends BaseService { class Dep extends BaseService {
private state = shallowReactive<State>({ private state = shallowReactive<State>({
collecting: false, collecting: false,
@ -54,6 +69,12 @@ class Dep extends BaseService {
private waitingWorker?: Promise<void>; private waitingWorker?: Promise<void>;
private resolveWaitingWorker?: () => void;
private workerGeneration = 0;
private activeBatches = new Set<CollectBatch>();
constructor() { constructor() {
super(); super();
@ -137,38 +158,44 @@ class Dep extends BaseService {
await this.waitingWorker; await this.waitingWorker;
} }
this.set('collecting', true); const batch: CollectBatch = {
let startTask = false; nodes,
this.watcher.collectByCallback(nodes, type, ({ node, target }) => { deep,
startTask = true; pending: 0,
dsPending: 0,
collectedEmitted: false,
dsSettled: false,
resolve: () => {},
};
this.enqueueTask(node, target, depExtendedData, deep); this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
this.enqueueTask(node, target, depExtendedData, deep, batch);
}); });
return new Promise<void>((resolve) => { // 没有命中任何 target无需收集直接完成
if (!startTask) { if (batch.pending === 0) {
this.emit('collected', nodes, deep); this.emit('collected', nodes, deep);
this.set('collecting', false); this.updateCollectingState();
resolve(); return true;
return; }
}
this.idleTask.once('finish', () => { this.activeBatches.add(batch);
this.emit('collected', nodes, deep); this.set('collecting', true);
this.set('collecting', false);
}); return new Promise<boolean>((resolve) => {
this.idleTask.once('hight-level-finish', () => { batch.resolve = resolve;
this.emit('ds-collected', nodes, deep);
resolve();
});
}); });
} }
public collectByWorker(dsl: MApp) { public collectByWorker(dsl: MApp) {
this.set('collecting', true); this.set('collecting', true);
this.workerGeneration += 1;
const generation = this.workerGeneration;
const { promise, resolve: waitingResolve } = Promise.withResolvers<void>(); const { promise, resolve: waitingResolve } = Promise.withResolvers<void>();
this.waitingWorker = promise; this.waitingWorker = promise;
this.resolveWaitingWorker = waitingResolve;
return new Promise<Record<string, Record<string, DepData>>>((resolve) => { return new Promise<Record<string, Record<string, DepData>>>((resolve) => {
const worker = new Work(); const worker = new Work();
@ -182,6 +209,11 @@ class Dep extends BaseService {
resolve({}); resolve({});
}; };
}).then((depsData) => { }).then((depsData) => {
if (generation !== this.workerGeneration) {
waitingResolve();
return depsData;
}
traverseTarget(this.watcher.getTargetsList(), (target) => { traverseTarget(this.watcher.getTargetsList(), (target) => {
if (depsData[target.type]?.[target.id]) { if (depsData[target.type]?.[target.id]) {
target.deps = reactive(depsData[target.type][target.id]); target.deps = reactive(depsData[target.type][target.id]);
@ -200,6 +232,10 @@ class Dep extends BaseService {
this.emit('collected', dsl.items, true); this.emit('collected', dsl.items, true);
this.emit('ds-collected', dsl.items, true); this.emit('ds-collected', dsl.items, true);
waitingResolve(); waitingResolve();
if (this.waitingWorker === promise) {
this.waitingWorker = undefined;
this.resolveWaitingWorker = undefined;
}
return depsData; return depsData;
}); });
@ -233,6 +269,7 @@ class Dep extends BaseService {
} }
public clearIdleTasks() { public clearIdleTasks() {
this.abortActiveBatches();
this.idleTask.clearTasks(); this.idleTask.clearTasks();
} }
@ -251,6 +288,11 @@ class Dep extends BaseService {
} }
public reset() { public reset() {
this.abortActiveBatches();
this.workerGeneration += 1;
this.resolveWaitingWorker?.();
this.resolveWaitingWorker = undefined;
this.waitingWorker = undefined;
this.idleTask.clearTasks(); this.idleTask.clearTasks();
for (const type of Object.keys(this.watcher.getTargetsList())) { for (const type of Object.keys(this.watcher.getTargetsList())) {
@ -286,25 +328,99 @@ class Dep extends BaseService {
} }
} }
private enqueueTask(node: MNode, target: Target, depExtendedData: DepExtendedData, deep: boolean) { private enqueueTask(
node: MNode,
target: Target,
depExtendedData: DepExtendedData,
deep: boolean,
batch: CollectBatch,
) {
const isDataSource = target.type === DepTargetType.DATA_SOURCE;
batch.pending += 1;
if (isDataSource) {
batch.dsPending += 1;
}
this.idleTask.enqueueTask( this.idleTask.enqueueTask(
({ node, deep, target }) => { ({ node, deep, target }) => {
this.collectNode(node, target, depExtendedData, deep); try {
this.collectNode(node, target, depExtendedData, deep);
} finally {
this.onBatchTaskDone(batch, isDataSource);
}
}, },
{ {
node, node,
deep: false, deep: false,
target, target,
}, },
target.type === DepTargetType.DATA_SOURCE, isDataSource,
); );
if (deep && Array.isArray(node.items) && node.items.length) { if (deep && Array.isArray(node.items) && node.items.length) {
node.items.forEach((item) => { node.items.forEach((item) => {
this.enqueueTask(item, target, depExtendedData, deep); this.enqueueTask(item, target, depExtendedData, deep, batch);
}); });
} }
} }
private onBatchTaskDone(batch: CollectBatch, isDataSource: boolean) {
if (isDataSource) {
batch.dsPending -= 1;
// 数据源依赖收集完先 resolve让 stage 尽快更新,其余依赖继续在后台收集
if (batch.dsPending === 0) {
this.settleBatchDs(batch);
}
}
batch.pending -= 1;
if (batch.pending === 0) {
this.finishBatch(batch);
}
}
private settleBatchDs(batch: CollectBatch) {
if (batch.dsSettled) return;
batch.dsSettled = true;
this.emit('ds-collected', batch.nodes, batch.deep);
batch.resolve(true);
}
private finishBatch(batch: CollectBatch) {
if (!batch.collectedEmitted) {
batch.collectedEmitted = true;
this.emit('collected', batch.nodes, batch.deep);
}
// 没有数据源任务的批次在此结算 Promise
this.settleBatchDs(batch);
this.activeBatches.delete(batch);
this.updateCollectingState();
}
/**
* idleTask
* collectIdle Promise resolvecollecting trueonce
* emit collected/ds-collected Promise
*/
private abortActiveBatches() {
if (!this.activeBatches.size) {
return;
}
const batches = [...this.activeBatches];
this.activeBatches.clear();
for (const batch of batches) {
batch.resolve(false);
}
this.updateCollectingState();
}
private updateCollectingState() {
this.set('collecting', this.activeBatches.size > 0);
}
} }
export type DepService = Dep; export type DepService = Dep;

View File

@ -554,6 +554,12 @@ class Editor extends BaseService {
doNotPushHistory, doNotPushHistory,
}); });
// 页面 / 页面片新增不入历史栈(见上方 isPage / isPageFragment 分支),这里合并补发一次结构变更通知
const addedPages = newNodes.filter((node) => isPage(node) || isPageFragment(node)) as (MPage | MPageFragment)[];
if (addedPages.length) {
historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
}
return Array.isArray(addNode) ? newNodes : newNodes[0]; return Array.isArray(addNode) ? newNodes : newNodes[0];
} }
@ -695,6 +701,12 @@ class Editor extends BaseService {
this.emit('remove', nodes); this.emit('remove', nodes);
this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory }); this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
// 页面 / 页面片删除不入历史栈(见上方 isPage / isPageFragment 分支),这里合并补发一次结构变更通知
const removedPages = nodes.filter((node) => isPage(node) || isPageFragment(node)) as (MPage | MPageFragment)[];
if (removedPages.length) {
historyService.notifyPageStructureChange({ add: [], remove: removedPages });
}
} }
public async doUpdate( public async doUpdate(
@ -1776,6 +1788,10 @@ class Editor extends BaseService {
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p])); const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`); const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
// 收集本次整体替换中增删的页面,循环结束后合并为一次结构变更通知(避免逐页派发多个事件)
const addedPages: (MPage | MPageFragment)[] = [];
const removedPages: (MPage | MPageFragment)[] = [];
nextPages.forEach((nextPage) => { nextPages.forEach((nextPage) => {
const prevPage = prevMap.get(`${nextPage.id}`); const prevPage = prevMap.get(`${nextPage.id}`);
if (!prevPage) { if (!prevPage) {
@ -1785,6 +1801,7 @@ class Editor extends BaseService {
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) }, { newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
source, source,
); );
addedPages.push(nextPage);
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) { } else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
this.pushPageDiffStep( this.pushPageDiffStep(
'update', 'update',
@ -1803,8 +1820,13 @@ class Editor extends BaseService {
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) }, { oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
source, source,
); );
removedPages.push(prevPage);
} }
}); });
if (addedPages.length || removedPages.length) {
historyService.notifyPageStructureChange({ add: addedPages, remove: removedPages });
}
} }
/** /**

View File

@ -19,7 +19,7 @@
import { reactive } from 'vue'; import { reactive } from 'vue';
import type { Writable } from 'type-fest'; import type { Writable } from 'type-fest';
import type { Id } from '@tmagic/core'; import type { Id, MPage, MPageFragment } from '@tmagic/core';
import { guid } from '@tmagic/utils'; import { guid } from '@tmagic/utils';
import type { import type {
@ -353,6 +353,27 @@ class History extends BaseService {
this.emit('mark-saved', { kind: stepType, id }); this.emit('mark-saved', { kind: stepType, id });
} }
/**
* / `page-structure-change`
*
* `editorService.add` / `remove` `page` editor.add / remove
* isPage / isPageFragment historyService
* setRoot DSL
*
* ** change **
* 使`add` / `remove`
*
* @param change { add: 新增的页面列表, remove: 删除的页面列表 }
*/
public notifyPageStructureChange(change: {
add: (MPage | MPageFragment)[];
remove: (MPage | MPageFragment)[];
}): void {
// 增删均为空时不派发,避免无意义通知
if (!change.add.length && !change.remove.length) return;
this.emit('page-structure-change', change);
}
/** /**
* / / / IndexedDB * / / / IndexedDB
* *

View File

@ -26,7 +26,7 @@
> .m-fields-group-list { > .m-fields-group-list {
> .tmagic-design-card--flat { > .tmagic-design-card--flat {
> .el-card__header { > .el-card__header {
padding: 16px 0; padding: 8px 0;
} }
} }
} }
@ -34,7 +34,7 @@
.create-button { .create-button {
&.fullWidth { &.fullWidth {
width: 100%; width: 100%;
margin: 0 0 16px 0; margin: 0 0 8px 0;
} }
} }
} }

View File

@ -1,8 +1,11 @@
.m-fields-event-select { .m-fields-event-select {
width: 100%; width: 100%;
.fullWidth { .fullWidth {
width: 100%; width: calc(100% - 32px);
box-sizing: border-box; box-sizing: border-box;
&.create-button {
width: 100%;
}
} }
.event-select-container { .event-select-container {
padding: 0 16px; padding: 0 16px;
@ -11,7 +14,7 @@
border-bottom: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5;
} }
> .el-card__body { > .el-card__body {
padding-bottom: 16px; padding-bottom: 8px;
.m-fields-group-list-footer { .m-fields-group-list-footer {
div { div {
justify-content: flex-start !important; justify-content: flex-start !important;
@ -19,7 +22,7 @@
} }
.el-card.tmagic-design-card--flat { .el-card.tmagic-design-card--flat {
> .el-card__header { > .el-card__header {
padding: 16px 0; padding: 8px 0;
} }
} }
} }
@ -76,17 +79,18 @@
.event-item-header { .event-item-header {
position: relative; position: relative;
width: 100%; width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.event-item-title { .event-item-title {
color: #0f1113; color: #0f1113;
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
line-height: 24px; line-height: 24px;
margin-bottom: 16px; margin-bottom: 8px;
} }
.event-item-delete-button { .event-item-delete-button {
position: absolute; color: #0f1113;
right: 0;
top: 0;
} }
.el-form-item { .el-form-item {

View File

@ -178,6 +178,11 @@ export interface StageOptions {
containerHighlightClassName?: string; containerHighlightClassName?: string;
containerHighlightDuration?: number; containerHighlightDuration?: number;
containerHighlightType?: ContainerHighlightType; containerHighlightType?: ContainerHighlightType;
/**
*
* false
*/
containerHighlightAddOnly?: boolean;
disabledDragStart?: boolean; disabledDragStart?: boolean;
render?: (stage: StageCore) => HTMLDivElement | void | Promise<HTMLDivElement | void>; render?: (stage: StageCore) => HTMLDivElement | void | Promise<HTMLDivElement | void>;
moveableOptions?: CustomizeMoveableOptions; moveableOptions?: CustomizeMoveableOptions;
@ -1385,6 +1390,12 @@ export interface HistoryEvents {
'mark-saved': [{ kind: HistoryStepType; id?: Id }]; 'mark-saved': [{ kind: HistoryStepType; id?: Id }];
clear: [{ id: Id; stepType: HistoryStepType }]; clear: [{ id: Id; stepType: HistoryStepType }];
'marker-change': [{ id: Id; marker: StepValue; stepType: HistoryStepType }]; 'marker-change': [{ id: Id; marker: StepValue; stepType: HistoryStepType }];
/**
* / / {@link HistoryService.notifyPageStructureChange}
* add / remove / setRoot ****
* `add` / `remove`
*/
'page-structure-change': [change: { add: (MPage | MPageFragment)[]; remove: (MPage | MPageFragment)[] }];
} }
export const canUsePluginMethods = { export const canUsePluginMethods = {

View File

@ -243,7 +243,7 @@ export const resolveFieldByPath = (
fields: DataSchema[] | undefined, fields: DataSchema[] | undefined,
fieldNames: string[], fieldNames: string[],
options: { skipNumberIndices?: boolean } = {}, options: { skipNumberIndices?: boolean } = {},
): { ok: boolean; field?: DataSchema; fields: DataSchema[] } => { ): { ok: boolean; field?: DataSchema; fields: DataSchema[]; failedName?: string } => {
let currentFields = fields || []; let currentFields = fields || [];
let field: DataSchema | undefined; let field: DataSchema | undefined;
@ -252,11 +252,11 @@ export const resolveFieldByPath = (
continue; continue;
} }
if (!currentFields.length) { if (!currentFields.length) {
return { ok: false, fields: currentFields }; return { ok: false, fields: currentFields, failedName: name };
} }
field = currentFields.find((item) => item.name === name); field = currentFields.find((item) => item.name === name);
if (!field) { if (!field) {
return { ok: false, fields: currentFields }; return { ok: false, fields: currentFields, failedName: name };
} }
currentFields = field.fields || []; currentFields = field.fields || [];
} }

View File

@ -25,6 +25,12 @@ globalThis.requestIdleCallback =
}, 1); }, 1);
}; };
globalThis.cancelIdleCallback =
globalThis.cancelIdleCallback ||
function (handle) {
clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
};
export class IdleTask<T = any> extends EventEmitter { export class IdleTask<T = any> extends EventEmitter {
private taskList: TaskList<T> = []; private taskList: TaskList<T> = [];

View File

@ -16,9 +16,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { AppContext } from 'vue';
import { import {
HookType, HookType,
NODE_CONDS_KEY, NODE_CONDS_KEY,
@ -27,18 +24,7 @@ import {
NODE_DISABLE_DATA_SOURCE_KEY, NODE_DISABLE_DATA_SOURCE_KEY,
} from '@tmagic/core'; } from '@tmagic/core';
import { tMagicMessage } from '@tmagic/design'; import { tMagicMessage } from '@tmagic/design';
import type { import type { ChildConfig, DisplayCondsConfig, FormConfig, TabConfig, TabPaneConfig } from '@tmagic/form';
ChildConfig,
DisplayCondsConfig,
FormConfig,
FormState,
FormValue,
TabConfig,
TabPaneConfig,
} from '@tmagic/form';
import { validateForm } from '@tmagic/form';
import type { Services } from '@editor/type';
export const arrayOptions = [ export const arrayOptions = [
{ text: '包含', value: 'include' }, { text: '包含', value: 'include' },
@ -403,86 +389,3 @@ export const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
}), }),
}; };
}); });
// #region ValidatePropsFormOptions
/**
* validatePropsForm
*/
export interface ValidatePropsFormOptions {
/** 组件属性表单配置 */
config: FormConfig;
/** 待校验的表单值 */
values: FormValue;
/**
* appContext `getCurrentInstance()?.appContext`
* services MForm appContext使DataSourceInput inject
*/
appContext?: AppContext | null;
/** 编辑器服务集合,注入到临时表单的 formState */
services?: Services;
/** stage 实例,注入到临时表单的 formState */
stage?: any;
/** 外部扩展的 formState */
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* `true`
* `false`
*/
debug?: boolean;
typeMatchValid?: boolean;
}
// #endregion ValidatePropsFormOptions
/**
* + ****
*
* `@tmagic/form` `validateForm` MForm
* provides appContext formState
* stage / services
*
*
* 使
*
* @returns `''` `<br>`
* reject
*
* @example
* ```ts
* const error = await validatePropsForm({
* config,
* values,
* appContext: getCurrentInstance()?.appContext,
* services,
* stage: editorService.get('stage'),
* extendState,
* });
* if (error) {
* // 配置不合法error 为错误文案
* }
* ```
*/
export const validatePropsForm = ({
config,
values,
appContext = null,
services,
stage,
extendState,
debug,
typeMatchValid,
}: ValidatePropsFormOptions): Promise<string> =>
validateForm({
config,
debug,
typeMatchValid,
initValues: values,
// 将当前组件实例的 provides含 Editor 顶层的 services / codeOptions 等组件级 provide
// 合入 appContext使临时 MForm 中的编辑器字段组件DataSourceInput 等)能正常 inject
appContext: appContext ? { ...appContext, provides: { services } } : null,
// 与页面表单保持一致:注入 stage/services 及外部扩展状态,保证校验规则依赖的上下文可用
extendState: async (state) => ({
...((await extendState?.(state)) || {}),
stage,
services,
}),
});

View File

@ -20,6 +20,7 @@ import type { DataSourceFieldType, DataSourceSchema, Id } from '@tmagic/core';
import { NodeType } from '@tmagic/core'; import { NodeType } from '@tmagic/core';
import { appendValidateSuggestion } from '@tmagic/design'; import { appendValidateSuggestion } from '@tmagic/design';
import type { TypeMatchValidateContext, TypeMatchValidator } from '@tmagic/form'; import type { TypeMatchValidateContext, TypeMatchValidator } from '@tmagic/form';
import { validateTypeMatch } from '@tmagic/form';
import { import {
DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX,
DATA_SOURCE_SET_DATA_METHOD_NAME, DATA_SOURCE_SET_DATA_METHOD_NAME,
@ -57,7 +58,7 @@ const stringifyExampleValue = (value: any): string => {
}; };
// 参考建议中最多展示的可选值个数,超出以「等」省略。 // 参考建议中最多展示的可选值个数,超出以「等」省略。
const MAX_SUGGESTION_OPTIONS = 5; const MAX_SUGGESTION_OPTIONS = 20;
/** /**
* 使xxxxxx * 使xxxxxx
@ -261,16 +262,20 @@ const validateDataSourceFieldPath = (
const ds = findDataSource(`${dsId}`); const ds = findDataSource(`${dsId}`);
if (!ds) { if (!ds) {
return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion()); return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
} }
if (!fieldNames.length) { if (!fieldNames.length) {
return undefined; return undefined;
} }
const { field, ok } = resolveFieldByPath(ds.fields, fieldNames); const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, fieldNames);
if (!ok) { if (!ok) {
return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion()); return defaultMessage(
options.message,
`数据源字段(${failedName})不存在`,
listSuggestion(fields.map((item) => item.name)),
);
} }
const allowedTypes = options.dataSourceFieldType || ['any']; const allowedTypes = options.dataSourceFieldType || ['any'];
@ -471,11 +476,12 @@ const isDataSourceFieldPathValue = (value: any, config: any): value is string[]
return `${value[0]}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX); return `${value[0]}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
}; };
const validateDataSourceFieldSelect: TypeMatchValidator = (value, { message, props }) => { const validateDataSourceFieldSelect: TypeMatchValidator = (value, { mForm, message, props }) => {
const config = props.config || {}; const config = props.config || {};
if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) { if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) {
return undefined; // 值不是数据源字段路径时,按 fieldConfig 的类型校验(与表单项自身 typeMatch 行为一致)
return validateTypeMatch(value, mForm, { ...props, config: { name: config.name, ...config.fieldConfig } }, message);
} }
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) { if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {

View File

@ -4,10 +4,27 @@
* Copyright (C) 2025 Tencent. * Copyright (C) 2025 Tencent.
*/ */
import { afterEach, describe, expect, test, vi } from 'vitest'; import { afterEach, describe, expect, test, vi } from 'vitest';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import ContentMenu from '@editor/components/ContentMenu.vue'; import ContentMenu from '@editor/components/ContentMenu.vue';
const roCallbacks: Array<() => void> = [];
class FakeResizeObserver {
public static disconnectSpy = vi.fn();
public cb: () => void;
constructor(cb: () => void) {
this.cb = cb;
roCallbacks.push(cb);
}
public observe() {}
public unobserve() {}
public disconnect() {
FakeResizeObserver.disconnectSpy();
}
}
(globalThis as any).ResizeObserver = FakeResizeObserver;
const provideServices = () => ({ const provideServices = () => ({
global: { global: {
provide: { provide: {
@ -22,6 +39,8 @@ const provideServices = () => ({
describe('ContentMenu.vue', () => { describe('ContentMenu.vue', () => {
afterEach(() => { afterEach(() => {
vi.useRealTimers(); vi.useRealTimers();
roCallbacks.length = 0;
FakeResizeObserver.disconnectSpy.mockClear();
}); });
test('show 后触发 show 事件', async () => { test('show 后触发 show 事件', async () => {
@ -67,6 +86,62 @@ describe('ContentMenu.vue', () => {
expect((wrapper.vm as any).menuPosition.top).toBeLessThanOrEqual(100); expect((wrapper.vm as any).menuPosition.top).toBeLessThanOrEqual(100);
}); });
test('菜单大小动态变化后修正位置避免超出可视范围', async () => {
// jsdom 中 clientWidth/clientHeight 默认为 0先 mock 一个足够大的视口,避免 show 时位置被修正
Object.defineProperty(document.body, 'clientHeight', { value: 1000, configurable: true });
Object.defineProperty(document.body, 'clientWidth', { value: 1000, configurable: true });
const wrapper = mount(ContentMenu as any, {
...provideServices(),
props: { menuData: [{ id: '1', type: 'button', text: 'a' }] as any },
attachTo: document.body,
});
(wrapper.vm as any).show({ clientX: 90, clientY: 90 });
await new Promise((r) => setTimeout(r, 0));
expect((wrapper.vm as any).menuPosition.top).toBe(90);
expect((wrapper.vm as any).menuPosition.left).toBe(90);
// 视口缩小为 100x100
Object.defineProperty(document.body, 'clientHeight', { value: 100, configurable: true });
Object.defineProperty(document.body, 'clientWidth', { value: 100, configurable: true });
const menuEl = wrapper.find('.magic-editor-content-menu').element as HTMLElement;
// 模拟菜单内容增多后尺寸变为 50x50
Object.defineProperty(menuEl, 'clientHeight', { value: 50, configurable: true });
Object.defineProperty(menuEl, 'clientWidth', { value: 50, configurable: true });
roCallbacks.forEach((cb) => cb());
await nextTick();
expect((wrapper.vm as any).menuPosition.top).toBe(50);
expect((wrapper.vm as any).menuPosition.left).toBe(50);
wrapper.unmount();
});
test('菜单未显示时尺寸变化不修正位置', async () => {
const wrapper = mount(ContentMenu as any, {
...provideServices(),
props: { menuData: [{ id: '1', type: 'button', text: 'a' }] as any },
});
Object.defineProperty(document.body, 'clientHeight', { value: 100, configurable: true });
roCallbacks.forEach((cb) => cb());
await nextTick();
expect((wrapper.vm as any).menuPosition.top).toBe(0);
expect((wrapper.vm as any).menuPosition.left).toBe(0);
wrapper.unmount();
});
test('卸载时断开 ResizeObserver', () => {
const wrapper = mount(ContentMenu as any, {
...provideServices(),
props: { menuData: [] as any },
});
wrapper.unmount();
expect(FakeResizeObserver.disconnectSpy).toHaveBeenCalled();
});
test('contains 判断 DOM 是否在菜单内部', async () => { test('contains 判断 DOM 是否在菜单内部', async () => {
const wrapper = mount(ContentMenu as any, { const wrapper = mount(ContentMenu as any, {
...provideServices(), ...provideServices(),

View File

@ -21,6 +21,7 @@ describe('defaultEditorProps', () => {
test('containerHighlight 默认值', () => { test('containerHighlight 默认值', () => {
expect(defaultEditorProps.containerHighlightDuration).toBe(800); expect(defaultEditorProps.containerHighlightDuration).toBe(800);
expect(typeof defaultEditorProps.containerHighlightClassName).toBe('string'); expect(typeof defaultEditorProps.containerHighlightClassName).toBe('string');
expect(defaultEditorProps.containerHighlightAddOnly).toBe(false);
}); });
test('数组/对象工厂函数返回空值', () => { test('数组/对象工厂函数返回空值', () => {

View File

@ -7,6 +7,8 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { defineComponent, h } from 'vue'; import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
import Code from '@editor/fields/Code.vue'; import Code from '@editor/fields/Code.vue';
// 用一个简单的桩组件代替 MagicCodeEditor把所有 props 原样渲染到 data-* 属性上, // 用一个简单的桩组件代替 MagicCodeEditor把所有 props 原样渲染到 data-* 属性上,
@ -45,7 +47,7 @@ vi.mock('@editor/layouts/CodeEditor.vue', () => ({
}), }),
})); }));
const mountCode = (props: Record<string, any>) => const mountCode = (props: Record<string, any>, global?: Record<string, any>) =>
mount(Code, { mount(Code, {
props: { props: {
// FieldProps 必填字段,用 as any 绕过测试中类型严格匹配 // FieldProps 必填字段,用 as any 绕过测试中类型严格匹配
@ -55,6 +57,7 @@ const mountCode = (props: Record<string, any>) =>
prop: 'codeField', prop: 'codeField',
...props, ...props,
} as any, } as any,
global,
}); });
const getEl = (wrapper: ReturnType<typeof mountCode>) => wrapper.find('.fake-code-editor').element as HTMLElement; const getEl = (wrapper: ReturnType<typeof mountCode>) => wrapper.find('.fake-code-editor').element as HTMLElement;
@ -168,6 +171,28 @@ describe('Code', () => {
}); });
}); });
describe('静默模式', () => {
test('注入 FORM_SILENT_MODE_KEY=true 时不渲染 CodeEditor', () => {
const wrapper = mountCode(
{},
{
provide: { [FORM_SILENT_MODE_KEY as symbol]: true },
},
);
expect(wrapper.find('.fake-code-editor').exists()).toBe(false);
});
test('注入 FORM_SILENT_MODE_KEY=false 时正常渲染 CodeEditor', () => {
const wrapper = mountCode(
{},
{
provide: { [FORM_SILENT_MODE_KEY as symbol]: false },
},
);
expect(wrapper.find('.fake-code-editor').exists()).toBe(true);
});
});
describe('diff 模式 (对比)', () => { describe('diff 模式 (对比)', () => {
test('isCompare=true 且有 lastValues 时切换为 diff 模式', () => { test('isCompare=true 且有 lastValues 时切换为 diff 模式', () => {
const wrapper = mountCode({ const wrapper = mountCode({

View File

@ -91,7 +91,7 @@ const mkServices = () => {
clear: vi.fn(), clear: vi.fn(),
clearTargets: vi.fn(), clearTargets: vi.fn(),
clearIdleTasks: vi.fn(), clearIdleTasks: vi.fn(),
collectIdle: vi.fn(async () => undefined), collectIdle: vi.fn(async () => true),
collectByWorker: vi.fn(async () => undefined), collectByWorker: vi.fn(async () => undefined),
reset: vi.fn(), reset: vi.fn(),
}; };

View File

@ -95,7 +95,7 @@ vi.mock('@editor/layouts/props-panel/FormPanel.vue', () => ({
// 模拟 CodeEditor 源码保存:仅传 values无 eventData、无 error对应 saveCode 路径) // 模拟 CodeEditor 源码保存:仅传 values无 eventData、无 error对应 saveCode 路径)
h('button', { h('button', {
class: 'code-save-btn', class: 'code-save-btn',
onClick: () => emit('submit', { id: 'n1', style: { color: 'red' } }), onClick: () => emit('submit', { id: 'n1', style: { color: 'red', width: '' } }),
}), }),
h('button', { class: 'submit-err-btn', onClick: () => emit('submit-error', new Error('e')) }), h('button', { class: 'submit-err-btn', onClick: () => emit('submit-error', new Error('e')) }),
h('button', { class: 'form-err-btn', onClick: () => emit('form-error', new Error('e')) }), h('button', { class: 'form-err-btn', onClick: () => emit('form-error', new Error('e')) }),
@ -239,6 +239,16 @@ describe('PropsPanel', () => {
expect(options.historySource).toBe('code'); expect(options.historySource).toBe('code');
}); });
test('CodeEditor 源码保存时 style 中的空字符串值被保留(表示清除该样式)', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await new Promise((r) => setTimeout(r, 0));
await wrapper.find('.code-save-btn').trigger('click');
const calledNode = (editorService.update.mock.calls[0] as any)[0];
expect(calledNode.style.color).toBe('red');
expect(calledNode.style.width).toBe('');
});
test('mounted 事件 emit', async () => { test('mounted 事件 emit', async () => {
const wrapper = mount(PropsPanel, { props: {} as any }); const wrapper = mount(PropsPanel, { props: {} as any });
await wrapper.find('.mounted-btn').trigger('click'); await wrapper.find('.mounted-btn').trigger('click');

View File

@ -135,6 +135,6 @@ describe('ComponentListPanel', () => {
const item = wrapper.find('.component-item'); const item = wrapper.find('.component-item');
await item.trigger('drag', { clientX: 0, clientY: 0 }); await item.trigger('drag', { clientX: 0, clientY: 0 });
await item.trigger('drag', { clientX: 0, clientY: 0 }); await item.trigger('drag', { clientX: 0, clientY: 0 });
expect(stage.delayedMarkContainer).toHaveBeenCalled(); expect(stage.delayedMarkContainer).toHaveBeenCalledWith(expect.anything(), [], true);
}); });
}); });

View File

@ -13,16 +13,17 @@ vi.mock('@editor/utils/dep/worker.ts?worker&inline', () => ({
default: class FakeWorker { default: class FakeWorker {
public static nextData: Record<string, any> = {}; public static nextData: Record<string, any> = {};
public static nextError = false; public static nextError = false;
public static nextDelay = 0;
public onmessage: ((e: any) => void) | null = null; public onmessage: ((e: any) => void) | null = null;
public onerror: (() => void) | null = null; public onerror: (() => void) | null = null;
public postMessage() { public postMessage() {
setTimeout(() => { setTimeout(() => {
if (FakeWorker.nextError) { if (FakeWorker.nextError) {
this.onerror?.(new Event('error')); this.onerror?.();
return; return;
} }
this.onmessage?.({ data: FakeWorker.nextData }); this.onmessage?.({ data: FakeWorker.nextData });
}, 0); }, FakeWorker.nextDelay);
} }
}, },
})); }));
@ -121,7 +122,7 @@ describe('Dep service', () => {
test('collectIdle - 没有命中时立即 resolve 并 emit collected', async () => { test('collectIdle - 没有命中时立即 resolve 并 emit collected', async () => {
const fn = vi.fn(); const fn = vi.fn();
depService.on('collected', fn); depService.on('collected', fn);
await depService.collectIdle([{ id: 'n1', type: 'text' }] as any); await expect(depService.collectIdle([{ id: 'n1', type: 'text' }] as any)).resolves.toBe(true);
expect(fn).toHaveBeenCalled(); expect(fn).toHaveBeenCalled();
depService.off('collected', fn); depService.off('collected', fn);
}); });
@ -219,6 +220,83 @@ describe('Dep service', () => {
fakeWorker.nextData = {}; fakeWorker.nextData = {};
}); });
test('collectIdle 命中 target 时最终 resolve 并按批次 emit collected/ds-collected', async () => {
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
const collected = vi.fn();
const dsCollected = vi.fn();
depService.on('collected', collected);
depService.on('ds-collected', dsCollected);
const nodes = [{ id: 'n1', type: 'text' }] as any;
await expect(depService.collectIdle(nodes, {}, false, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
expect(dsCollected).toHaveBeenCalledWith(nodes, false);
expect(collected).toHaveBeenCalledWith(nodes, false);
expect(depService.get('collecting')).toBe(false);
depService.off('collected', collected);
depService.off('ds-collected', dsCollected);
});
test('clearIdleTasks 会结算在途 collectIdle避免 Promise 永久挂起且 collecting 复位', async () => {
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
expect(depService.get('collecting')).toBe(true);
// 快速触发:任务尚未执行就清空队列,批次应被主动结算而不是永久挂起
depService.clearIdleTasks();
await expect(promise).resolves.toBe(false);
expect(depService.get('collecting')).toBe(false);
});
test('reset 会结算在途 collectIdle', async () => {
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
depService.reset();
await expect(promise).resolves.toBe(false);
expect(depService.get('collecting')).toBe(false);
});
test('reset 会忽略在途 worker 的过期结果,避免覆盖新依赖', async () => {
const fakeWorker = (await import('@editor/utils/dep/worker.ts?worker&inline')).default as any;
fakeWorker.nextDelay = 20;
fakeWorker.nextData = {
[DepTargetType.DATA_SOURCE]: { ds1: { n1: { data: {} } } },
};
const workerPromise = depService.collectByWorker({ items: [], id: 'app', type: 'app' } as any);
depService.reset();
const target = makeTarget('ds1', DepTargetType.DATA_SOURCE);
depService.addTarget(target);
const idlePromise = depService.collectIdle(
[{ id: 'n1', type: 'text' }] as any,
{},
false,
DepTargetType.DATA_SOURCE,
);
await Promise.all([workerPromise, idlePromise]);
expect(target.deps.n1).toBeUndefined();
fakeWorker.nextDelay = 0;
fakeWorker.nextData = {};
});
test('多个批次并发时各自独立 resolve全部完成后 collecting 复位', async () => {
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
const p1 = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
const p2 = depService.collectIdle([{ id: 'n2', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
await Promise.all([p1, p2]);
expect(depService.get('collecting')).toBe(false);
});
test('destroy 会 reset 并移除监听', () => { test('destroy 会 reset 并移除监听', () => {
depService.addTarget(makeTarget('destroy-me')); depService.addTarget(makeTarget('destroy-me'));
expect(() => depService.destroy()).not.toThrow(); expect(() => depService.destroy()).not.toThrow();

View File

@ -105,8 +105,8 @@ describe('data-source utils', () => {
fields: [], fields: [],
}); });
expect(resolveFieldByPath(fields, ['obj']).field?.name).toBe('obj'); expect(resolveFieldByPath(fields, ['obj']).field?.name).toBe('obj');
expect(resolveFieldByPath(fields, ['unknown'])).toEqual({ ok: false, fields }); expect(resolveFieldByPath(fields, ['unknown'])).toEqual({ ok: false, fields, failedName: 'unknown' });
expect(resolveFieldByPath(undefined, ['x'])).toEqual({ ok: false, fields: [] }); expect(resolveFieldByPath(undefined, ['x'])).toEqual({ ok: false, fields: [], failedName: 'x' });
expect(resolveFieldByPath(fields, []).ok).toBe(true); expect(resolveFieldByPath(fields, []).ok).toBe(true);
}); });
@ -124,7 +124,9 @@ describe('data-source utils', () => {
expect(result.field?.name).toBe('item'); expect(result.field?.name).toBe('item');
expect(result.fields).toEqual([{ name: 'n', type: 'string' }]); expect(result.fields).toEqual([{ name: 'n', type: 'string' }]);
expect(resolveFieldByPath(fields, ['arr', '0', 'missing'], { skipNumberIndices: true }).ok).toBe(false); const failed = resolveFieldByPath(fields, ['arr', '0', 'missing'], { skipNumberIndices: true });
expect(failed.ok).toBe(false);
expect(failed.failedName).toBe('missing');
}); });
test('getFieldType 沿 path 取最终类型', () => { test('getFieldType 沿 path 取最终类型', () => {

View File

@ -3,10 +3,9 @@
* *
* Copyright (C) 2025 Tencent. * Copyright (C) 2025 Tencent.
*/ */
import { beforeEach, describe, expect, test, vi } from 'vitest'; import { describe, expect, test, vi } from 'vitest';
import { NODE_CONDS_RESULT_KEY } from '@tmagic/core'; import { NODE_CONDS_RESULT_KEY } from '@tmagic/core';
import { validateForm } from '@tmagic/form';
import { import {
advancedTabConfig, advancedTabConfig,
@ -20,7 +19,6 @@ import {
numberOptions, numberOptions,
removeStyleDisplayConfig, removeStyleDisplayConfig,
styleTabConfig, styleTabConfig,
validatePropsForm,
} from '@editor/utils/props'; } from '@editor/utils/props';
vi.mock('@tmagic/design', () => ({ vi.mock('@tmagic/design', () => ({
@ -172,59 +170,6 @@ describe('fillConfig', () => {
}); });
}); });
describe('validatePropsForm', () => {
beforeEach(() => {
vi.mocked(validateForm).mockReset();
});
test('合入 appContext.provides、注入 stage/services 并合并外部 extendStatedebug 默认 undefined', async () => {
vi.mocked(validateForm).mockResolvedValue('err-text');
const services = { uiService: {} } as any;
const stage = { id: 'stage' };
const appContext = { app: {}, provides: { foo: 1 } } as any;
const result = await validatePropsForm({
config: [],
values: { a: 1 },
appContext,
services,
stage,
extendState: async () => ({ extra: true }),
});
// 直接返回 validateForm 的结果
expect(result).toBe('err-text');
const arg = vi.mocked(validateForm).mock.calls[0][0];
expect(arg.config).toEqual([]);
expect(arg.debug).toBeUndefined();
expect(arg.initValues).toEqual({ a: 1 });
// appContext 保留原字段,但 provides 被替换为 { services }
expect(arg.appContext).toEqual({ app: {}, provides: { services } });
// extendState 合并外部返回并注入 stage/services
const state = await arg.extendState!({} as any);
expect(state).toEqual({ extra: true, stage, services });
});
test('appContext 缺省为 nullextendState 缺省时仍注入 stage/services可覆盖 debug', async () => {
vi.mocked(validateForm).mockResolvedValue('');
const services = {} as any;
const stage = {};
await validatePropsForm({ config: [], values: {}, services, stage, debug: false });
const arg = vi.mocked(validateForm).mock.calls[0][0];
expect(arg.appContext).toBeNull();
expect(arg.debug).toBe(false);
const state = await arg.extendState!({} as any);
expect(state).toEqual({ stage, services });
});
});
describe('removeStyleDisplayConfig', () => { describe('removeStyleDisplayConfig', () => {
test('将 tab 内「样式」pane 的 display 置为 true', () => { test('将 tab 内「样式」pane 的 display 置为 true', () => {
const config = [ const config = [

View File

@ -207,9 +207,18 @@ describe('editorTypeMatchRules', () => {
expect(firstLine(run('data-source-method-select', ['missing', 'custom']))).toBe('数据源(missing)不存在'); expect(firstLine(run('data-source-method-select', ['missing', 'custom']))).toBe('数据源(missing)不存在');
}); });
test('data-source-field-select 校验路径与 fieldConfig 跳过', () => { test('data-source-field-select 校验路径与 fieldConfig 类型校验', () => {
expect(firstLine(run('data-source-field-select', 'x'))).toBe('x类型应为字符串数组'); expect(firstLine(run('data-source-field-select', 'x'))).toBe('x类型应为字符串数组');
expect(run('data-source-field-select', 'text-value', { fieldConfig: { type: 'text' } })).toBeUndefined();
// 有 fieldConfig 且值不是数据源字段路径时,按 fieldConfig 的类型校验text 允许数字)
expect(run('data-source-field-select', 'text-value', { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined();
expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: { type: 'text' } })).toBeUndefined();
expect(firstLine(run('data-source-field-select', { a: 1 }, { name: 'f', fieldConfig: { type: 'text' } }))).toBe(
'[object Object] 类型应为字符串',
);
expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: { type: 'number' } })).toBeUndefined();
// fieldConfig 未声明 type 时不做类型校验
expect(run('data-source-field-select', 123, { name: 'f', fieldConfig: {} })).toBeUndefined();
dataSourcesState.value = [ dataSourcesState.value = [
{ {
@ -225,7 +234,26 @@ describe('editorTypeMatchRules', () => {
expect(run('data-source-field-select', ['ds1', 'title'], { value: 'key' })).toBeUndefined(); expect(run('data-source-field-select', ['ds1', 'title'], { value: 'key' })).toBeUndefined();
expect(run('data-source-field-select', [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'title'])).toBeUndefined(); expect(run('data-source-field-select', [`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}ds1`, 'title'])).toBeUndefined();
expect(firstLine(run('data-source-field-select', ['ds1', 'missing'], { value: 'key' }))).toBe('值不在可选项中'); // 数据源不存在时直出具体的数据源 id
expect(firstLine(run('data-source-field-select', ['missing', 'title'], { value: 'key' }))).toBe(
'数据源(missing)不存在',
);
expect(firstLine(run('data-source-field-select', ['title'], { dataSourceId: 'missing' }))).toBe(
'数据源(missing)不存在',
);
// 字段路径解析失败时直出具体失败的字段名
expect(firstLine(run('data-source-field-select', ['ds1', 'missing'], { value: 'key' }))).toBe(
'数据源字段(missing)不存在',
);
// 字段路径解析失败时,建议列出失败层级的可用字段名而非数据源 id
expect(run('data-source-field-select', ['ds1', 'missing'], { value: 'key' })).toBe(
'数据源字段(missing)不存在\n\n请使用以下某一个值"title""obj"',
);
expect(run('data-source-field-select', ['ds1', 'obj', 'missing'], { value: 'key' })).toBe(
'数据源字段(missing)不存在\n\n请使用以下某一个值"a"',
);
// 父字段无子字段时无建议
expect(run('data-source-field-select', ['ds1', 'title', 'x'], { value: 'key' })).toBe('数据源字段(x)不存在');
expect( expect(
firstLine( firstLine(
run('data-source-field-select', ['title'], { run('data-source-field-select', ['title'], {

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/element-plus-adapter", "name": "@tmagic/element-plus-adapter",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/form-schema", "name": "@tmagic/form-schema",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/form", "name": "@tmagic/form",
"type": "module", "type": "module",
"sideEffects": [ "sideEffects": [

View File

@ -261,6 +261,18 @@ const formState: FormState = reactive<FormState>({
}, },
}); });
/**
* formState 的内置 key 快照keyProp / values / $emit / fields / post
*
* `extendState` 首次合并前捕获`applyExtendState` 会据此禁止 `extendState`
* 覆盖这些已有字段只能新增字段避免表单核心状态被外部意外改写
*
* 之所以在此处effect 之外捕获而不是在 `applyExtendState` 内动态取
* `watchEffect` 会在依赖变化时重跑若动态取`extendState` 自己新增的字段在第二次
* 合并时也会被当成已有 key而拒绝刷新这里只锁定内置字段即可规避该问题
*/
const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(formState));
/** /**
* `extendState` 的同步段直到第一个 `await` 之前所访问的任何响应式数据 * `extendState` 的同步段直到第一个 `await` 之前所访问的任何响应式数据
* 都会被 `watchEffect` 自动跟踪这样可以兼容历史用法 * 都会被 `watchEffect` 自动跟踪这样可以兼容历史用法
@ -299,7 +311,7 @@ watchEffect(async (onCleanup) => {
} }
if (stale) return; if (stale) return;
applyExtendState(formState, state); applyExtendState(formState, state, reservedStateKeys);
}); });
provide('mForm', formState); provide('mForm', formState);

View File

@ -12,6 +12,7 @@
:disabled="disabled" :disabled="disabled"
@change="changeHandler" @change="changeHandler"
@add-diff-count="onAddDiffCount" @add-diff-count="onAddDiffCount"
:is-in-row="true"
></Container> ></Container>
</TMagicCol> </TMagicCol>
</template> </template>

View File

@ -240,7 +240,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, readonly, ref, toRaw, watch, watchEffect } from 'vue'; import { computed, inject, provide, readonly, ref, toRaw, watch, watchEffect } from 'vue';
import { ArrowDown, ArrowUp, WarningFilled } from '@element-plus/icons-vue'; import { ArrowDown, ArrowUp, WarningFilled } from '@element-plus/icons-vue';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
@ -287,6 +287,8 @@ const props = withDefaults(
size?: string; size?: string;
/** 是否开启对比模式 */ /** 是否开启对比模式 */
isCompare?: boolean; isCompare?: boolean;
/** 是否在行容器中 */
isInRow?: boolean;
}>(), }>(),
{ {
prop: '', prop: '',
@ -476,6 +478,8 @@ watch(
}, },
); );
provide('isInRow', props.isInRow ?? false);
const expandHandler = () => (expand.value = !expand.value); const expandHandler = () => (expand.value = !expand.value);
const key = (config: any) => config[mForm?.keyProps]; const key = (config: any) => config[mForm?.keyProps];

View File

@ -29,6 +29,17 @@ export interface FormDiffConfig {
export const FORM_DIFF_CONFIG_KEY: InjectionKey<FormDiffConfig> = Symbol('mFormDiffConfig'); export const FORM_DIFF_CONFIG_KEY: InjectionKey<FormDiffConfig> = Symbol('mFormDiffConfig');
export const FORM_TYPE_MATCH_VALID_KEY: InjectionKey<ComputedRef<boolean>> = Symbol('mFormTypeMatchValid'); export const FORM_TYPE_MATCH_VALID_KEY: InjectionKey<ComputedRef<boolean>> = Symbol('mFormTypeMatchValid');
/**
* `submitForm` / `validateForm` providedebug
*
* vs-code inject FormItem model
* UI monaco worker / model
*
* onMounted/watch immediate emit change DOM
*
*/
export const FORM_SILENT_MODE_KEY: InjectionKey<boolean> = Symbol('mFormSilentMode');
export interface ValidateError { export interface ValidateError {
message: string; message: string;
field: string; field: string;

View File

@ -16,11 +16,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { type AppContext, type Component, createApp, defineComponent, h, nextTick, type Ref, ref, watch } from 'vue'; import {
type AppContext,
type Component,
createApp,
defineComponent,
h,
nextTick,
provide,
type Ref,
ref,
watch,
} from 'vue';
import { applyExtendState } from './utils/form'; import { applyExtendState } from './utils/form';
import Form from './Form.vue'; import Form from './Form.vue';
import type { ChangeRecord, FormConfig, FormState } from './schema'; import { type ChangeRecord, FORM_SILENT_MODE_KEY, type FormConfig, type FormState } from './schema';
// #region SubmitFormOptions // #region SubmitFormOptions
/** /**
@ -76,6 +87,11 @@ export interface SubmitFormOptions {
*/ */
debug?: boolean; debug?: boolean;
typeMatchValid?: boolean; typeMatchValid?: boolean;
/**
* abort `signal.reason` reject
* `debug`
*/
signal?: AbortSignal;
} }
// #endregion SubmitFormOptions // #endregion SubmitFormOptions
@ -116,7 +132,7 @@ interface MountFormInstanceOptions<T> {
formProps: Record<string, any>; formProps: Record<string, any>;
/** 父级应用上下文用于继承全局组件、指令、provide 等 */ /** 父级应用上下文用于继承全局组件、指令、provide 等 */
appContext?: AppContext | null; appContext?: AppContext | null;
/** 等待表单初始化的最长时间(毫秒),<=0 表示不注册超时 */ /** 等待表单初始化的最长时间(毫秒),<=0 时回退到默认超时以保证兜底清理生效 */
timeout: number; timeout: number;
/** 超时 reject 的错误文案 */ /** 超时 reject 的错误文案 */
timeoutMessage: string; timeoutMessage: string;
@ -124,10 +140,15 @@ interface MountFormInstanceOptions<T> {
hidden?: boolean; hidden?: boolean;
/** 是否跳过超时注册。调试模式等待人工操作,应传 `true` */ /** 是否跳过超时注册。调试模式等待人工操作,应传 `true` */
skipTimeout?: boolean; skipTimeout?: boolean;
/** 外部中断信号abort 时会 reject 并卸载实例、移除容器,用于取消无超时(如 debug的挂载 */
signal?: AbortSignal;
/** 构造 wrapper 组件 */ /** 构造 wrapper 组件 */
createWrapper: FormWrapperFactory<T>; createWrapper: FormWrapperFactory<T>;
} }
/** 未指定或传入非正数 timeout 时的兜底超时(毫秒),保证非 debug 挂载始终能被清理 */
const DEFAULT_MOUNT_TIMEOUT = 10000;
/** /**
* submitForm / validateForm * submitForm / validateForm
* *
@ -138,20 +159,36 @@ interface MountFormInstanceOptions<T> {
* *
*/ */
const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T> => { const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T> => {
const { formProps, appContext, timeout, timeoutMessage, hidden = true, skipTimeout = false, createWrapper } = options; const {
formProps,
appContext,
timeout,
timeoutMessage,
hidden = true,
skipTimeout = false,
signal,
createWrapper,
} = options;
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
// 已中断则直接 reject不创建任何容器/实例
if (signal?.aborted) {
reject(signal.reason ?? new Error('mountFormInstance aborted'));
return;
}
let cleaned = false;
let timer: ReturnType<typeof setTimeout> | null = null;
let onAbort: (() => void) | null = null;
// 用 holder 持有 app使 cleanup 可在 app 创建之前定义const app + 无 TDZ / 无 use-before-define
const instance: { app: ReturnType<typeof createApp> | null } = { app: null };
const container = document.createElement('div'); const container = document.createElement('div');
if (hidden) { if (hidden) {
container.style.display = 'none'; container.style.display = 'none';
} }
document.body.appendChild(container); document.body.appendChild(container);
let cleaned = false;
let timer: ReturnType<typeof setTimeout> | null = null;
// 用 holder 持有 app使 cleanup 可在 app 创建之前定义const app + 无 TDZ / 无 use-before-define
const instance: { app: ReturnType<typeof createApp> | null } = { app: null };
const cleanup = () => { const cleanup = () => {
if (cleaned) return; if (cleaned) return;
cleaned = true; cleaned = true;
@ -159,6 +196,10 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
clearTimeout(timer); clearTimeout(timer);
timer = null; timer = null;
} }
if (signal && onAbort) {
signal.removeEventListener('abort', onAbort);
onAbort = null;
}
try { try {
instance.app?.unmount(); instance.app?.unmount();
} catch { } catch {
@ -167,67 +208,101 @@ const mountFormInstance = <T>(options: MountFormInstanceOptions<T>): Promise<T>
container.parentNode?.removeChild(container); container.parentNode?.removeChild(container);
}; };
const formRef = ref<any>(null); // 支持外部通过 AbortSignal 主动中断debug 模式无超时兜底,若调用方放弃了该 Promise
// 可通过 abort 卸载实例、移除遮罩/容器,避免无限驻留在 DOM 中。
if (signal) {
onAbort = () => {
if (cleaned) return;
reject(signal.reason ?? new Error('mountFormInstance aborted'));
cleanup();
};
signal.addEventListener('abort', onAbort);
}
// 将 extendState 从 formProps 中剥离:不由 Form.vue 的 async watchEffect 异步应用, // 从容器创建到 mount 的全流程统一 try/catch任一步骤createWrapper/createApp/上下文合并/mount
// 而是在 wrapper 中通过 sync watch 在 formRef 就绪后直接写入 formState // 抛错都会走到 cleanup避免已插入 body 的 container 及未挂载的 app 残留导致泄漏。
// 避免 display 等 filterFunction 在首次渲染时读到 undefined。 try {
// 与 CompareForm / FormPanel 中「formRef.value.formState.services = ...」的做法一致。 const formRef = ref<any>(null);
const { extendState, ...restFormProps } = formProps;
const userWrapper = createWrapper({ formRef, formProps: restFormProps, cleanup, resolve, reject }); // 将 extendState 从 formProps 中剥离:不由 Form.vue 的 async watchEffect 异步应用,
// 而是在 wrapper 中通过 sync watch 在 formRef 就绪后直接写入 formState
// 避免 display 等 filterFunction 在首次渲染时读到 undefined。
// 与 CompareForm / FormPanel 中「formRef.value.formState.services = ...」的做法一致。
const { extendState, ...restFormProps } = formProps;
const wrapperComponent = const userWrapper = createWrapper({ formRef, formProps: restFormProps, cleanup, resolve, reject });
typeof extendState === 'function'
const wrapperComponent =
typeof extendState === 'function'
? defineComponent({
name: 'MFormExtendStateInjector',
setup() {
watch(
() => formRef.value,
(form) => {
if (!form) return;
let result: any;
try {
result = extendState(form.formState);
} catch (e) {
console.error('[MForm] extendState failed:', e);
return;
}
// formState 的内置 key 快照:在 extendState 合并前捕获,
// 供 applyExtendState 禁止 extendState 覆盖这些已有字段(只能新增),
// 与 Form.vue 中 reservedStateKeys 的语义保持一致。
const reservedStateKeys = new Set<string | symbol>(Reflect.ownKeys(form.formState));
// 合并逻辑收口在 applyExtendStateprops 派生的只读 getter 字段
// keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
const apply = (state: Record<string, any> | null | undefined) =>
applyExtendState(form.formState, state, reservedStateKeys);
if (result && typeof result.then === 'function') {
result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
} else {
apply(result);
}
},
{ flush: 'sync', immediate: true },
);
return () => h(userWrapper);
},
})
: userWrapper;
// 静默隐藏挂载模式下注入静默标记vs-code 等重型字段组件可据此跳过自身渲染,
// 校验/取值依赖 FormItem 与 model 值,与叶子 UI 组件无关(见 FORM_SILENT_MODE_KEY 注释)。
// 用组件级 provide 而非 app.provideappContext 合并后 app._context.provides 与父级应用
// 共享引用app.provide 会把标记泄漏到父级应用。
const rootComponent = hidden
? defineComponent({ ? defineComponent({
name: 'MFormExtendStateInjector', name: 'MFormSilentProvider',
setup() { setup() {
watch( provide(FORM_SILENT_MODE_KEY, true);
() => formRef.value, return () => h(wrapperComponent);
(form) => {
if (!form) return;
let result: any;
try {
result = extendState(form.formState);
} catch (e) {
console.error('[MForm] extendState failed:', e);
return;
}
// 合并逻辑收口在 applyExtendStateprops 派生的只读 getter 字段
// keyProp 等)以普通字段形式返回时会被跳过并告警,避免 proxy set 抛错
const apply = (state: Record<string, any> | null | undefined) =>
applyExtendState(form.formState, state);
if (result && typeof result.then === 'function') {
result.then(apply, (e: any) => console.error('[MForm] extendState failed:', e));
} else {
apply(result);
}
},
{ flush: 'sync', immediate: true },
);
return () => h(userWrapper);
}, },
}) })
: userWrapper; : wrapperComponent;
const app = createApp(wrapperComponent); const app = createApp(rootComponent);
instance.app = app; instance.app = app;
// 继承父级应用上下文components / directives / provides / config 等) // 继承父级应用上下文components / directives / provides / config 等)
if (appContext) { if (appContext) {
Object.assign(app._context, appContext); Object.assign(app._context, appContext);
} }
if (timeout > 0 && !skipTimeout) { // 非 debug未跳过超时场景始终注册超时兜底timeout 为非正数时回退到默认值,
timer = setTimeout(() => { // 避免表单永不初始化时实例/容器/watcher 无限驻留而泄漏。
if (!cleaned) { if (!skipTimeout) {
reject(new Error(timeoutMessage)); const effectiveTimeout = timeout > 0 ? timeout : DEFAULT_MOUNT_TIMEOUT;
cleanup(); timer = setTimeout(() => {
} if (!cleaned) {
}, timeout); reject(new Error(timeoutMessage));
} cleanup();
}
}, effectiveTimeout);
}
try {
app.mount(container); app.mount(container);
} catch (err) { } catch (err) {
reject(err); reject(err);
@ -425,12 +500,13 @@ const createDebugWrapper = (options: DebugWrapperOptions): Component => {
* ``` * ```
*/ */
export const submitForm = (options: SubmitFormOptions): Promise<any> => { export const submitForm = (options: SubmitFormOptions): Promise<any> => {
const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, ...formProps } = options; const { native, appContext, timeout = 10000, returnChangeRecords, debug = false, signal, ...formProps } = options;
return mountFormInstance<any>({ return mountFormInstance<any>({
formProps, formProps,
appContext, appContext,
timeout, timeout,
signal,
// 调试模式需把表单展示出来;普通模式隐藏挂载 // 调试模式需把表单展示出来;普通模式隐藏挂载
hidden: !debug, hidden: !debug,
// 调试模式等待人工操作,不应用超时 // 调试模式等待人工操作,不应用超时
@ -534,6 +610,11 @@ export interface ValidateFormOptions {
*/ */
debug?: boolean; debug?: boolean;
typeMatchValid?: boolean; typeMatchValid?: boolean;
/**
* abort `signal.reason` reject
* `debug`
*/
signal?: AbortSignal;
} }
// #endregion ValidateFormOptions // #endregion ValidateFormOptions
@ -629,7 +710,7 @@ export const stripTabItemsLazy = (config: FormConfig): FormConfig => {
* ``` * ```
*/ */
export const validateForm = (options: ValidateFormOptions): Promise<string> => { export const validateForm = (options: ValidateFormOptions): Promise<string> => {
const { appContext, timeout = 10000, debug = false, config, ...rest } = options; const { appContext, timeout = 10000, debug = false, config, signal, ...rest } = options;
// 去掉 tab 容器各标签页的 lazy确保懒加载标签页内的字段也参与校验 // 去掉 tab 容器各标签页的 lazy确保懒加载标签页内的字段也参与校验
const formProps = { ...rest, config: stripTabItemsLazy(config) }; const formProps = { ...rest, config: stripTabItemsLazy(config) };
@ -638,6 +719,7 @@ export const validateForm = (options: ValidateFormOptions): Promise<string> => {
formProps, formProps,
appContext, appContext,
timeout, timeout,
signal,
// 调试模式需把表单展示出来;普通模式隐藏挂载 // 调试模式需把表单展示出来;普通模式隐藏挂载
hidden: !debug, hidden: !debug,
// 调试模式等待人工操作,不应用超时 // 调试模式等待人工操作,不应用超时

View File

@ -2,7 +2,6 @@
.m-fields-group-list-item.tmagic-design-card--flat:last-child { .m-fields-group-list-item.tmagic-design-card--flat:last-child {
border: 0; border: 0;
} }
.el-button--text { .el-button--text {
padding: 0; padding: 0;
margin-bottom: 7px; margin-bottom: 7px;
@ -32,9 +31,19 @@
} }
.m-fields-group-list-footer { .m-fields-group-list-footer {
display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 10px; margin-top: 10px;
margin-bottom: 16px; margin-bottom: 16px;
} }
} }
/** 最外层的groupList需要每个item需要增加空白区域界限时增加outer-gorup_list可以实现 */
.m-container-group-list {
&.outer-gorup_list {
> .m-fields-group-list {
> .m-fields-group-list-item {
margin-bottom: 10px;
}
}
}
}

View File

@ -92,4 +92,15 @@
text-align: left; text-align: left;
} }
} }
.m-fields-group-list {
.el-table__empty-block {
.el-table__empty-text {
width: 100%;
background-color: rgba(0, 0, 0, 0.03);
margin-top: 8px;
border-radius: 4px;
}
}
}
} }

View File

@ -449,16 +449,27 @@ export const sortChange = (data: any[], { prop, order }: SortProp) => {
* - accessor `{ get stage() { return ... } }` defineProperty * - accessor `{ get stage() { return ... } }` defineProperty
* `configurable: true` 便 define * `configurable: true` 便 define
* *
* formState props keyProp / popperClass / config / initValues / * extendState formState key
* isCompare / lastValues / parentValues getter setterextendState * `reservedKeys` key keyProp / popperClass /
* key proxy set trap * config / initValues / isCompare / lastValues / parentValues / values / $emit / fields /
* `TypeError: 'set' on proxy: trap returned falsish` * post key
* extendState get 访 *
* `reservedKeys` props getter setter
* proxy set trap
* `TypeError: 'set' on proxy: trap returned falsish`
*/ */
export const applyExtendState = (formState: FormState, state: Record<string, any> | null | undefined): void => { export const applyExtendState = (
formState: FormState,
state: Record<string, any> | null | undefined,
reservedKeys?: Set<string | symbol>,
): void => {
if (!state) return; if (!state) return;
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) { for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(state))) {
if (reservedKeys?.has(key)) {
continue;
}
if (!('value' in descriptor)) { if (!('value' in descriptor)) {
descriptor.configurable = true; descriptor.configurable = true;
Object.defineProperty(formState, key, descriptor); Object.defineProperty(formState, key, descriptor);

View File

@ -142,7 +142,7 @@ const stringifyExampleValue = (value: any): string => {
}; };
// 参考建议中最多展示的可选值个数,超出以「等」省略。 // 参考建议中最多展示的可选值个数,超出以「等」省略。
const MAX_SUGGESTION_OPTIONS = 5; const MAX_SUGGESTION_OPTIONS = 20;
/** /**
* 使xxxxxx * 使xxxxxx
@ -558,6 +558,11 @@ const validateBuiltinTypeMatch = (
return undefined; return undefined;
} }
// text 常与数字(如 id、数量绑定值允许为数字错误建议仍按字符串给出
if (fieldType === 'text' && isNumberValue(value)) {
return undefined;
}
if (typeof value !== 'string') { if (typeof value !== 'string') {
return defaultMessage( return defaultMessage(
message, message,
@ -697,9 +702,13 @@ export const validateTypeMatch = (
return undefined; return undefined;
} }
if (!props.config?.name) {
return undefined;
}
const rawFieldType = 'type' in (props.config || {}) ? props.config.type : ''; const rawFieldType = 'type' in (props.config || {}) ? props.config.type : '';
if (typeof rawFieldType !== 'string' || !rawFieldType) { if (typeof rawFieldType !== 'string' || !rawFieldType) {
return; return undefined;
} }
// 统一将驼峰形式(如 radioGroup归一化为连字符形式radio-group与内置规则的 key 保持一致 // 统一将驼峰形式(如 radioGroup归一化为连字符形式radio-group与内置规则的 key 保持一致

View File

@ -189,9 +189,7 @@ describe('Form.vue —— extendState', () => {
expect(v2).toMatch(/^stage-/); expect(v2).toMatch(/^stage-/);
}); });
test('extendState 以普通字段返回 props 派生的只读字段时跳过并告警,不抛错', async () => { test('extendState 以普通字段返回内置保留字段时静默跳过,其余字段正常合并', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const wrapper = mountForm({ const wrapper = mountForm({
keyProp: 'id', keyProp: 'id',
extendState: () => ({ keyProp: 'custom', config: [], extra: 'ok' }), extendState: () => ({ keyProp: 'custom', config: [], extra: 'ok' }),
@ -201,18 +199,15 @@ describe('Form.vue —— extendState', () => {
await nextTick(); await nextTick();
await nextTick(); await nextTick();
// 只读派生字段保持 props 值,未被覆盖 // 内置保留字段keyProp / config未被 extendState 覆盖
expect((wrapper.vm.formState as any).keyProp).toBe('id'); expect((wrapper.vm.formState as any).keyProp).toBe('id');
expect(Array.isArray((wrapper.vm.formState as any).config)).toBe(true); expect(Array.isArray((wrapper.vm.formState as any).config)).toBe(true);
// 其他字段正常合并 // 非保留字段正常合并
expect((wrapper.vm.formState as any).extra).toBe('ok'); expect((wrapper.vm.formState as any).extra).toBe('ok');
expect(warnSpy).toHaveBeenCalled();
expect(wrapper.find('.m-form').exists()).toBe(true); expect(wrapper.find('.m-form').exists()).toBe(true);
warnSpy.mockRestore();
}); });
test('extendState 以 get 访问器返回只读字段同名 key 时允许显式覆盖', async () => { test('extendState 以 get 访问器返回内置保留字段同名 key 时仍被拦截,无法覆盖', async () => {
const wrapper = mountForm({ const wrapper = mountForm({
keyProp: 'id', keyProp: 'id',
extendState: () => extendState: () =>
@ -228,7 +223,8 @@ describe('Form.vue —— extendState', () => {
await nextTick(); await nextTick();
await nextTick(); await nextTick();
expect((wrapper.vm.formState as any).keyProp).toBe('custom-key'); // keyProp 属于内置保留字段,即使以访问器形式返回也不允许覆盖
expect((wrapper.vm.formState as any).keyProp).toBe('id');
}); });
test('extendState 同步段读到的响应式数据变化时会重跑', async () => { test('extendState 同步段读到的响应式数据变化时会重跑', async () => {

View File

@ -16,18 +16,29 @@
* limitations under the License. * limitations under the License.
*/ */
import { afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; import { afterEach, beforeAll, describe, expect, test, vi } from 'vitest';
import { type AppContext, createApp, defineComponent, h, nextTick } from 'vue'; import { type AppContext, createApp, defineComponent, h, inject, nextTick } from 'vue';
import MagicForm, { submitForm } from '@form/index'; import MagicForm, { FORM_SILENT_MODE_KEY, submitForm, validateForm } from '@form/index';
import ElementPlus from 'element-plus'; import ElementPlus from 'element-plus';
let appContext: AppContext; let appContext: AppContext;
// 探针字段:挂载时注入静默标记并记录,用于验证 submitForm/validateForm 的 provide 行为
const silentProbeValues: (boolean | undefined)[] = [];
const SilentProbe = defineComponent({
name: 'MFieldsSilentProbe',
setup() {
silentProbeValues.push(inject(FORM_SILENT_MODE_KEY, undefined));
return () => h('div');
},
});
beforeAll(() => { beforeAll(() => {
// 构造一个父级 app把 element-plus 与 m-form 插件装上, // 构造一个父级 app把 element-plus 与 m-form 插件装上,
// 之后通过 appContext 传给 submitForm 复用全局注册 // 之后通过 appContext 传给 submitForm 复用全局注册
const parentApp = createApp(defineComponent({ render: () => h('div') })); const parentApp = createApp(defineComponent({ render: () => h('div') }));
parentApp.use(ElementPlus); parentApp.use(ElementPlus);
parentApp.use(MagicForm); parentApp.use(MagicForm);
parentApp.component('m-fields-silent-probe', SilentProbe);
appContext = parentApp._context; appContext = parentApp._context;
}); });
@ -79,9 +90,7 @@ describe('submitForm', () => {
expect(extendState).toHaveBeenCalled(); expect(extendState).toHaveBeenCalled();
}); });
test('extendState 返回 keyProp 等只读派生字段时不抛错且正常 resolve', async () => { test('extendState 返回 keyProp 等内置保留字段时静默跳过且正常 resolve', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const values = await submitForm({ const values = await submitForm({
config: [{ type: 'text', name: 'text', text: 'text' }], config: [{ type: 'text', name: 'text', text: 'text' }],
initValues: { text: 'foo' }, initValues: { text: 'foo' },
@ -89,10 +98,8 @@ describe('submitForm', () => {
appContext, appContext,
}); });
// keyProp 属于内置保留字段,被静默跳过,不污染最终 values
expect(values).toEqual({ text: 'foo' }); expect(values).toEqual({ text: 'foo' });
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
}); });
test('在嵌套 items 配置下也能正确 resolve', async () => { test('在嵌套 items 配置下也能正确 resolve', async () => {
@ -216,6 +223,44 @@ describe('submitForm', () => {
expect(caught).toBeInstanceOf(Error); expect(caught).toBeInstanceOf(Error);
}); });
test('静默(隐藏挂载)模式下向字段 provide FORM_SILENT_MODE_KEY=true', async () => {
silentProbeValues.length = 0;
const values = await submitForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
appContext,
});
expect(values).toEqual({ text: 'hello' });
expect(silentProbeValues).toEqual([true]);
});
test('静默标记不会泄漏到父级应用的 provides', async () => {
silentProbeValues.length = 0;
await submitForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
appContext,
});
expect((appContext as any).provides[FORM_SILENT_MODE_KEY as symbol]).toBeUndefined();
});
test('validateForm 静默模式下同样 provide FORM_SILENT_MODE_KEY=true', async () => {
silentProbeValues.length = 0;
const error = await validateForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
appContext,
});
expect(error).toBe('');
expect(silentProbeValues).toEqual([true]);
});
test('timeout > 0 时会注册定时器timeout <= 0 时不注册', async () => { test('timeout > 0 时会注册定时器timeout <= 0 时不注册', async () => {
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout'); const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');
@ -354,6 +399,24 @@ describe('submitForm —— debug 模式', () => {
expect(document.body.querySelector('.m-form')).toBeNull(); expect(document.body.querySelector('.m-form')).toBeNull();
}); });
test('debug 模式不提供静默标记(表单可见,字段应正常渲染)', async () => {
silentProbeValues.length = 0;
const pending = submitForm({
config: [{ type: 'silent-probe', name: 'text', text: 'text' }],
initValues: { text: 'hello' },
debug: true,
appContext,
});
await nextTick();
await nextTick();
expect(silentProbeValues).toEqual([undefined]);
findButton('确定').click();
await pending;
});
test('debug 模式不注册超时定时器(等待人工操作)', async () => { test('debug 模式不注册超时定时器(等待人工操作)', async () => {
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout'); const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');

View File

@ -16,9 +16,10 @@
* limitations under the License. * limitations under the License.
*/ */
import { describe, expect, test } from 'vitest'; import { describe, expect, test, vi } from 'vitest';
import type { FormState } from '@form/index'; import type { FormState } from '@form/index';
import { import {
applyExtendState,
createObjectProp, createObjectProp,
createValues, createValues,
datetimeFormatter, datetimeFormatter,
@ -1014,3 +1015,130 @@ describe('createObjectProp', () => {
expect(createObjectProp('a.b.c', 'newKey', 'c')).toBe('a.b.newKey'); expect(createObjectProp('a.b.c', 'newKey', 'c')).toBe('a.b.newKey');
}); });
}); });
describe('applyExtendState', () => {
test('state 为 null 或 undefined 时不修改 formState', () => {
const formState = { a: 1 } as any;
applyExtendState(formState, null);
applyExtendState(formState, undefined);
expect(formState).toEqual({ a: 1 });
});
test('普通字段data descriptor以新增字段形式合并进 formState', () => {
const formState = {} as any;
applyExtendState(formState, { foo: 'bar', num: 1 });
expect(formState.foo).toBe('bar');
expect(formState.num).toBe(1);
});
test('accessor descriptor 按原样 define 且读时求值', () => {
const formState = {} as any;
let count = 0;
const state = {};
Object.defineProperty(state, 'stage', {
get() {
count += 1;
return 'stageValue';
},
enumerable: true,
configurable: true,
});
applyExtendState(formState, state);
// 读时才求值
expect(count).toBe(0);
expect(formState.stage).toBe('stageValue');
expect(count).toBe(1);
});
test('accessor descriptor 强制 configurable=true可再次 define 不报错', () => {
const formState = {} as any;
const state = {};
// 原始描述符 configurable 为 false
Object.defineProperty(state, 'stage', {
get: () => 'v',
enumerable: true,
configurable: false,
});
applyExtendState(formState, state);
const descriptor = Object.getOwnPropertyDescriptor(formState, 'stage');
expect(descriptor?.configurable).toBe(true);
// 再次合并(重新 define不应抛出
expect(() => applyExtendState(formState, state)).not.toThrow();
});
test('reservedKeys 命中的 key 被跳过,不覆盖已有内置字段', () => {
const formState = { values: { origin: true } } as any;
const reservedKeys = new Set<string | symbol>(['values']);
applyExtendState(formState, { values: { changed: true }, extra: 1 }, reservedKeys);
// reserved key 未被覆盖
expect(formState.values).toEqual({ origin: true });
// 非 reserved 的新字段正常新增
expect(formState.extra).toBe(1);
});
test('reservedKeys 对 accessor 形式的同名 key 同样跳过', () => {
const formState = { keyProp: 'origin' } as any;
const reservedKeys = new Set<string | symbol>(['keyProp']);
const state = {};
Object.defineProperty(state, 'keyProp', {
get: () => 'override',
enumerable: true,
configurable: true,
});
applyExtendState(formState, state, reservedKeys);
expect(formState.keyProp).toBe('origin');
});
test('未传 reservedKeys 时,只读 getter 字段被跳过并告警', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const formState = {} as any;
Object.defineProperty(formState, 'keyProp', {
get: () => 'readonly',
enumerable: true,
configurable: true,
});
applyExtendState(formState, { keyProp: 'newValue' });
expect(formState.keyProp).toBe('readonly');
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
});
test('未传 reservedKeys 时,存在 setter 的字段可被赋值', () => {
let inner = 'old';
const formState = {} as any;
Object.defineProperty(formState, 'writable', {
get: () => inner,
set: (v: string) => {
inner = v;
},
enumerable: true,
configurable: true,
});
applyExtendState(formState, { writable: 'new' });
expect(formState.writable).toBe('new');
});
test('未传 reservedKeys 时,普通可写字段正常覆盖赋值', () => {
const formState = { editable: 'old' } as any;
applyExtendState(formState, { editable: 'new' });
expect(formState.editable).toBe('new');
});
});

View File

@ -21,6 +21,7 @@ import type { FormState } from '@form/index';
import { getRules } from '@form/utils/form'; import { getRules } from '@form/utils/form';
import { import {
clearTypeMatchRules, clearTypeMatchRules,
createTypeMatchValidator,
deleteTypeMatchRule, deleteTypeMatchRule,
getTypeMatchRule, getTypeMatchRule,
registerTypeMatchRule, registerTypeMatchRule,
@ -51,9 +52,12 @@ const mForm: FormState = {
}; };
const propsOf = (config: Record<string, any>, model: Record<string, any> = {}) => ({ const propsOf = (config: Record<string, any>, model: Record<string, any> = {}) => ({
config, config: {
name: 'field',
...config,
},
model, model,
prop: config.name || 'field', prop: config.name || config.prop || 'field',
}); });
describe('validateTypeMatch', () => { describe('validateTypeMatch', () => {
@ -64,6 +68,19 @@ describe('validateTypeMatch', () => {
expect(validateTypeMatch([], mForm, propsOf({ type: 'select', multiple: true }))).toBeUndefined(); expect(validateTypeMatch([], mForm, propsOf({ type: 'select', multiple: true }))).toBeUndefined();
}); });
test('config 未配置 name 时跳过校验', () => {
const noNameProps = { config: { type: 'text' }, model: {} };
expect(validateTypeMatch(123, mForm, noNameProps)).toBeUndefined();
expect(validateTypeMatch({ a: 1 }, mForm, { config: { type: 'number' }, model: {} })).toBeUndefined();
expect(
validateTypeMatch('bad', mForm, {
config: { type: 'select', options: [{ text: 'A', value: 'a' }] },
model: {},
}),
).toBeUndefined();
expect(validateTypeMatch(123, mForm, { config: undefined, model: {} })).toBeUndefined();
});
test('0 / false 不视为空值', () => { test('0 / false 不视为空值', () => {
expect(validateTypeMatch(0, mForm, propsOf({ type: 'number' }))).toBeUndefined(); expect(validateTypeMatch(0, mForm, propsOf({ type: 'number' }))).toBeUndefined();
expect(validateTypeMatch(false, mForm, propsOf({ type: 'switch' }))).toBeUndefined(); expect(validateTypeMatch(false, mForm, propsOf({ type: 'switch' }))).toBeUndefined();
@ -74,12 +91,17 @@ describe('validateTypeMatch', () => {
expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'hidden' }))).toBeUndefined(); expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'hidden' }))).toBeUndefined();
}); });
test('text 期望 string', () => { test('text 期望 string,允许 number', () => {
expect(validateTypeMatch('ok', mForm, propsOf({ type: 'text' }))).toBeUndefined(); expect(validateTypeMatch('ok', mForm, propsOf({ type: 'text' }))).toBeUndefined();
expect(validateTypeMatch(1, mForm, propsOf({ type: 'text' }))).toBe( expect(validateTypeMatch(1, mForm, propsOf({ type: 'text' }))).toBeUndefined();
'1 类型应为字符串\n\n请参考以下示例值"文本内容"', expect(validateTypeMatch(0, mForm, propsOf({ type: 'text' }))).toBeUndefined();
expect(validateTypeMatch(NaN, mForm, propsOf({ type: 'text' }))).toBe(
'NaN 类型应为字符串\n\n请参考以下示例值"文本内容"',
); );
expect(validateTypeMatch(1, mForm, propsOf({ type: 'text' }), '自定义错误')).toBe('自定义错误'); expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'text' }))).toBe(
'[object Object] 类型应为字符串\n\n请参考以下示例值"文本内容"',
);
expect(validateTypeMatch(true, mForm, propsOf({ type: 'text' }), '自定义错误')).toBe('自定义错误');
}); });
test('text filter=number 时期望 number', () => { test('text filter=number 时期望 number', () => {
@ -174,13 +196,14 @@ describe('validateTypeMatch', () => {
expect(validateTypeMatch(3, mForm, propsOf(config))).toBe('3 不在可选项中\n\n请使用以下某一个值12'); expect(validateTypeMatch(3, mForm, propsOf(config))).toBe('3 不在可选项中\n\n请使用以下某一个值12');
}); });
test('可选项超过 5 个时建议仅展示前 5 个并以「等」省略', () => { test('可选项超过 20 个时建议仅展示前 20 个并以「等」省略', () => {
const values = Array.from({ length: 21 }, (_, i) => i + 1);
const config = { const config = {
type: 'select', type: 'select',
options: [1, 2, 3, 4, 5, 6, 7].map((v) => ({ text: `${v}`, value: v })), options: values.map((v) => ({ text: `${v}`, value: v })),
}; };
expect(validateTypeMatch(99, mForm, propsOf(config))).toBe( expect(validateTypeMatch(99, mForm, propsOf(config))).toBe(
'99 不在可选项中\n\n请使用以下某一个值12345 等', `99 不在可选项中\n\n请使用以下某一个值${values.slice(0, 20).join('')}`,
); );
}); });
@ -386,6 +409,11 @@ describe('validateTypeMatch', () => {
).toBe('a 类型应为数组\n\n请参考以下示例值["a"]'); ).toBe('a 类型应为数组\n\n请参考以下示例值["a"]');
}); });
test('radio-group / checkbox-group 无 options 时跳过校验', () => {
expect(validateTypeMatch('a', mForm, propsOf({ type: 'radio-group' }))).toBeUndefined();
expect(validateTypeMatch('a', mForm, propsOf({ type: 'checkbox-group' }))).toBeUndefined();
});
test('timerange 按 valueFormat 校验', () => { test('timerange 按 valueFormat 校验', () => {
expect(validateTypeMatch(['12:00:00', '13:00:00'], mForm, propsOf({ type: 'timerange' }))).toBeUndefined(); expect(validateTypeMatch(['12:00:00', '13:00:00'], mForm, propsOf({ type: 'timerange' }))).toBeUndefined();
expect(validateTypeMatch(['bad'], mForm, propsOf({ type: 'timerange' }))).toMatch( expect(validateTypeMatch(['bad'], mForm, propsOf({ type: 'timerange' }))).toMatch(
@ -504,8 +532,8 @@ describe('validateTypeMatch', () => {
expect(validateTypeMatch('1', mForm, propsOf({ type: 'number', defaultValue: () => Promise.resolve(123) }))).toBe( expect(validateTypeMatch('1', mForm, propsOf({ type: 'number', defaultValue: () => Promise.resolve(123) }))).toBe(
'1 类型应为数字\n\n请参考以下示例值123', '1 类型应为数字\n\n请参考以下示例值123',
); );
expect(validateTypeMatch(1, mForm, propsOf({ type: 'text', defaultValue: async () => '示例' }))).toBe( expect(validateTypeMatch({ a: 1 }, mForm, propsOf({ type: 'text', defaultValue: async () => '示例' }))).toBe(
'1 类型应为字符串\n\n请参考以下示例值"文本内容"', '[object Object] 类型应为字符串\n\n请参考以下示例值"文本内容"',
); );
}); });
}); });
@ -523,7 +551,7 @@ describe('getRules typeMatch', () => {
const rules: any = [{ typeMatch: true, message: '类型错误' }]; const rules: any = [{ typeMatch: true, message: '类型错误' }];
const newRules: any = getRules(mForm, rules, propsOf({ type: 'text' })); const newRules: any = getRules(mForm, rules, propsOf({ type: 'text' }));
const callback = vi.fn(); const callback = vi.fn();
newRules[0].validator({}, 123, callback); newRules[0].validator({}, { a: 1 }, callback);
expect(callback).toHaveBeenCalledWith(expect.any(Error)); expect(callback).toHaveBeenCalledWith(expect.any(Error));
expect(callback.mock.calls[0][0].message).toBe('类型错误'); expect(callback.mock.calls[0][0].message).toBe('类型错误');
}); });
@ -568,7 +596,7 @@ describe('getRules tdesign validator', () => {
const rules: any = [{ typeMatch: true, message: '类型错误' }]; const rules: any = [{ typeMatch: true, message: '类型错误' }];
const newRules: any = getRules(mForm, rules, propsOf({ type: 'text' })); const newRules: any = getRules(mForm, rules, propsOf({ type: 'text' }));
// TDesign 调用签名validator(val) // TDesign 调用签名validator(val)
await expect(newRules[0].validator(123)).resolves.toEqual({ await expect(newRules[0].validator({ a: 1 })).resolves.toEqual({
result: false, result: false,
message: '类型错误', message: '类型错误',
}); });
@ -694,3 +722,30 @@ describe('plugin typeMatchRules', () => {
expect(validateTypeMatch('bad', mForm, propsOf({ type: 'install-type' }))).toBe('install error'); expect(validateTypeMatch('bad', mForm, propsOf({ type: 'install-type' }))).toBe('install error');
}); });
}); });
describe('createTypeMatchValidator', () => {
beforeEach(() => {
clearTypeMatchRules();
});
afterEach(() => {
clearTypeMatchRules();
});
test('validateTypeMatch 抛异常时仍执行原始 validator', () => {
registerTypeMatchRule('throw', () => {
throw new Error('boom');
});
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const originalValidator = vi.fn();
const validator = createTypeMatchValidator(mForm, propsOf({ type: 'throw' }), {
validator: originalValidator,
} as any);
validator({}, 'value', () => {}, {}, {});
expect(originalValidator).toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalled();
errorSpy.mockRestore();
});
});

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/schema", "name": "@tmagic/schema",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/stage", "name": "@tmagic/stage",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -84,6 +84,8 @@ export default class ActionManager extends EventEmitter {
private containerHighlightDuration: number; private containerHighlightDuration: number;
/** 将组件加入容器的操作方式 */ /** 将组件加入容器的操作方式 */
private containerHighlightType?: ContainerHighlightType; private containerHighlightType?: ContainerHighlightType;
/** 是否仅在新增组件时才启用将组件加入容器 */
private containerHighlightAddOnly = false;
private isAltKeydown = false; private isAltKeydown = false;
private getTargetElement: GetTargetElement; private getTargetElement: GetTargetElement;
private getElementsFromPoint: GetElementsFromPoint; private getElementsFromPoint: GetElementsFromPoint;
@ -124,6 +126,7 @@ export default class ActionManager extends EventEmitter {
this.containerHighlightClassName = config.containerHighlightClassName || CONTAINER_HIGHLIGHT_CLASS_NAME; this.containerHighlightClassName = config.containerHighlightClassName || CONTAINER_HIGHLIGHT_CLASS_NAME;
this.containerHighlightDuration = config.containerHighlightDuration || defaultContainerHighlightDuration; this.containerHighlightDuration = config.containerHighlightDuration || defaultContainerHighlightDuration;
this.containerHighlightType = config.containerHighlightType; this.containerHighlightType = config.containerHighlightType;
this.containerHighlightAddOnly = config.containerHighlightAddOnly ?? false;
this.disabledMultiSelect = config.disabledMultiSelect ?? false; this.disabledMultiSelect = config.disabledMultiSelect ?? false;
this.alwaysMultiSelect = config.alwaysMultiSelect ?? false; this.alwaysMultiSelect = config.alwaysMultiSelect ?? false;
this.getTargetElement = config.getTargetElement; this.getTargetElement = config.getTargetElement;
@ -428,10 +431,15 @@ export default class ActionManager extends EventEmitter {
* 12 * 12
* @param event * @param event
* @param excludeElList * @param excludeElList
* @param isAdd containerHighlightAddOnly
* @returns timeoutIdtimeout * @returns timeoutIdtimeout
*/ */
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined { public delayedMarkContainer(
if (this.canAddToContainer()) { event: MouseEvent,
excludeElList: Element[] = [],
isAdd = false,
): NodeJS.Timeout | undefined {
if (this.canAddToContainer(isAdd)) {
return globalThis.setTimeout(() => { return globalThis.setTimeout(() => {
this.addContainerHighlightClassName(event, excludeElList); this.addContainerHighlightClassName(event, excludeElList);
}, this.containerHighlightDuration); }, this.containerHighlightDuration);
@ -609,9 +617,13 @@ export default class ActionManager extends EventEmitter {
} }
/** /**
* alt模式则是按住alt+ * alt模式则是按住alt+
* containerHighlightAddOnly时
* @param isAdd
*/ */
private canAddToContainer(): boolean { private canAddToContainer(isAdd = false): boolean {
if (this.containerHighlightAddOnly && !isAdd) return false;
return ( return (
this.containerHighlightType === ContainerHighlightType.DEFAULT || this.containerHighlightType === ContainerHighlightType.DEFAULT ||
(this.containerHighlightType === ContainerHighlightType.ALT && this.isAltKeydown) (this.containerHighlightType === ContainerHighlightType.ALT && this.isAltKeydown)
@ -624,10 +636,10 @@ export default class ActionManager extends EventEmitter {
*/ */
private markContainerEnd(): HTMLElement | null { private markContainerEnd(): HTMLElement | null {
const doc = this.getRenderDocument(); const doc = this.getRenderDocument();
if (doc && this.canAddToContainer()) { if (!doc) return null;
return removeClassNameByClassName(doc, this.containerHighlightClassName);
} const markedContainer = removeClassNameByClassName(doc, this.containerHighlightClassName);
return null; return this.canAddToContainer() ? markedContainer : null;
} }
private initMouseEvent(): void { private initMouseEvent(): void {

View File

@ -263,10 +263,15 @@ export default class StageCore extends EventEmitter {
* 12 * 12
* @param event * @param event
* @param excludeElList * @param excludeElList
* @param isAdd containerHighlightAddOnly
* @returns timeoutIdtimeout * @returns timeoutIdtimeout
*/ */
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined { public delayedMarkContainer(
return this.actionManager?.delayedMarkContainer(event, excludeElList); event: MouseEvent,
excludeElList: Element[] = [],
isAdd = false,
): NodeJS.Timeout | undefined {
return this.actionManager?.delayedMarkContainer(event, excludeElList, isAdd);
} }
public getMoveableOption<K extends keyof MoveableOptions>(key: K): MoveableOptions[K] | undefined { public getMoveableOption<K extends keyof MoveableOptions>(key: K): MoveableOptions[K] | undefined {
@ -368,6 +373,7 @@ export default class StageCore extends EventEmitter {
containerHighlightClassName: config.containerHighlightClassName, containerHighlightClassName: config.containerHighlightClassName,
containerHighlightDuration: config.containerHighlightDuration, containerHighlightDuration: config.containerHighlightDuration,
containerHighlightType: config.containerHighlightType, containerHighlightType: config.containerHighlightType,
containerHighlightAddOnly: config.containerHighlightAddOnly,
moveableOptions: config.moveableOptions, moveableOptions: config.moveableOptions,
container: this.mask!.content, container: this.mask!.content,
disabledDragStart: config.disabledDragStart, disabledDragStart: config.disabledDragStart,

View File

@ -53,7 +53,11 @@ export type GetTargetElement = (id: Id) => HTMLElement | null;
/** render提供的接口通过坐标获得坐标下所有HTML元素数组 */ /** render提供的接口通过坐标获得坐标下所有HTML元素数组 */
export type GetElementsFromPoint = (point: Point) => HTMLElement[]; export type GetElementsFromPoint = (point: Point) => HTMLElement[];
export type GetRenderDocument = () => Document | undefined; export type GetRenderDocument = () => Document | undefined;
export type DelayedMarkContainer = (event: MouseEvent, exclude: Element[]) => NodeJS.Timeout | undefined; export type DelayedMarkContainer = (
event: MouseEvent,
exclude?: Element[],
isAdd?: boolean,
) => NodeJS.Timeout | undefined;
export type MarkContainerEnd = () => HTMLElement | null; export type MarkContainerEnd = () => HTMLElement | null;
export type GetRootContainer = () => HTMLDivElement | undefined; export type GetRootContainer = () => HTMLDivElement | undefined;
@ -74,6 +78,11 @@ export interface StageCoreConfig {
containerHighlightClassName?: string; containerHighlightClassName?: string;
containerHighlightDuration?: number; containerHighlightDuration?: number;
containerHighlightType?: ContainerHighlightType; containerHighlightType?: ContainerHighlightType;
/**
*
* false
*/
containerHighlightAddOnly?: boolean;
moveableOptions?: CustomizeMoveableOptions; moveableOptions?: CustomizeMoveableOptions;
/** runtime 的HTML地址可以是一个HTTP地址如果和编辑器不同域需要设置跨域也可以是一个相对或绝对路径 */ /** runtime 的HTML地址可以是一个HTTP地址如果和编辑器不同域需要设置跨域也可以是一个相对或绝对路径 */
runtimeUrl?: string; runtimeUrl?: string;
@ -102,6 +111,8 @@ export interface ActionManagerConfig {
containerHighlightClassName?: string; containerHighlightClassName?: string;
containerHighlightDuration?: number; containerHighlightDuration?: number;
containerHighlightType?: ContainerHighlightType; containerHighlightType?: ContainerHighlightType;
/** 见 StageCoreConfig.containerHighlightAddOnly */
containerHighlightAddOnly?: boolean;
moveableOptions?: CustomizeMoveableOptions; moveableOptions?: CustomizeMoveableOptions;
disabledDragStart?: boolean; disabledDragStart?: boolean;
disabledMultiSelect?: boolean; disabledMultiSelect?: boolean;

View File

@ -275,6 +275,84 @@ describe('ActionManager - 交互与事件', () => {
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(true); expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(true);
}); });
test('delayedMarkContainer 未配置 containerHighlightType 时不标记容器', () => {
am = new ActionManager(createConfig({ isContainer: async () => true }));
expect(am.delayedMarkContainer(mouseAtOrigin())).toBeUndefined();
});
test('delayedMarkContainer alt 模式下需按住 alt 才标记容器', () => {
am = new ActionManager(
createConfig({
containerHighlightType: ContainerHighlightType.ALT,
isContainer: async () => true,
}),
);
expect(am.delayedMarkContainer(mouseAtOrigin())).toBeUndefined();
(am as any).isAltKeydown = true;
expect(am.delayedMarkContainer(mouseAtOrigin())).toBeDefined();
});
test('containerHighlightAddOnly 开启时拖动已有组件不标记容器', () => {
const containerEl = globalThis.document.createElement('div');
containerEl.dataset.tmagicId = 'container_1';
am = new ActionManager(
createConfig({
containerHighlightType: ContainerHighlightType.DEFAULT,
containerHighlightAddOnly: true,
getElementsFromPoint: () => [containerEl],
isContainer: async () => true,
}),
);
expect(am.delayedMarkContainer(mouseAtOrigin(), [globalThis.document.createElement('div')])).toBeUndefined();
vi.advanceTimersByTime(900);
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(false);
});
test('containerHighlightAddOnly 开启时新增组件仍按 containerHighlightType 标记容器', async () => {
const containerEl = globalThis.document.createElement('div');
containerEl.dataset.tmagicId = 'container_1';
am = new ActionManager(
createConfig({
containerHighlightType: ContainerHighlightType.DEFAULT,
containerHighlightAddOnly: true,
getElementsFromPoint: () => [containerEl],
isContainer: async () => true,
}),
);
expect(am.delayedMarkContainer(mouseAtOrigin(), [], true)).toBeDefined();
vi.advanceTimersByTime(900);
await Promise.resolve();
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(true);
});
test('containerHighlightAddOnly 开启且 containerHighlightType 为 alt 时新增组件也需按住 alt', () => {
am = new ActionManager(
createConfig({
containerHighlightType: ContainerHighlightType.ALT,
containerHighlightAddOnly: true,
isContainer: async () => true,
}),
);
expect(am.delayedMarkContainer(mouseAtOrigin(), [], true)).toBeUndefined();
(am as any).isAltKeydown = true;
expect(am.delayedMarkContainer(mouseAtOrigin(), [], true)).toBeDefined();
});
test('containerHighlightAddOnly 开启时结束标记仍清理高亮但不返回容器', () => {
const containerEl = globalThis.document.createElement('div');
containerEl.classList.add('tmagic-stage-container-highlight');
globalThis.document.body.appendChild(containerEl);
am = new ActionManager(
createConfig({
containerHighlightType: ContainerHighlightType.DEFAULT,
containerHighlightAddOnly: true,
}),
);
expect((am as any).markContainerEnd()).toBeNull();
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(false);
});
test('updateMoveableOptions / getDragStatus 可调用', () => { test('updateMoveableOptions / getDragStatus 可调用', () => {
am = new ActionManager(createConfig()); am = new ActionManager(createConfig());
expect(() => am.updateMoveableOptions()).not.toThrow(); expect(() => am.updateMoveableOptions()).not.toThrow();

View File

@ -181,6 +181,16 @@ describe('StageCore', () => {
stage.destroy(); stage.destroy();
}); });
test('delayedMarkContainer 将新增标识透传给 actionManager', async () => {
const { host, stage } = createStage();
await stage.mount(host);
const spy = vi.spyOn(stage.actionManager!, 'delayedMarkContainer');
const event = mouseAtOrigin();
stage.delayedMarkContainer(event, [], true);
expect(spy).toHaveBeenCalledWith(event, [], true);
stage.destroy();
});
test('autoScrollIntoView 选中时调用 mask.observerIntersection', async () => { test('autoScrollIntoView 选中时调用 mask.observerIntersection', async () => {
const host = globalThis.document.createElement('div'); const host = globalThis.document.createElement('div');
globalThis.document.body.appendChild(host); globalThis.document.body.appendChild(host);

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/table", "name": "@tmagic/table",
"type": "module", "type": "module",
"sideEffects": [ "sideEffects": [

View File

@ -17,6 +17,7 @@
:width="action.subActionConfig?.popoverWidth" :width="action.subActionConfig?.popoverWidth"
:popper-class="action.subActionConfig?.popoverClass" :popper-class="action.subActionConfig?.popoverClass"
:destroy-on-close="action.subActionConfig?.popoverDestroyOnClose ?? true" :destroy-on-close="action.subActionConfig?.popoverDestroyOnClose ?? true"
@clickoutside="popoverVisible = false"
> >
<template #reference> <template #reference>
<TMagicButton <TMagicButton
@ -84,8 +85,15 @@
> >
</template> </template>
<script lang="ts" setup> <script lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
// sub-actions
const activePopoverId = ref<symbol | null>(null);
</script>
<script lang="ts" setup>
import { watch } from 'vue';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue'; import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
@ -117,7 +125,23 @@ const props = withDefaults(
}, },
); );
const popoverId = Symbol('sub-actions-popover');
const popoverVisible = ref(false); const popoverVisible = ref(false);
watch(popoverVisible, (visible) => {
if (visible) {
activePopoverId.value = popoverId;
} else if (activePopoverId.value === popoverId) {
activePopoverId.value = null;
}
});
watch(activePopoverId, (id) => {
if (id !== popoverId && popoverVisible.value) {
popoverVisible.value = false;
}
});
const togglePopover = () => { const togglePopover = () => {
popoverVisible.value = !popoverVisible.value; popoverVisible.value = !popoverVisible.value;
}; };
@ -134,7 +158,8 @@ const actionHandler = async (action: ColumnActionConfig, row: any, index: number
} else { } else {
await action.handler?.(row, index); await action.handler?.(row, index);
} }
action.after?.(row, index); await action.after?.(row, index);
popoverVisible.value = false;
}; };
const save = async (index: number, config: ColumnConfig) => { const save = async (index: number, config: ColumnConfig) => {

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/tdesign-vue-next-adapter", "name": "@tmagic/tdesign-vue-next-adapter",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,5 +1,5 @@
{ {
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"name": "@tmagic/utils", "name": "@tmagic/utils",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,

View File

@ -1,6 +1,6 @@
{ {
"name": "tmagic-playground", "name": "tmagic-playground",
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
@ -12,11 +12,11 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@tmagic/core": "1.8.0-beta.14", "@tmagic/core": "1.8.0-beta.17",
"@tmagic/design": "1.8.0-beta.14", "@tmagic/design": "1.8.0-beta.17",
"@tmagic/editor": "1.8.0-beta.14", "@tmagic/editor": "1.8.0-beta.17",
"@tmagic/element-plus-adapter": "1.8.0-beta.14", "@tmagic/element-plus-adapter": "1.8.0-beta.17",
"@tmagic/tdesign-vue-next-adapter": "1.8.0-beta.14", "@tmagic/tdesign-vue-next-adapter": "1.8.0-beta.17",
"@tmagic/tmagic-form-runtime": "1.1.3", "@tmagic/tmagic-form-runtime": "1.1.3",
"element-plus": "^2.14.3", "element-plus": "^2.14.3",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

228
pnpm-lock.yaml generated
View File

@ -564,23 +564,23 @@ importers:
specifier: ^2.3.2 specifier: ^2.3.2
version: 2.3.2(vue@3.5.40(typescript@6.0.3)) version: 2.3.2(vue@3.5.40(typescript@6.0.3))
'@tmagic/core': '@tmagic/core':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(typescript@6.0.3) version: 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/design': '@tmagic/design':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) version: 1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/editor': '@tmagic/editor':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) version: 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/element-plus-adapter': '@tmagic/element-plus-adapter':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) version: 1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/tdesign-vue-next-adapter': '@tmagic/tdesign-vue-next-adapter':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) version: 1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/tmagic-form-runtime': '@tmagic/tmagic-form-runtime':
specifier: 1.1.3 specifier: 1.1.3
version: 1.1.3(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) version: 1.1.3(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
element-plus: element-plus:
specifier: ^2.14.3 specifier: ^2.14.3
version: 2.14.3(vue@3.5.40(typescript@6.0.3)) version: 2.14.3(vue@3.5.40(typescript@6.0.3))
@ -920,14 +920,14 @@ importers:
runtime/react: runtime/react:
dependencies: dependencies:
'@tmagic/core': '@tmagic/core':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(typescript@6.0.3) version: 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/react-runtime-help': '@tmagic/react-runtime-help':
specifier: 0.2.2 specifier: 0.2.2
version: 0.2.2(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.18.1)(react@18.3.1)(typescript@6.0.3) version: 0.2.2(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.18.1)(react@18.3.1)(typescript@6.0.3)
'@tmagic/stage': '@tmagic/stage':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) version: 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
axios: axios:
specifier: ^1.13.2 specifier: ^1.13.2
version: 1.13.2 version: 1.13.2
@ -942,8 +942,8 @@ importers:
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
devDependencies: devDependencies:
'@tmagic/cli': '@tmagic/cli':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(typescript@6.0.3) version: 1.8.0-beta.17(typescript@6.0.3)
'@types/fs-extra': '@types/fs-extra':
specifier: ^11.0.4 specifier: ^11.0.4
version: 11.0.4 version: 11.0.4
@ -1018,14 +1018,14 @@ importers:
runtime/vue: runtime/vue:
dependencies: dependencies:
'@tmagic/core': '@tmagic/core':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(typescript@6.0.3) version: 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/stage': '@tmagic/stage':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) version: 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
'@tmagic/vue-runtime-help': '@tmagic/vue-runtime-help':
specifier: ^2.0.4 specifier: ^2.0.4
version: 2.0.4(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) version: 2.0.4(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
axios: axios:
specifier: ^1.13.2 specifier: ^1.13.2
version: 1.13.2 version: 1.13.2
@ -1034,8 +1034,8 @@ importers:
version: 3.5.40(typescript@6.0.3) version: 3.5.40(typescript@6.0.3)
devDependencies: devDependencies:
'@tmagic/cli': '@tmagic/cli':
specifier: 1.8.0-beta.14 specifier: 1.8.0-beta.17
version: 1.8.0-beta.14(typescript@6.0.3) version: 1.8.0-beta.17(typescript@6.0.3)
'@types/fs-extra': '@types/fs-extra':
specifier: ^11.0.4 specifier: ^11.0.4
version: 11.0.4 version: 11.0.4
@ -2997,8 +2997,8 @@ packages:
'@sxzz/popperjs-es@2.11.8': '@sxzz/popperjs-es@2.11.8':
resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==} resolution: {integrity: sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==}
'@tmagic/cli@1.8.0-beta.14': '@tmagic/cli@1.8.0-beta.17':
resolution: {integrity: sha512-y6tTnO+NIi1xYrTDK1b/33ipJmM7jytqCMDcdNOYuG2ng416w+CxWyguAvcCwFPL50pJQereboTVsMDLFnjvBg==} resolution: {integrity: sha512-M2HORy79DarjNTaB08huag0TIhkD0vTctZEltH/YrPrenCvv5izD3PQK3/QacnDZqgXC7TFepw0fZvHcUnlwPw==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -3025,8 +3025,8 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/core@1.8.0-beta.14': '@tmagic/core@1.8.0-beta.17':
resolution: {integrity: sha512-DrUnNGTz+LK4pSCTIkmEvAfvNJglPXd+MLLu79lWJgfjyEmjrJ1rsdw6KprPzn6LkHBL9bKi/ZXGAcxCVs7P6w==} resolution: {integrity: sha512-VmtY2lM5eMYwjXUaUh+r1cFVq075rKuXeo5sPOfUKVaGeIhEYO5L7X4IEEv21TcAD91vUabbZUKVwYN4SVO7Cw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
typescript: ^6.0.3 typescript: ^6.0.3
@ -3054,11 +3054,11 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/data-source@1.8.0-beta.14': '@tmagic/data-source@1.8.0-beta.17':
resolution: {integrity: sha512-mpmxcL/tvJx8L7QV3YWz9VM2tEc5WW/9r6JK2Dsv+La6C4wbArZVYoJMhGIxPs4MBCB9VPU+HbOpVLSapwuVaQ==} resolution: {integrity: sha512-Gw+VrNF4WgHe+H5pgzFjpeoymkQjZ/hM50irxeAmUGF4NZdkPULzig+NHxbLt4wzwLsS6JAvHvlfitajv5NW9w==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/core': 1.8.0-beta.14 '@tmagic/core': 1.8.0-beta.17
typescript: ^6.0.3 typescript: ^6.0.3
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
@ -3086,12 +3086,12 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/dep@1.8.0-beta.14': '@tmagic/dep@1.8.0-beta.17':
resolution: {integrity: sha512-XPdj2XBXNGTSau9cexQsMerV8MxLlnF0KWIOJlhaZKtrtpkmFgdxhB6LksDxB3H5MsHwmev5qMgWQFc0665EWQ==} resolution: {integrity: sha512-22yQTbWcKwvByGBfjzGNlnfKqbv+aPJ34bC6weYRZV5vFLZS8QitdQLgM74TT+7nagzeV8uWZVPCGtrr6JUtEw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/schema': 1.8.0-beta.14 '@tmagic/schema': 1.8.0-beta.17
'@tmagic/utils': 1.8.0-beta.14 '@tmagic/utils': 1.8.0-beta.17
typescript: ^6.0.3 typescript: ^6.0.3
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
@ -3107,8 +3107,8 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/design@1.8.0-beta.14': '@tmagic/design@1.8.0-beta.17':
resolution: {integrity: sha512-Ia803Ps59npRlBwMh+HiRMzjbspn6xz0nG1CG5b6785njo4yEQS4fd6yOEuTIJy+25zXb0/Gl5ytc7AOjhl+OA==} resolution: {integrity: sha512-9ZxLqd+WrqEBRbxQd8daDaMmiuDDHl/Pbq8jib1bgX84iVO+AbFQxV0WFIzcqygFj2aE3KoszjIeZzjlSja6Kg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
typescript: ^6.0.3 typescript: ^6.0.3
@ -3129,11 +3129,11 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/editor@1.8.0-beta.14': '@tmagic/editor@1.8.0-beta.17':
resolution: {integrity: sha512-AmtB3lA3QVdunNXU6bRd+hWalNf5RiePpsi638foGIaD934mNf+POop5HCWbfuBkU7GDbSElT4cqrvE2EGLK7A==} resolution: {integrity: sha512-T9gDbIk/o1UKgd5ioFrJHssFp/P2VbRNHGGjAouH4ujMw7WkNtL8CFdpOMTWG8krCaWxkJ4IUQg/WhVDjotLHg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/core': 1.8.0-beta.14 '@tmagic/core': 1.8.0-beta.17
monaco-editor: '^0.55.1 ' monaco-editor: '^0.55.1 '
type-fest: ^5.2.0 type-fest: ^5.2.0
typescript: ^6.0.3 typescript: ^6.0.3
@ -3144,11 +3144,11 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/element-plus-adapter@1.8.0-beta.14': '@tmagic/element-plus-adapter@1.8.0-beta.17':
resolution: {integrity: sha512-MRqnFIyoPA8wfa6fIm4Tie8f3qZjWICq95PZm9GWZMAuOIUNM1GUj86iOZExhte52+xYV4xNC3Rh+gsRntRULQ==} resolution: {integrity: sha512-szV/jaxbW6DEkI90sDeqscobnu5QNn8yx+3JyfonY0tsJ4HEzaNE0xMUTaGeDNtjj6xAYIX8ld/uz6tChmgajA==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/design': 1.8.0-beta.14 '@tmagic/design': 1.8.0-beta.17
element-plus: ^2.14.3 element-plus: ^2.14.3
typescript: ^6.0.3 typescript: ^6.0.3
vue: ^3.5.40 vue: ^3.5.40
@ -3165,8 +3165,8 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/form-schema@1.8.0-beta.14': '@tmagic/form-schema@1.8.0-beta.17':
resolution: {integrity: sha512-dVoW9YEb8A1Vcc/xZiM3lzRlhyN8h4rOkX+7Y03ZphYxvQ4x/lIuPkE07sOY8XV6aJJmwEeCDcrO+DY26a/C4Q==} resolution: {integrity: sha512-fESNNvRQFsJcNv1y1wuA9vXvJnOxi5qdRf2I8Ph5O7cCPjPltoFWI2m/otEws1iPiMRhjHLQJrLupPkILd1Kwg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
typescript: ^6.0.3 typescript: ^6.0.3
@ -3187,13 +3187,13 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/form@1.8.0-beta.14': '@tmagic/form@1.8.0-beta.17':
resolution: {integrity: sha512-OuMn2hTugeu8LmprproMsf4BL58GdpfgBSGdzzTKQUX18CPUZoXKSNMrlAJY2P4ZzJoSk0W8InDMbF/Os+SmKA==} resolution: {integrity: sha512-ttol7h4U69ypS+Q7pslvNO3XwdCa3XnC9vIEsH1aSfoIHPRBRp7GC3IJ80orkUkxeTciXmDZtrxO0Jy5epMIMA==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/design': 1.8.0-beta.14 '@tmagic/design': 1.8.0-beta.17
'@tmagic/form-schema': 1.8.0-beta.14 '@tmagic/form-schema': 1.8.0-beta.17
'@tmagic/utils': 1.8.0-beta.14 '@tmagic/utils': 1.8.0-beta.17
typescript: ^6.0.3 typescript: ^6.0.3
vue: ^3.5.40 vue: ^3.5.40
peerDependenciesMeta: peerDependenciesMeta:
@ -3235,8 +3235,8 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/schema@1.8.0-beta.14': '@tmagic/schema@1.8.0-beta.17':
resolution: {integrity: sha512-cjqm0LtXRoaCpqTJxKsPBuQh8RsdRf+EpPstMz7UKHf8DagHDHds/UK3mjDYPj7npKes04E0xw/o5bIsOjYmWQ==} resolution: {integrity: sha512-9BXKkhXuZtmp9xp5fKxZ2noMs7lC4YKxtehmv7Q3YEWFklbQBuIhYbpl/BTpdy7eYM7zvOQQN3ow2PdCdGwfOw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
typescript: ^6.0.3 typescript: ^6.0.3
@ -3264,11 +3264,11 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/stage@1.8.0-beta.14': '@tmagic/stage@1.8.0-beta.17':
resolution: {integrity: sha512-OlDNwlxwV1pZgqyV5ICrbSjrBuqGU20cMagBi0Faq0kSZJ2hDNhhzxXXE1O4lUpZf0hXGl+rKvUjtV+UJ5l+aQ==} resolution: {integrity: sha512-fmI9VPRDv6rSzjfB6s/6B/6NO8IwHrIGqy5poN345okBIp3g+Mi1vdRDRWUl/Fyan8em5uVzDhseAa5qVTi88Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/core': 1.8.0-beta.14 '@tmagic/core': 1.8.0-beta.17
typescript: ^6.0.3 typescript: ^6.0.3
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
@ -3286,24 +3286,24 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/table@1.8.0-beta.14': '@tmagic/table@1.8.0-beta.17':
resolution: {integrity: sha512-gCRepIj7OgW+8DJP7Gs5MMXiTweYdOFnr+OLP1CxH8Fpt8d0rtT+06e/GkGiwKwlW0Dve+yrPE41MVRNbrBwYQ==} resolution: {integrity: sha512-Vk+ZU2ibXFuZ+7JfFl4XlVIJrFu+u2j+WgXGE8Tp91pfE+I8588D2PywLJnw2X2LJE1rlivhyHrkE7PsVSc51Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/design': 1.8.0-beta.14 '@tmagic/design': 1.8.0-beta.17
'@tmagic/form': 1.8.0-beta.14 '@tmagic/form': 1.8.0-beta.17
'@tmagic/utils': 1.8.0-beta.14 '@tmagic/utils': 1.8.0-beta.17
typescript: ^6.0.3 typescript: ^6.0.3
vue: ^3.5.40 vue: ^3.5.40
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
'@tmagic/tdesign-vue-next-adapter@1.8.0-beta.14': '@tmagic/tdesign-vue-next-adapter@1.8.0-beta.17':
resolution: {integrity: sha512-iKEvhPbPK8+QVWxXHTF6jHrbC433vUAm8cChaaT0zm0KEffw+XSpPpdGP2Xqx0Cmd64oDVW4yPY3ZQn3sbk2xA==} resolution: {integrity: sha512-nvOfCFrimeIQfUX16gIE/jEI8Qo9YKhGOm7aH3uFHxz5QJoK1DWLgHFm34s55ZJwtt+z+w6tHO0Oe2zsbs4YWw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/design': 1.8.0-beta.14 '@tmagic/design': 1.8.0-beta.17
tdesign-vue-next: ^1.17.1 tdesign-vue-next: ^1.17.1
typescript: ^6.0.3 typescript: ^6.0.3
vue: ^3.5.40 vue: ^3.5.40
@ -3346,11 +3346,11 @@ packages:
typescript: typescript:
optional: true optional: true
'@tmagic/utils@1.8.0-beta.14': '@tmagic/utils@1.8.0-beta.17':
resolution: {integrity: sha512-yi5e6pNiSFU0IMc9vBRMBXjHcMrSoosVHCiwvxEHMK0VKJYcV+n408O39UEZIupOtBvl7d6Z91yk1hG0nTk38w==} resolution: {integrity: sha512-2rHDYffWbIYa++kYmTarD1DD6lgHJtRFNzXuhO3dZlzFE/KG5XK4v5/IOa4CNbuLyJhwWm10UdywqlYK1P6o0A==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@tmagic/schema': 1.8.0-beta.14 '@tmagic/schema': 1.8.0-beta.17
typescript: ^6.0.3 typescript: ^6.0.3
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
@ -8737,7 +8737,7 @@ snapshots:
'@sxzz/popperjs-es@2.11.8': {} '@sxzz/popperjs-es@2.11.8': {}
'@tmagic/cli@1.8.0-beta.14(typescript@6.0.3)': '@tmagic/cli@1.8.0-beta.17(typescript@6.0.3)':
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
chokidar: 3.6.0 chokidar: 3.6.0
@ -8772,12 +8772,12 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/core@1.8.0-beta.14(typescript@6.0.3)': '@tmagic/core@1.8.0-beta.17(typescript@6.0.3)':
dependencies: dependencies:
'@tmagic/data-source': 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/data-source': 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
'@tmagic/dep': 1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3) '@tmagic/dep': 1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)
'@tmagic/schema': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/schema': 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/utils': 1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/utils': 1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
events: 3.3.0 events: 3.3.0
lodash-es: 4.18.1 lodash-es: 4.18.1
optionalDependencies: optionalDependencies:
@ -8801,9 +8801,9 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/data-source@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3)': '@tmagic/data-source@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)':
dependencies: dependencies:
'@tmagic/core': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/core': 1.8.0-beta.17(typescript@6.0.3)
deep-state-observer: 5.5.14 deep-state-observer: 5.5.14
events: 3.3.0 events: 3.3.0
lodash-es: 4.18.1 lodash-es: 4.18.1
@ -8824,10 +8824,10 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/dep@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)': '@tmagic/dep@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)':
dependencies: dependencies:
'@tmagic/schema': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/schema': 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/utils': 1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/utils': 1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
@ -8838,7 +8838,7 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
'@popperjs/core': 2.11.8 '@popperjs/core': 2.11.8
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
@ -8872,15 +8872,15 @@ snapshots:
- '@tmagic/form-schema' - '@tmagic/form-schema'
- '@tmagic/schema' - '@tmagic/schema'
'@tmagic/editor@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/editor@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
'@element-plus/icons-vue': 2.3.2(vue@3.5.40(typescript@6.0.3)) '@element-plus/icons-vue': 2.3.2(vue@3.5.40(typescript@6.0.3))
'@tmagic/core': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/core': 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/design': 1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/design': 1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/form': 1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/form': 1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/stage': 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/stage': 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
'@tmagic/table': 1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/table': 1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/utils': 1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/utils': 1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
buffer: 6.0.3 buffer: 6.0.3
deep-object-diff: 1.1.9 deep-object-diff: 1.1.9
emmet-monaco-es: 5.7.0(monaco-editor@0.55.1) emmet-monaco-es: 5.7.0(monaco-editor@0.55.1)
@ -8900,9 +8900,9 @@ snapshots:
- '@tmagic/form-schema' - '@tmagic/form-schema'
- '@tmagic/schema' - '@tmagic/schema'
'@tmagic/element-plus-adapter@1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/element-plus-adapter@1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
'@tmagic/design': 1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/design': 1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
element-plus: 2.14.3(vue@3.5.40(typescript@6.0.3)) element-plus: 2.14.3(vue@3.5.40(typescript@6.0.3))
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
optionalDependencies: optionalDependencies:
@ -8914,9 +8914,9 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3)': '@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3)':
dependencies: dependencies:
'@tmagic/schema': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/schema': 1.8.0-beta.17(typescript@6.0.3)
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
@ -8934,13 +8934,13 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/form@1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/form@1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
'@element-plus/icons-vue': 2.3.2(vue@3.5.40(typescript@6.0.3)) '@element-plus/icons-vue': 2.3.2(vue@3.5.40(typescript@6.0.3))
'@popperjs/core': 2.11.8 '@popperjs/core': 2.11.8
'@tmagic/design': 1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/design': 1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/form-schema': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/form-schema': 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/utils': 1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/utils': 1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
dayjs: 1.11.21 dayjs: 1.11.21
lodash-es: 4.18.1 lodash-es: 4.18.1
sortablejs: 1.15.6 sortablejs: 1.15.6
@ -8948,13 +8948,13 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/react-runtime-help@0.2.2(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.18.1)(react@18.3.1)(typescript@6.0.3)': '@tmagic/react-runtime-help@0.2.2(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.18.1)(react@18.3.1)(typescript@6.0.3)':
dependencies: dependencies:
lodash-es: 4.18.1 lodash-es: 4.18.1
react: 18.3.1 react: 18.3.1
optionalDependencies: optionalDependencies:
'@tmagic/core': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/core': 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/stage': 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/stage': 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/schema@1.7.0(typescript@6.0.3)': '@tmagic/schema@1.7.0(typescript@6.0.3)':
@ -8965,7 +8965,7 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/schema@1.8.0-beta.14(typescript@6.0.3)': '@tmagic/schema@1.8.0-beta.17(typescript@6.0.3)':
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
@ -8995,10 +8995,10 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/stage@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3)': '@tmagic/stage@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)':
dependencies: dependencies:
'@scena/guides': 0.29.2 '@scena/guides': 0.29.2
'@tmagic/core': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/core': 1.8.0-beta.17(typescript@6.0.3)
'@zumer/snapdom': 2.8.0 '@zumer/snapdom': 2.8.0
events: 3.3.0 events: 3.3.0
keycon: 1.4.0 keycon: 1.4.0
@ -9018,32 +9018,32 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/table@1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/table@1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
'@element-plus/icons-vue': 2.3.2(vue@3.5.40(typescript@6.0.3)) '@element-plus/icons-vue': 2.3.2(vue@3.5.40(typescript@6.0.3))
'@tmagic/design': 1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/design': 1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/form': 1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/form': 1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
'@tmagic/utils': 1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/utils': 1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
lodash-es: 4.18.1 lodash-es: 4.18.1
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/tdesign-vue-next-adapter@1.8.0-beta.14(@tmagic/design@1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/tdesign-vue-next-adapter@1.8.0-beta.17(@tmagic/design@1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
'@tmagic/design': 1.8.0-beta.14(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/design': 1.8.0-beta.17(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
tdesign-vue-next: 1.17.3(vue@3.5.40(typescript@6.0.3)) tdesign-vue-next: 1.17.3(vue@3.5.40(typescript@6.0.3))
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/tmagic-form-runtime@1.1.3(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/tmagic-form-runtime@1.1.3(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)))(element-plus@2.14.3(vue@3.5.40(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
'@tmagic/editor': 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.14(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) '@tmagic/editor': 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.17(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))
element-plus: 2.14.3(vue@3.5.40(typescript@6.0.3)) element-plus: 2.14.3(vue@3.5.40(typescript@6.0.3))
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
optionalDependencies: optionalDependencies:
'@tmagic/core': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/core': 1.8.0-beta.17(typescript@6.0.3)
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/utils@1.7.0(@tmagic/schema@1.7.0(typescript@6.0.3))(typescript@6.0.3)': '@tmagic/utils@1.7.0(@tmagic/schema@1.7.0(typescript@6.0.3))(typescript@6.0.3)':
@ -9060,19 +9060,19 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/utils@1.8.0-beta.14(@tmagic/schema@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3)': '@tmagic/utils@1.8.0-beta.17(@tmagic/schema@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)':
dependencies: dependencies:
'@tmagic/schema': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/schema': 1.8.0-beta.17(typescript@6.0.3)
lodash-es: 4.18.1 lodash-es: 4.18.1
optionalDependencies: optionalDependencies:
typescript: 6.0.3 typescript: 6.0.3
'@tmagic/vue-runtime-help@2.0.4(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': '@tmagic/vue-runtime-help@2.0.4(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))':
dependencies: dependencies:
vue: 3.5.40(typescript@6.0.3) vue: 3.5.40(typescript@6.0.3)
optionalDependencies: optionalDependencies:
'@tmagic/core': 1.8.0-beta.14(typescript@6.0.3) '@tmagic/core': 1.8.0-beta.17(typescript@6.0.3)
'@tmagic/stage': 1.8.0-beta.14(@tmagic/core@1.8.0-beta.14(typescript@6.0.3))(typescript@6.0.3) '@tmagic/stage': 1.8.0-beta.17(@tmagic/core@1.8.0-beta.17(typescript@6.0.3))(typescript@6.0.3)
typescript: 6.0.3 typescript: 6.0.3
'@tybys/wasm-util@0.10.3': '@tybys/wasm-util@0.10.3':

View File

@ -1,6 +1,6 @@
{ {
"name": "runtime-react", "name": "runtime-react",
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"type": "module", "type": "module",
"private": true, "private": true,
"engines": { "engines": {
@ -16,16 +16,16 @@
"build:playground": "node scripts/build.mjs --type=playground" "build:playground": "node scripts/build.mjs --type=playground"
}, },
"dependencies": { "dependencies": {
"@tmagic/core": "1.8.0-beta.14", "@tmagic/core": "1.8.0-beta.17",
"@tmagic/react-runtime-help": "0.2.2", "@tmagic/react-runtime-help": "0.2.2",
"@tmagic/stage": "1.8.0-beta.14", "@tmagic/stage": "1.8.0-beta.17",
"axios": "^1.13.2", "axios": "^1.13.2",
"qrcode": "^1.5.0", "qrcode": "^1.5.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@tmagic/cli": "1.8.0-beta.14", "@tmagic/cli": "1.8.0-beta.17",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "runtime-vue", "name": "runtime-vue",
"version": "1.8.0-beta.14", "version": "1.8.0-beta.17",
"type": "module", "type": "module",
"private": true, "private": true,
"engines": { "engines": {
@ -16,14 +16,14 @@
"build:playground": "node scripts/build.mjs --type=playground" "build:playground": "node scripts/build.mjs --type=playground"
}, },
"dependencies": { "dependencies": {
"@tmagic/core": "1.8.0-beta.14", "@tmagic/core": "1.8.0-beta.17",
"@tmagic/stage": "1.8.0-beta.14", "@tmagic/stage": "1.8.0-beta.17",
"@tmagic/vue-runtime-help": "^2.0.4", "@tmagic/vue-runtime-help": "^2.0.4",
"axios": "^1.13.2", "axios": "^1.13.2",
"vue": "catalog:" "vue": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@tmagic/cli": "1.8.0-beta.14", "@tmagic/cli": "1.8.0-beta.17",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/node": "^24.0.10", "@types/node": "^24.0.10",
"@vitejs/plugin-legacy": "^8.0.1", "@vitejs/plugin-legacy": "^8.0.1",