mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-10-28 05:20:43 +00:00
feat(form): 支持 table/group-list 行操作按钮文案与 props 配置
通过 ActionButtonConfig 统一 delete/copy/move/operate 等按钮的 text 与 props,并补充对应单测。
This commit is contained in:
parent
8ac1a38601
commit
cbd0442ffc
@ -832,6 +832,14 @@ export interface AddButtonConfig {
|
||||
}
|
||||
// #endregion AddButtonConfig
|
||||
|
||||
// #region ActionButtonConfig
|
||||
/**
|
||||
* 行操作按钮。与 `addButtonConfig` 相同:`text` 是文案,`props` 透传给按钮。
|
||||
* 不支持吸底。未配置 `text` 时使用内置中文。
|
||||
*/
|
||||
export type ActionButtonConfig = Omit<AddButtonConfig, 'sticky'>;
|
||||
// #endregion ActionButtonConfig
|
||||
|
||||
// #region GroupListHeaderConfig
|
||||
/** group-list 卡片标题;仅 sticky 开启时吸顶 */
|
||||
export interface GroupListHeaderConfig {
|
||||
@ -862,6 +870,16 @@ export interface TableGroupListCommonConfig extends FormItem {
|
||||
/** 新增后滚动到最后一项(group-list 形态,默认关闭) */
|
||||
scrollLastItemIntoView?: boolean;
|
||||
addButtonConfig?: AddButtonConfig;
|
||||
/** 删除按钮,text 默认「删除」 */
|
||||
deleteButtonConfig?: ActionButtonConfig;
|
||||
/** 复制按钮,text 默认「复制」 */
|
||||
copyButtonConfig?: ActionButtonConfig;
|
||||
/** 上移按钮,text 默认「上移」。仅 group-list 形态 */
|
||||
moveUpButtonConfig?: ActionButtonConfig;
|
||||
/** 下移按钮,text 默认「下移」。仅 group-list 形态 */
|
||||
moveDownButtonConfig?: ActionButtonConfig;
|
||||
/** 移动至按钮,text 默认「移动至」。仅 group-list 形态 */
|
||||
moveToButtonConfig?: ActionButtonConfig;
|
||||
/** group-list 形态的标题吸顶(默认关闭);table 形态忽略 */
|
||||
header?: GroupListHeaderConfig;
|
||||
}
|
||||
@ -898,6 +916,11 @@ export interface TableConfig<T = never> extends TableGroupListCommonConfig {
|
||||
showIndex?: boolean;
|
||||
/** 操作栏宽度 */
|
||||
operateColWidth?: number | string;
|
||||
/**
|
||||
* 操作列。`text` 是列标题,默认「操作」。
|
||||
* `props` 透传给列(如 width、align、fixed、minWidth),可覆盖 operateColWidth / fixed。
|
||||
*/
|
||||
operateColConfig?: ActionButtonConfig;
|
||||
pagination?: boolean;
|
||||
/** 表格标题前缀 ,新增xxx,暂无xxx数据,表明表格内容是什么 */
|
||||
titlePrefix?: string;
|
||||
@ -919,6 +942,8 @@ export interface TableConfig<T = never> extends TableGroupListCommonConfig {
|
||||
dropActionButtonIcon?: any;
|
||||
copyActionButtonIcon?: any;
|
||||
deleteActionButtonIcon?: any;
|
||||
/** 拖动排序按钮,text 作为提示,默认「拖动排序」 */
|
||||
dropSortButtonConfig?: ActionButtonConfig;
|
||||
/** 是否显示全屏按钮 */
|
||||
enableFullscreen?: boolean;
|
||||
fixed?: boolean | 'left' | 'right';
|
||||
|
||||
@ -19,8 +19,9 @@
|
||||
type="primary"
|
||||
:icon="DocumentCopy"
|
||||
:disabled="disabled"
|
||||
v-bind="config.copyButtonConfig?.props"
|
||||
@click="copyHandler"
|
||||
>复制</TMagicButton
|
||||
>{{ actionButtonText(config.copyButtonConfig, 'copy') }}</TMagicButton
|
||||
>
|
||||
|
||||
<template v-if="movable && !isCompare">
|
||||
@ -30,8 +31,9 @@
|
||||
size="default"
|
||||
:disabled="disabled"
|
||||
:icon="Top"
|
||||
v-bind="config.moveUpButtonConfig?.props"
|
||||
@click="changeOrder(-1)"
|
||||
>上移</TMagicButton
|
||||
>{{ actionButtonText(config.moveUpButtonConfig, 'moveUp') }}</TMagicButton
|
||||
>
|
||||
<TMagicButton
|
||||
v-show="index !== length - 1"
|
||||
@ -39,8 +41,9 @@
|
||||
size="default"
|
||||
:disabled="disabled"
|
||||
:icon="Bottom"
|
||||
v-bind="config.moveDownButtonConfig?.props"
|
||||
@click="changeOrder(1)"
|
||||
>下移</TMagicButton
|
||||
>{{ actionButtonText(config.moveDownButtonConfig, 'moveDown') }}</TMagicButton
|
||||
>
|
||||
</template>
|
||||
|
||||
@ -58,8 +61,9 @@
|
||||
type="primary"
|
||||
:icon="Position"
|
||||
:disabled="disabled"
|
||||
v-bind="config.moveToButtonConfig?.props"
|
||||
@click="moveSpecifyLocationVisible = true"
|
||||
>移动至</TMagicButton
|
||||
>{{ actionButtonText(config.moveToButtonConfig, 'moveTo') }}</TMagicButton
|
||||
>
|
||||
</template>
|
||||
<div>
|
||||
@ -81,7 +85,10 @@
|
||||
</TMagicPopover>
|
||||
|
||||
<!-- v-if 必须挂在 Tooltip 上:子节点为 false 时 ElOnlyChild 会报 no valid child node found -->
|
||||
<TMagicTooltip v-if="!isCompare" :content="`删除 ${title}`">
|
||||
<TMagicTooltip
|
||||
v-if="!isCompare"
|
||||
:content="`${actionButtonText(config.deleteButtonConfig, 'delete')} ${title}`"
|
||||
>
|
||||
<TMagicButton
|
||||
v-show="showDelete"
|
||||
size="default"
|
||||
@ -89,6 +96,7 @@
|
||||
class="delete-button"
|
||||
:icon="Delete"
|
||||
:disabled="disabled"
|
||||
v-bind="config.deleteButtonConfig?.props"
|
||||
@click="removeHandler"
|
||||
></TMagicButton>
|
||||
</TMagicTooltip>
|
||||
@ -122,6 +130,7 @@ import { ArrowDown, ArrowRight, Bottom, Delete, DocumentCopy, Position, Top } fr
|
||||
import { TMagicButton, TMagicCard, TMagicIcon, TMagicInputNumber, TMagicPopover, TMagicTooltip } from '@tmagic/design';
|
||||
|
||||
import type { ContainerChangeEventData, FormState, GroupListConfig } from '../schema';
|
||||
import { actionButtonText } from '../utils/actionButton';
|
||||
import { appendProp, filterFunction } from '../utils/form';
|
||||
import { getGroupListRowConfig } from '../utils/tableGroupList';
|
||||
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<slot name="operateCol" :scope="{ $index: index, row: row }"></slot>
|
||||
<TMagicTooltip v-if="config.dropSort && config.dropSortHandle" content="拖动排序" placement="left-start">
|
||||
<TMagicTooltip
|
||||
v-if="config.dropSort && config.dropSortHandle"
|
||||
:content="actionButtonText(config.dropSortButtonConfig, 'dropSort')"
|
||||
placement="left-start"
|
||||
>
|
||||
<TMagicButton
|
||||
size="small"
|
||||
link
|
||||
:class="{ 'tmagic-form-table-drag-target': config.dropSortHandle }"
|
||||
:icon="config.dropActionButtonIcon || Sort"
|
||||
v-bind="config.dropSortButtonConfig?.props"
|
||||
>
|
||||
</TMagicButton>
|
||||
</TMagicTooltip>
|
||||
@ -15,11 +20,12 @@
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
title="删除"
|
||||
:title="actionButtonText(config.deleteButtonConfig, 'delete')"
|
||||
:icon="flat ? undefined : config.deleteActionButtonIcon || Delete"
|
||||
v-bind="config.deleteButtonConfig?.props"
|
||||
@click="removeHandler(index + 1 + currentPage * pageSize - 1)"
|
||||
>
|
||||
<template v-if="flat">删除</template>
|
||||
<template v-if="flat">{{ actionButtonText(config.deleteButtonConfig, 'delete') }}</template>
|
||||
</TMagicButton>
|
||||
|
||||
<TMagicButton
|
||||
@ -27,12 +33,13 @@
|
||||
link
|
||||
size="small"
|
||||
type="primary"
|
||||
title="复制"
|
||||
:title="actionButtonText(config.copyButtonConfig, 'copy')"
|
||||
:icon="flat ? undefined : config.copyActionButtonIcon || DocumentCopy"
|
||||
:disabled="disabled"
|
||||
v-bind="config.copyButtonConfig?.props"
|
||||
@click="copyHandler(index + 1 + currentPage * pageSize - 1)"
|
||||
>
|
||||
<template v-if="flat">复制</template>
|
||||
<template v-if="flat">{{ actionButtonText(config.copyButtonConfig, 'copy') }}</template>
|
||||
</TMagicButton>
|
||||
</template>
|
||||
|
||||
@ -44,6 +51,7 @@ import { cloneDeep } from 'lodash-es';
|
||||
import { TMagicButton, TMagicTooltip } from '@tmagic/design';
|
||||
|
||||
import type { FormState, TableConfig } from '@form/schema';
|
||||
import { actionButtonText } from '@form/utils/actionButton';
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import { type TableColumnOptions, TMagicIcon, TMagicTooltip } from '@tmagic/desi
|
||||
import type { FormItemConfig, FormState } from '@tmagic/form-schema';
|
||||
|
||||
import type { ContainerChangeEventData } from '@form/schema';
|
||||
import { actionButtonText } from '@form/utils/actionButton';
|
||||
import { isGlobalFlat } from '@form/utils/config';
|
||||
import { appendProp, display as displayFunc, getDataByPage, sortArray } from '@form/utils/form';
|
||||
import { isTableColumnRendered, makeTableColumnConfig } from '@form/utils/tableGroupList';
|
||||
@ -112,11 +113,12 @@ export const useTableColumns = (
|
||||
|
||||
const actionColumn = {
|
||||
props: {
|
||||
label: '操作',
|
||||
fixed: actionFixed,
|
||||
|
||||
width: props.config.operateColWidth ?? (props.config.dropSortHandle && props.config.dropSort ? 132 : 112),
|
||||
align: 'center',
|
||||
fixed: actionFixed,
|
||||
width: props.config.operateColWidth ?? (props.config.dropSortHandle && props.config.dropSort ? 132 : 112),
|
||||
// props 覆盖列上的默认属性(宽度、对齐、fixed 等),标题仍以 text 为准
|
||||
...props.config.operateColConfig?.props,
|
||||
label: actionButtonText(props.config.operateColConfig, 'operate'),
|
||||
},
|
||||
cell: ({ row, $index }: any) =>
|
||||
h(ActionsColumn, {
|
||||
|
||||
35
packages/form/src/utils/actionButton.ts
Normal file
35
packages/form/src/utils/actionButton.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ActionButtonConfig } from '@tmagic/form-schema';
|
||||
|
||||
/** table / group-list 行操作的内置文案。空 text 与未配置一样回退到这里,与 addButtonConfig 一致 */
|
||||
export const DEFAULT_ACTION_BUTTON_TEXT = {
|
||||
operate: '操作',
|
||||
delete: '删除',
|
||||
copy: '复制',
|
||||
dropSort: '拖动排序',
|
||||
moveUp: '上移',
|
||||
moveDown: '下移',
|
||||
moveTo: '移动至',
|
||||
} as const;
|
||||
|
||||
export const actionButtonText = (
|
||||
config: ActionButtonConfig | undefined,
|
||||
key: keyof typeof DEFAULT_ACTION_BUTTON_TEXT,
|
||||
): string => config?.text || DEFAULT_ACTION_BUTTON_TEXT[key];
|
||||
@ -29,14 +29,25 @@ const createMForm = (context: Record<string, any>): FormState =>
|
||||
() => context,
|
||||
);
|
||||
|
||||
const mountColumn = (config: any, context: Record<string, any> = { env: 'prod' }) =>
|
||||
mount(ActionsColumn as any, {
|
||||
const COMPONENT_PROP_KEYS = ['flat', 'disabled', 'sortKey', 'model'] as const;
|
||||
|
||||
const mountColumn = (config: any, context: Record<string, any> = { env: 'prod' }) => {
|
||||
const tableConfig = { ...config };
|
||||
const extra: Record<string, any> = {};
|
||||
for (const key of COMPONENT_PROP_KEYS) {
|
||||
if (key in tableConfig) {
|
||||
extra[key] = tableConfig[key];
|
||||
delete tableConfig[key];
|
||||
}
|
||||
}
|
||||
|
||||
return mount(ActionsColumn as any, {
|
||||
global: {
|
||||
plugins: [ElementPlus as any, MagicForm as any],
|
||||
provide: { mForm: createMForm(context) },
|
||||
},
|
||||
props: {
|
||||
config: { type: 'table', name: 'list', ...config },
|
||||
config: { type: 'table', name: 'list', ...tableConfig },
|
||||
model: { list: [{ text: 'a' }] },
|
||||
name: 'list',
|
||||
prop: 'list',
|
||||
@ -44,8 +55,10 @@ const mountColumn = (config: any, context: Record<string, any> = { env: 'prod' }
|
||||
pageSize: 10,
|
||||
index: 0,
|
||||
row: { text: 'a' },
|
||||
...extra,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('ActionsColumn', () => {
|
||||
test('copyable 函数收到 model / index / prop', async () => {
|
||||
@ -75,4 +88,82 @@ describe('ActionsColumn', () => {
|
||||
|
||||
expect(wrapper.text()).not.toContain('复制');
|
||||
});
|
||||
|
||||
test('未配置按钮文案时使用默认操作名称', async () => {
|
||||
const wrapper = mountColumn({ flat: true, dropSort: true, dropSortHandle: true });
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('删除');
|
||||
expect(wrapper.text()).toContain('复制');
|
||||
expect(wrapper.findComponent({ name: 'TMTooltip' }).props('content')).toBe('拖动排序');
|
||||
});
|
||||
|
||||
test('按钮配置与 addButtonConfig 一样,用 text 改名称、props 透传', async () => {
|
||||
const wrapper = mountColumn({
|
||||
flat: true,
|
||||
dropSort: true,
|
||||
dropSortHandle: true,
|
||||
deleteButtonConfig: { text: '移除', props: { class: 'custom-delete', type: 'warning', size: 'large' } },
|
||||
copyButtonConfig: { text: '克隆', props: { plain: true } },
|
||||
dropSortButtonConfig: { text: '拖拽换序', props: { class: 'custom-drag' } },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('移除');
|
||||
expect(wrapper.text()).toContain('克隆');
|
||||
expect(wrapper.text()).not.toContain('删除');
|
||||
expect(wrapper.text()).not.toContain('复制');
|
||||
expect(wrapper.find('.custom-delete').exists()).toBe(true);
|
||||
expect(wrapper.find('.custom-drag').exists()).toBe(true);
|
||||
const deleteButton = wrapper.findAllComponents({ name: 'TMButton' }).find((btn) => btn.props('type') === 'warning');
|
||||
expect(deleteButton?.props('size')).toBe('large');
|
||||
const copyButton = wrapper.findAllComponents({ name: 'TMButton' }).find((btn) => btn.text().includes('克隆'));
|
||||
expect(copyButton?.find('button').classes()).toContain('is-plain');
|
||||
expect(wrapper.findComponent({ name: 'TMTooltip' }).props('content')).toBe('拖拽换序');
|
||||
expect(wrapper.findAll('button').map((btn) => btn.attributes('title'))).toEqual(
|
||||
expect.arrayContaining(['移除', '克隆']),
|
||||
);
|
||||
});
|
||||
|
||||
test('delete 函数返回 false 时不展示删除,点击删除会移除当前行', async () => {
|
||||
const deleteFn = vi.fn(() => false);
|
||||
const hidden = mountColumn({ delete: deleteFn, flat: true });
|
||||
await nextTick();
|
||||
expect(deleteFn).toHaveBeenCalled();
|
||||
expect((hidden.findAll('button')[0].element as HTMLElement).style.display).toBe('none');
|
||||
|
||||
const wrapper = mountColumn({ flat: true });
|
||||
await nextTick();
|
||||
await wrapper.findAll('button')[0].trigger('click');
|
||||
expect(wrapper.emitted('change')?.[0][0]).toEqual([]);
|
||||
});
|
||||
|
||||
test('disabled 时点击删除不改数据,复制会沿 sortKey 递减', async () => {
|
||||
const blocked = mountColumn({ disabled: true, flat: true });
|
||||
await nextTick();
|
||||
await blocked.find('button').trigger('click');
|
||||
expect(blocked.emitted('change')).toBeUndefined();
|
||||
|
||||
const wrapper = mountColumn({
|
||||
flat: true,
|
||||
sortKey: 'order',
|
||||
copyable: true,
|
||||
});
|
||||
await wrapper.setProps({
|
||||
model: {
|
||||
list: [
|
||||
{ text: 'a', order: 3 },
|
||||
{ text: 'b', order: 1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
const copyButton = wrapper.findAll('button').find((btn) => btn.text().includes('复制'));
|
||||
await copyButton?.trigger('click');
|
||||
expect(wrapper.emitted('change')?.[0][0]).toEqual([
|
||||
{ text: 'a', order: 3 },
|
||||
{ text: 'b', order: 1 },
|
||||
{ text: 'a', order: 0 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -661,6 +661,85 @@ describe('GroupList container', () => {
|
||||
expect(wrapper.find('.m-fields-group-list.is-header-sticky').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('行操作按钮用 text 改名称,形态与 addButtonConfig 相同', async () => {
|
||||
const wrapper = mountForm(
|
||||
[
|
||||
{
|
||||
type: 'group-list',
|
||||
name: 'list',
|
||||
copyable: true,
|
||||
movable: true,
|
||||
moveSpecifyLocation: true,
|
||||
titleKey: 'text',
|
||||
titlePrefix: '条目',
|
||||
copyButtonConfig: { text: '克隆', props: { class: 'custom-copy', type: 'success' } },
|
||||
moveUpButtonConfig: { text: '前移', props: { class: 'custom-up' } },
|
||||
moveDownButtonConfig: { text: '后移', props: { class: 'custom-down' } },
|
||||
moveToButtonConfig: { text: '挪到', props: { class: 'custom-move' } },
|
||||
deleteButtonConfig: { text: '移除', props: { class: 'custom-remove' } },
|
||||
items: [{ name: 'text', type: 'text', text: 'text' }],
|
||||
},
|
||||
],
|
||||
{ list: [{ text: 'a' }, { text: 'b' }] },
|
||||
);
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('克隆');
|
||||
expect(wrapper.text()).toContain('前移');
|
||||
expect(wrapper.text()).toContain('后移');
|
||||
expect(wrapper.text()).toContain('挪到');
|
||||
expect(wrapper.text()).not.toContain('复制');
|
||||
expect(wrapper.find('.custom-copy').exists()).toBe(true);
|
||||
expect(wrapper.find('.custom-up').exists()).toBe(true);
|
||||
expect(wrapper.find('.custom-down').exists()).toBe(true);
|
||||
expect(wrapper.find('.custom-move').exists()).toBe(true);
|
||||
expect(wrapper.find('.custom-remove').exists()).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.findAllComponents({ name: 'TMButton' })
|
||||
.find((btn) => btn.text().includes('克隆'))
|
||||
?.props('type'),
|
||||
).toBe('success');
|
||||
expect(wrapper.findComponent({ name: 'TMTooltip' }).props('content')).toBe('移除 条目 1: a');
|
||||
|
||||
await wrapper.find('.expand-button').trigger('click');
|
||||
await wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('挪到'))
|
||||
?.trigger('click');
|
||||
await wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('取消'))
|
||||
?.trigger('click');
|
||||
await wrapper
|
||||
.findAll('button')
|
||||
.find((btn) => btn.text().includes('确认'))
|
||||
?.trigger('click');
|
||||
await nextTick();
|
||||
expect((wrapper.vm as any).values.list.map((item: { text: string }) => item.text)).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
test('delete 函数可隐藏单行删除,title 函数作为卡片标题', async () => {
|
||||
const deleteFn = vi.fn(() => false);
|
||||
const wrapper = mountForm(
|
||||
[
|
||||
{
|
||||
type: 'group-list',
|
||||
name: 'list',
|
||||
delete: deleteFn,
|
||||
title: () => '自定义标题',
|
||||
items: [{ name: 'text', type: 'text', text: 'text' }],
|
||||
},
|
||||
],
|
||||
{ list: [{ text: 'a' }] },
|
||||
);
|
||||
await nextTick();
|
||||
|
||||
expect(deleteFn).toHaveBeenCalled();
|
||||
expect((wrapper.find('.delete-button').element as HTMLElement).style.display).toBe('none');
|
||||
expect(wrapper.text()).toContain('自定义标题');
|
||||
});
|
||||
|
||||
test('嵌套列表各自按 header.sticky 决定是否吸顶', async () => {
|
||||
const wrapper = mountForm(
|
||||
[
|
||||
|
||||
218
packages/form/tests/unit/containers/useTableColumns.spec.ts
Normal file
218
packages/form/tests/unit/containers/useTableColumns.spec.ts
Normal file
@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h, nextTick, reactive } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { useTableColumns } from '@form/containers/table/useTableColumns';
|
||||
import { setConfig } from '@form/utils/config';
|
||||
|
||||
const Harness = defineComponent({
|
||||
name: 'ColumnsHarness',
|
||||
props: {
|
||||
tableProps: { type: Object, required: true },
|
||||
},
|
||||
setup(props) {
|
||||
const events: any[] = [];
|
||||
const { columns } = useTableColumns(
|
||||
props.tableProps as any,
|
||||
(...args: any[]) => {
|
||||
events.push(args);
|
||||
},
|
||||
props.tableProps.currentPage,
|
||||
props.tableProps.pageSize,
|
||||
props.tableProps.modelName,
|
||||
);
|
||||
return { columns, events };
|
||||
},
|
||||
render() {
|
||||
return h('div');
|
||||
},
|
||||
});
|
||||
|
||||
const createState = (overrides: Record<string, any> = {}) => {
|
||||
const currentPage = overrides.currentPage ?? { value: 0 };
|
||||
const pageSize = overrides.pageSize ?? { value: 10 };
|
||||
const modelName = overrides.modelName ?? { value: 'list' };
|
||||
return reactive({
|
||||
model: { list: [{ text: 'a' }, { text: 'b' }] },
|
||||
lastValues: { list: [{ text: 'old-a' }, { text: 'old-b' }] },
|
||||
name: 'list',
|
||||
prop: 'list',
|
||||
showIndex: false,
|
||||
sort: false,
|
||||
config: {
|
||||
type: 'table',
|
||||
name: 'list',
|
||||
items: [{ name: 'text', label: '文本', type: 'text' }],
|
||||
},
|
||||
currentPage,
|
||||
pageSize,
|
||||
modelName,
|
||||
...overrides,
|
||||
config: {
|
||||
type: 'table',
|
||||
name: 'list',
|
||||
items: [{ name: 'text', label: '文本', type: 'text' }],
|
||||
...(overrides.config || {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mountColumns = (state: ReturnType<typeof createState>, mForm?: any) =>
|
||||
mount(Harness, {
|
||||
props: { tableProps: state },
|
||||
global: mForm ? { provide: { mForm } } : undefined,
|
||||
});
|
||||
|
||||
const labels = (wrapper: ReturnType<typeof mountColumns>) =>
|
||||
(wrapper.vm as any).columns.map((column: any) => column.props.label ?? column.props.type);
|
||||
|
||||
describe('useTableColumns', () => {
|
||||
test('操作列标题默认为「操作」,operateColConfig.text 可替换', async () => {
|
||||
setConfig({});
|
||||
const defaults = mountColumns(createState());
|
||||
await nextTick();
|
||||
expect(labels(defaults)).toContain('操作');
|
||||
|
||||
const custom = mountColumns(
|
||||
createState({
|
||||
config: {
|
||||
operateColWidth: 112,
|
||||
fixed: 'left',
|
||||
operateColConfig: { text: '动作', props: { width: 200, align: 'left', minWidth: 80, fixed: 'right' } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await nextTick();
|
||||
const operateCol = (custom.vm as any).columns.find((column: any) => column.props.label === '动作');
|
||||
expect(operateCol).toBeTruthy();
|
||||
expect(labels(custom)).not.toContain('操作');
|
||||
expect(operateCol.props).toMatchObject({ width: 200, align: 'left', minWidth: 80, fixed: 'right' });
|
||||
});
|
||||
|
||||
test('展开列、选择列、排序列、序号列与数据列按配置拼出', async () => {
|
||||
setConfig({ flat: true });
|
||||
const itemExtra = vi.fn(() => '<em>extra</em>');
|
||||
const titleTip = vi.fn(() => '提示');
|
||||
const selection = vi.fn(() => true);
|
||||
const mForm = { initValues: { list: [] }, values: { list: [{ text: 'form' }] }, $emit: vi.fn() };
|
||||
const state = createState({
|
||||
sort: true,
|
||||
showIndex: true,
|
||||
config: {
|
||||
itemExtra,
|
||||
selection,
|
||||
showIndex: true,
|
||||
flat: undefined,
|
||||
fixed: 'right',
|
||||
pagination: true,
|
||||
items: [
|
||||
{ name: 'hidden', type: 'hidden' },
|
||||
{ name: 'skip', label: '跳过', display: false },
|
||||
{ name: 'text', label: '文本', titleTip, itemsFunction: (row: any) => [{ name: 'inner', text: row.text }] },
|
||||
],
|
||||
},
|
||||
});
|
||||
const wrapper = mountColumns(state, mForm);
|
||||
await nextTick();
|
||||
|
||||
const { columns } = wrapper.vm as any;
|
||||
expect(labels(wrapper)).toEqual(['expand', 'selection', '排序', '序号', '文本', '操作']);
|
||||
|
||||
const extraVnode = columns[0].cell({ $index: 1 });
|
||||
expect(itemExtra).toHaveBeenCalled();
|
||||
expect(extraVnode.props.innerHTML).toBe('<em>extra</em>');
|
||||
expect(extraVnode.props.class).toBe('m-form-tip');
|
||||
|
||||
const sortVnode = columns[2].cell({ $index: 1 });
|
||||
sortVnode.props.onSwap(1, 0);
|
||||
expect(mForm.$emit).toHaveBeenCalledWith('field-change', expect.any(Array));
|
||||
expect((wrapper.vm as any).events.at(-1)[0]).toBe('change');
|
||||
|
||||
expect(String(columns[3].cell({ $index: 1 }).children)).toBe('2');
|
||||
|
||||
const dataVnode = columns[4].cell({ row: { text: 'a' }, $index: 0 });
|
||||
expect(dataVnode.props.config.items).toEqual([{ name: 'inner', text: 'a' }]);
|
||||
expect(dataVnode.props.lastValues).toEqual({ text: 'old-a' });
|
||||
dataVnode.props.onChange({ text: 'next' }, { changeRecords: [] });
|
||||
dataVnode.props.onAddDiffCount();
|
||||
expect((wrapper.vm as any).events.map((event: any[]) => event[0])).toEqual(
|
||||
expect.arrayContaining(['change', 'addDiffCount']),
|
||||
);
|
||||
|
||||
const titleVnode = columns[4].title();
|
||||
expect(titleTip).toHaveBeenCalled();
|
||||
expect(titleVnode).toBeTruthy();
|
||||
|
||||
const actionVnode = columns[5].cell({ row: { text: 'a' }, $index: 0 });
|
||||
expect(actionVnode.props.flat).toBe(true);
|
||||
actionVnode.props.onChange([{ text: 'only' }]);
|
||||
expect((wrapper.vm as any).events.at(-1)).toEqual(['change', [{ text: 'only' }]]);
|
||||
|
||||
const dropped = mountColumns(
|
||||
createState({
|
||||
config: {
|
||||
fixed: false,
|
||||
dropSort: true,
|
||||
itemExtra: 'plain',
|
||||
selection: false,
|
||||
pagination: false,
|
||||
items: [{ name: 'text', label: '文本', titleTip: '静态提示' }],
|
||||
},
|
||||
}),
|
||||
mForm,
|
||||
);
|
||||
await nextTick();
|
||||
|
||||
const nextColumns = (dropped.vm as any).columns;
|
||||
expect(nextColumns[0].props.label).toBe('操作');
|
||||
expect(nextColumns[0].props.fixed).toBeUndefined();
|
||||
expect(nextColumns.some((column: any) => column.props.type === 'expand')).toBe(false);
|
||||
const plain = nextColumns.find((column: any) => column.props.prop === 'text');
|
||||
expect(plain.props.class).toBe('el-table__column--dropable');
|
||||
expect(plain.title()).toBeTruthy();
|
||||
expect(plain.cell({ row: { text: 'a' }, $index: 0 }).props.lastValues).toEqual({ text: 'old-a' });
|
||||
|
||||
setConfig({});
|
||||
});
|
||||
|
||||
test('选择函数返回 false、非法 fixed、全局非 flat 时不铺平操作列', async () => {
|
||||
setConfig({});
|
||||
const state = createState({
|
||||
sort: true,
|
||||
model: { list: [{ text: 'only' }] },
|
||||
lastValues: {},
|
||||
config: {
|
||||
selection: () => false,
|
||||
fixed: 'center',
|
||||
items: [{ name: 'text', label: '文本', display: () => false }],
|
||||
},
|
||||
});
|
||||
const wrapper = mountColumns(state);
|
||||
await nextTick();
|
||||
|
||||
const { columns } = wrapper.vm as any;
|
||||
expect(columns.map((column: any) => column.props.label ?? column.props.type)).toEqual(['操作']);
|
||||
expect(columns[0].props.fixed).toBe('left');
|
||||
const actionVnode = columns[0].cell({ row: { text: 'only' }, $index: 0 });
|
||||
expect(actionVnode.props.flat).toBeUndefined();
|
||||
|
||||
const plain = mountColumns(
|
||||
createState({
|
||||
lastValues: {},
|
||||
config: {
|
||||
fixed: 'left',
|
||||
items: [{ name: 'text', label: '文本' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await nextTick();
|
||||
const dataColumn = (plain.vm as any).columns.find((column: any) => column.props.prop === 'text');
|
||||
expect(dataColumn.cell({ row: { text: 'only' }, $index: 0 }).props.lastValues).toBeUndefined();
|
||||
expect(dataColumn.title).toBeUndefined();
|
||||
});
|
||||
});
|
||||
21
packages/form/tests/unit/utils/actionButton.spec.ts
Normal file
21
packages/form/tests/unit/utils/actionButton.spec.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { actionButtonText, DEFAULT_ACTION_BUTTON_TEXT } from '@form/utils/actionButton';
|
||||
|
||||
describe('actionButtonText', () => {
|
||||
test('未配置时返回内置文案', () => {
|
||||
expect(actionButtonText(undefined, 'operate')).toBe(DEFAULT_ACTION_BUTTON_TEXT.operate);
|
||||
expect(actionButtonText({}, 'delete')).toBe('删除');
|
||||
});
|
||||
|
||||
test('text 覆盖内置文案,空字符串回退', () => {
|
||||
expect(actionButtonText({ text: '克隆' }, 'copy')).toBe('克隆');
|
||||
expect(actionButtonText({ text: '' }, 'moveUp')).toBe('上移');
|
||||
expect(actionButtonText({ text: '前移' }, 'moveDown')).toBe('前移');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user