Merge branch 'fork/dream-weave/update-zod'
This commit is contained in:
@@ -59,6 +59,7 @@
|
||||
"preview": "turbo-run preview",
|
||||
"publint": "vsh publint",
|
||||
"reinstall": "pnpm run clean -- --del-lock && pnpm install",
|
||||
"test:benchmark": "vitest bench --run",
|
||||
"test:unit": "vitest run --dom",
|
||||
"test:e2e": "turbo run test:e2e",
|
||||
"update:deps": "npx taze -r -w",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { BaseFormComponentType } from '../src/types';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FormApi } from '../src/form-api';
|
||||
import { FormCodecError } from '../src/form-codec';
|
||||
|
||||
describe('formApi', () => {
|
||||
let formApi: FormApi;
|
||||
@@ -171,6 +172,71 @@ describe('formApi', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should isolate codec results from live form values', async () => {
|
||||
interface ProfileFormValues {
|
||||
profile: { name: string };
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const values: ProfileFormValues = {
|
||||
profile: { name: 'Ada' },
|
||||
tags: ['admin'],
|
||||
};
|
||||
const codecFormApi = new FormApi<ProfileFormValues>({
|
||||
codec: {
|
||||
decode: (submitValues) => submitValues,
|
||||
encode: (formValues) => ({
|
||||
profile: formValues.profile,
|
||||
tags: formValues.tags,
|
||||
}),
|
||||
},
|
||||
});
|
||||
const formActions: any = { meta: {}, values };
|
||||
|
||||
codecFormApi.mount(formActions, new Map());
|
||||
const initialSubmissionValues = codecFormApi.getLatestSubmissionValues();
|
||||
values.profile.name = 'Grace';
|
||||
values.tags.push('user');
|
||||
|
||||
expect(initialSubmissionValues).toEqual({
|
||||
profile: { name: 'Ada' },
|
||||
tags: ['admin'],
|
||||
});
|
||||
const submissionValues = await codecFormApi.getValues();
|
||||
expect(submissionValues.profile).not.toBe(values.profile);
|
||||
expect(submissionValues.tags).not.toBe(values.tags);
|
||||
});
|
||||
|
||||
it('should fall back to raw values when the initial codec encode fails', async () => {
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const codecFormApi = new FormApi<
|
||||
{ name?: string },
|
||||
BaseFormComponentType,
|
||||
Record<never, never>,
|
||||
{ normalizedName: string }
|
||||
>({
|
||||
codec: {
|
||||
decode: (values) => ({ name: values.normalizedName }),
|
||||
encode() {
|
||||
throw new Error('incomplete initial values');
|
||||
},
|
||||
},
|
||||
});
|
||||
const formActions: any = { meta: {}, values: { name: 'Ada' } };
|
||||
|
||||
expect(() => codecFormApi.mount(formActions, new Map())).not.toThrow();
|
||||
|
||||
expect(codecFormApi.isMounted).toBe(true);
|
||||
expect(codecFormApi.getLatestSubmissionValues()).toEqual({ name: 'Ada' });
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[Vben Form] Failed to encode initial values. Falling back to raw form values.',
|
||||
expect.objectContaining({ phase: 'encode' }),
|
||||
);
|
||||
await expect(codecFormApi.getValues()).rejects.toBeInstanceOf(
|
||||
FormCodecError,
|
||||
);
|
||||
});
|
||||
|
||||
it('should scan deprecated schema transforms once for unchanged state', async () => {
|
||||
const getChildren = vi.fn(() => []);
|
||||
const schema = {
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import type { FormSchema } from '../src/types';
|
||||
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import { defineComponent, h, nextTick } from 'vue';
|
||||
|
||||
import { afterAll, bench, describe } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { setupVbenForm } from '../src/config';
|
||||
import { useVbenForm } from '../src/use-vben-form';
|
||||
|
||||
const BENCHMARK_OPTIONS = { time: 750, warmupTime: 150 } as const;
|
||||
const FIELD_COUNT = 100;
|
||||
const MOUNT_FIELD_COUNT = 50;
|
||||
|
||||
const TestInput = defineComponent({
|
||||
inheritAttrs: false,
|
||||
emits: ['update:modelValue'],
|
||||
setup(_props, { attrs, emit }) {
|
||||
function handleInput(event: Event) {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
emit('update:modelValue', target.value);
|
||||
}
|
||||
}
|
||||
|
||||
return () =>
|
||||
h('input', {
|
||||
...attrs,
|
||||
onInput: handleInput,
|
||||
value: attrs.modelValue ?? '',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function createFlatSchema(
|
||||
fieldCount: number,
|
||||
withRules: boolean = false,
|
||||
): FormSchema[] {
|
||||
const rule = withRules ? z.string().min(1) : undefined;
|
||||
return Array.from({ length: fieldCount }, (_, index) => ({
|
||||
component: TestInput,
|
||||
defaultValue: `Value ${index}`,
|
||||
fieldName: `field${index}`,
|
||||
label: `Field ${index}`,
|
||||
rules: rule,
|
||||
}));
|
||||
}
|
||||
|
||||
function createFlatValues(prefix: string) {
|
||||
return Object.fromEntries(
|
||||
Array.from({ length: FIELD_COUNT }, (_, index) => [
|
||||
`field${index}`,
|
||||
`${prefix} ${index}`,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
setupVbenForm({ config: {}, rules: {} });
|
||||
|
||||
const flatSchema = createFlatSchema(FIELD_COUNT);
|
||||
const [FlatForm, flatFormApi] = useVbenForm<Record<string, string>>({
|
||||
schema: flatSchema,
|
||||
showDefaultActions: false,
|
||||
});
|
||||
const flatWrapper = mount(FlatForm);
|
||||
|
||||
const [ValidationForm, validationFormApi] = useVbenForm<Record<string, string>>(
|
||||
{
|
||||
schema: createFlatSchema(FIELD_COUNT, true),
|
||||
showDefaultActions: false,
|
||||
},
|
||||
);
|
||||
const validationWrapper = mount(ValidationForm);
|
||||
|
||||
const dependencySchema: FormSchema[] = [
|
||||
{
|
||||
component: TestInput,
|
||||
defaultValue: 'editable',
|
||||
fieldName: 'mode',
|
||||
label: 'Mode',
|
||||
},
|
||||
...Array.from({ length: 50 }, (_, index) => ({
|
||||
component: TestInput,
|
||||
defaultValue: `Value ${index}`,
|
||||
dependencies: {
|
||||
resolve: ({ values }) => ({ disabled: values.mode === 'locked' }),
|
||||
triggerFields: ['mode'],
|
||||
},
|
||||
fieldName: `dependent${index}`,
|
||||
label: `Dependent ${index}`,
|
||||
})),
|
||||
];
|
||||
const [DependencyForm, dependencyFormApi] = useVbenForm<Record<string, string>>(
|
||||
{
|
||||
schema: dependencySchema,
|
||||
showDefaultActions: false,
|
||||
},
|
||||
);
|
||||
const dependencyWrapper = mount(DependencyForm);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
const batchValues = [createFlatValues('Alpha'), createFlatValues('Beta')];
|
||||
const schemaPatches = [false, true].map((disabled) =>
|
||||
Array.from({ length: FIELD_COUNT }, (_, index) => ({
|
||||
componentProps: { disabled },
|
||||
fieldName: `field${index}`,
|
||||
})),
|
||||
);
|
||||
let batchIteration = 0;
|
||||
let dependencyIteration = 0;
|
||||
let fieldIteration = 0;
|
||||
let resetIteration = 0;
|
||||
let schemaIteration = 0;
|
||||
|
||||
afterAll(() => {
|
||||
dependencyWrapper.unmount();
|
||||
flatWrapper.unmount();
|
||||
validationWrapper.unmount();
|
||||
});
|
||||
|
||||
describe('form render performance', () => {
|
||||
bench(
|
||||
'initialize, mount, and unmount 50 fields',
|
||||
async () => {
|
||||
const [Form] = useVbenForm<Record<string, string>>({
|
||||
schema: createFlatSchema(MOUNT_FIELD_COUNT),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
await flushPromises();
|
||||
wrapper.unmount();
|
||||
},
|
||||
BENCHMARK_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
describe('form value performance', () => {
|
||||
bench(
|
||||
'update one field in a 100-field form',
|
||||
async () => {
|
||||
fieldIteration += 1;
|
||||
await flatFormApi.setFieldValue('field50', `Value ${fieldIteration}`);
|
||||
await nextTick();
|
||||
},
|
||||
BENCHMARK_OPTIONS,
|
||||
);
|
||||
|
||||
bench(
|
||||
'set 100 fields in one batch',
|
||||
async () => {
|
||||
batchIteration += 1;
|
||||
await flatFormApi.setValues(batchValues[batchIteration % 2] ?? {});
|
||||
await nextTick();
|
||||
},
|
||||
BENCHMARK_OPTIONS,
|
||||
);
|
||||
|
||||
bench(
|
||||
'reset 100 fields to alternate values',
|
||||
async () => {
|
||||
resetIteration += 1;
|
||||
await flatFormApi.reset(
|
||||
{ values: batchValues[resetIteration % 2] ?? {} },
|
||||
{ force: true },
|
||||
);
|
||||
await nextTick();
|
||||
},
|
||||
BENCHMARK_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
describe('form validation performance', () => {
|
||||
bench(
|
||||
'validate 100 fields with zod rules',
|
||||
async () => {
|
||||
await validationFormApi.validate();
|
||||
},
|
||||
BENCHMARK_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
describe('form schema performance', () => {
|
||||
bench(
|
||||
'update 100 schema entries',
|
||||
async () => {
|
||||
schemaIteration += 1;
|
||||
flatFormApi.updateSchema(schemaPatches[schemaIteration % 2] ?? []);
|
||||
await nextTick();
|
||||
},
|
||||
BENCHMARK_OPTIONS,
|
||||
);
|
||||
|
||||
bench(
|
||||
'resolve 50 dependencies from one trigger',
|
||||
async () => {
|
||||
dependencyIteration += 1;
|
||||
await dependencyFormApi.setFieldValue(
|
||||
'mode',
|
||||
dependencyIteration % 2 === 0 ? 'editable' : 'locked',
|
||||
);
|
||||
await flushPromises();
|
||||
},
|
||||
BENCHMARK_OPTIONS,
|
||||
);
|
||||
});
|
||||
@@ -104,6 +104,34 @@ describe('useVbenForm integration', () => {
|
||||
expect(validateValue).toHaveBeenCalledTimes(initialValidationCount + 1);
|
||||
});
|
||||
|
||||
it('keeps values reactive when exposed through the default slot', async () => {
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: TestInput,
|
||||
defaultValue: 'Ada',
|
||||
fieldName: 'name',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
const wrapper = mount(Form, {
|
||||
slots: {
|
||||
default: ({ values }: { values: Record<string, any> }) =>
|
||||
h('span', { class: 'slot-value' }, values.name),
|
||||
},
|
||||
});
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('.slot-value').text()).toBe('Ada');
|
||||
|
||||
await formApi.setFieldValue('name', 'Grace');
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('.slot-value').text()).toBe('Grace');
|
||||
});
|
||||
|
||||
it('supports a field-level change event fallback for legacy components', async () => {
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
@@ -485,6 +513,85 @@ describe('useVbenForm integration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves array row inputs and focus while editing', async () => {
|
||||
const [Form] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
component: TestInput,
|
||||
fieldName: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
],
|
||||
defaultValue: [{ name: 'Ada' }],
|
||||
fieldName: 'contacts',
|
||||
type: 'array',
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form, { attachTo: document.body });
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
const input = wrapper.get('input');
|
||||
const inputElement = input.element;
|
||||
inputElement.focus();
|
||||
|
||||
await input.setValue('Ada Lovelace');
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('input').element).toBe(inputElement);
|
||||
expect((input.element as HTMLInputElement).value).toBe('Ada Lovelace');
|
||||
expect(document.activeElement).toBe(inputElement);
|
||||
});
|
||||
|
||||
it('updates optimized array rows when values and schemas change', async () => {
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
component: TestInput,
|
||||
fieldName: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
],
|
||||
defaultValue: [{ name: 'Ada' }, { name: 'Grace' }],
|
||||
fieldName: 'contacts',
|
||||
type: 'array',
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
const firstInput = wrapper.get('input[name="contacts[0].name"]');
|
||||
const firstInputElement = firstInput.element;
|
||||
|
||||
await formApi.setFieldValue('contacts[0].name', 'Ada Lovelace');
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.get('input[name="contacts[0].name"]').element).toBe(
|
||||
firstInputElement,
|
||||
);
|
||||
expect(firstInput.element.getAttribute('value')).toBe('Ada Lovelace');
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: { disabled: true },
|
||||
fieldName: 'contacts.name',
|
||||
},
|
||||
]);
|
||||
await flushPromises();
|
||||
|
||||
expect(
|
||||
wrapper.get('input[name="contacts[0].name"]').attributes(),
|
||||
).toHaveProperty('disabled');
|
||||
expect(
|
||||
wrapper.get('input[name="contacts[1].name"]').attributes(),
|
||||
).toHaveProperty('disabled');
|
||||
});
|
||||
|
||||
it('scopes resolve dependencies to array rows', async () => {
|
||||
const resolve = vi.fn(({ schema }: Record<string, any>) => ({
|
||||
componentProps: {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import { defineComponent, h, nextTick } from 'vue';
|
||||
|
||||
import { afterAll, bench, describe } from 'vitest';
|
||||
|
||||
import { setupVbenForm } from '../src/config';
|
||||
import { FormApi } from '../src/form-api';
|
||||
import { encodeFormValues } from '../src/form-codec';
|
||||
import { useVbenForm } from '../src/use-vben-form';
|
||||
|
||||
interface ContactValues {
|
||||
enabled: boolean;
|
||||
metadata: {
|
||||
permissions: string[];
|
||||
team: string;
|
||||
};
|
||||
name: string;
|
||||
phone: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface PerformanceFormValues extends Record<string, unknown> {
|
||||
contacts: ContactValues[];
|
||||
settings: {
|
||||
alerts: boolean;
|
||||
locale: string;
|
||||
sections: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const ROW_COUNT = 100;
|
||||
|
||||
const TestInput = defineComponent({
|
||||
inheritAttrs: false,
|
||||
emits: ['update:modelValue'],
|
||||
setup(_props, { attrs, emit }) {
|
||||
function handleInput(event: Event) {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
emit('update:modelValue', target.value);
|
||||
}
|
||||
}
|
||||
|
||||
return () =>
|
||||
h('input', {
|
||||
...attrs,
|
||||
onInput: handleInput,
|
||||
value: attrs.modelValue ?? '',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function createFormValues(): PerformanceFormValues {
|
||||
return {
|
||||
contacts: Array.from({ length: ROW_COUNT }, (_, index) => ({
|
||||
enabled: index % 2 === 0,
|
||||
metadata: {
|
||||
permissions: ['read', 'write', 'review'],
|
||||
team: `team-${index % 10}`,
|
||||
},
|
||||
name: ` Contact ${index} `,
|
||||
phone: `10086-${index}`,
|
||||
tags: ['primary', 'on-call', `group-${index % 5}`],
|
||||
})),
|
||||
settings: {
|
||||
alerts: true,
|
||||
locale: 'zh-CN',
|
||||
sections: ['profile', 'security', 'notifications'],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const codec = {
|
||||
decode: (values: Readonly<PerformanceFormValues>) => ({ ...values }),
|
||||
encode: (values: Readonly<PerformanceFormValues>) => ({
|
||||
...values,
|
||||
contacts: values.contacts.map((contact) => ({
|
||||
...contact,
|
||||
name: contact.name.trim(),
|
||||
})),
|
||||
}),
|
||||
};
|
||||
|
||||
const formValues = createFormValues();
|
||||
const codecFormApi = new FormApi<PerformanceFormValues>({ codec });
|
||||
codecFormApi.mount({ meta: {}, values: formValues } as never, new Map());
|
||||
|
||||
setupVbenForm({ config: {}, rules: {} });
|
||||
const [ArrayForm, arrayFormApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
component: TestInput,
|
||||
fieldName: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
],
|
||||
defaultValue: Array.from({ length: ROW_COUNT }, (_, index) => ({
|
||||
name: `Contact ${index}`,
|
||||
})),
|
||||
fieldName: 'contacts',
|
||||
type: 'array',
|
||||
},
|
||||
],
|
||||
});
|
||||
const arrayWrapper = mount(ArrayForm);
|
||||
await flushPromises();
|
||||
const arraySchemaPatches = [false, true].map((disabled) => ({
|
||||
componentProps: { disabled },
|
||||
fieldName: 'contacts.name',
|
||||
}));
|
||||
let arrayEditIteration = 0;
|
||||
let arraySchemaIteration = 0;
|
||||
|
||||
afterAll(() => {
|
||||
arrayWrapper.unmount();
|
||||
});
|
||||
|
||||
describe('form codec performance', () => {
|
||||
bench(
|
||||
'encode 100 nested rows without isolation',
|
||||
() => {
|
||||
encodeFormValues(codec, formValues);
|
||||
},
|
||||
{ time: 1000, warmupTime: 200 },
|
||||
);
|
||||
|
||||
bench(
|
||||
'encode 100 nested rows with isolated input',
|
||||
() => {
|
||||
codecFormApi.formatValues(formValues);
|
||||
},
|
||||
{ time: 1000, warmupTime: 200 },
|
||||
);
|
||||
|
||||
bench(
|
||||
'create submit snapshot for 100 nested rows',
|
||||
async () => {
|
||||
await codecFormApi.getValueSnapshot();
|
||||
},
|
||||
{ time: 1000, warmupTime: 200 },
|
||||
);
|
||||
});
|
||||
|
||||
describe('form array performance', () => {
|
||||
bench(
|
||||
'edit one field in a 100-row array',
|
||||
async () => {
|
||||
arrayEditIteration += 1;
|
||||
await arrayFormApi.setFieldValue(
|
||||
'contacts[50].name',
|
||||
`Contact ${arrayEditIteration}`,
|
||||
);
|
||||
await nextTick();
|
||||
},
|
||||
{ time: 1000, warmupTime: 200 },
|
||||
);
|
||||
|
||||
bench(
|
||||
'append and remove one row from a 100-row array',
|
||||
async () => {
|
||||
arrayFormApi.form.pushFieldValue('contacts', { name: 'Temporary' });
|
||||
await nextTick();
|
||||
await arrayFormApi.form.removeFieldValue('contacts', ROW_COUNT);
|
||||
await nextTick();
|
||||
},
|
||||
{ time: 1000, warmupTime: 200 },
|
||||
);
|
||||
|
||||
bench(
|
||||
'update one child schema across 100 rows',
|
||||
async () => {
|
||||
arraySchemaIteration += 1;
|
||||
arrayFormApi.updateSchema([
|
||||
arraySchemaPatches[arraySchemaIteration % 2] ?? {},
|
||||
]);
|
||||
await nextTick();
|
||||
},
|
||||
{ time: 1000, warmupTime: 200 },
|
||||
);
|
||||
});
|
||||
@@ -228,6 +228,10 @@ describe('form public types', () => {
|
||||
handleReset(values) {
|
||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||
},
|
||||
handleSubmit(values, rawValues) {
|
||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||
expectTypeOf(rawValues).toEqualTypeOf<Readonly<AccountFormValues>>();
|
||||
},
|
||||
schema: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/store": "catalog:",
|
||||
"@tanstack/vue-form": "catalog:",
|
||||
"@vben-core/composables": "workspace:*",
|
||||
"@vben-core/icons": "workspace:*",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
VbenIconButton,
|
||||
VbenRenderContent,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { cn, isObject, set } from '@vben-core/shared/utils';
|
||||
import { cn, get, set } from '@vben-core/shared/utils';
|
||||
|
||||
import { injectRenderFormProps } from '../form-render/context';
|
||||
import FormField from '../form-render/form-field.vue';
|
||||
@@ -71,16 +71,16 @@ if (!form) {
|
||||
throw new Error('Form api is required in <VbenFormFieldArray />');
|
||||
}
|
||||
const formActions = form;
|
||||
const arrayValue = formActions.useFieldValue(props.name);
|
||||
const rowKeys = new WeakMap<object, string>();
|
||||
let nextRowKey = 0;
|
||||
|
||||
const fields = computed<Record<string, any>[]>(() => {
|
||||
return Array.isArray(arrayValue.value) ? arrayValue.value : [];
|
||||
const arrayLength = formActions.useSelector((state) => {
|
||||
const value = get(state.values, props.name);
|
||||
return Array.isArray(value) ? value.length : 0;
|
||||
});
|
||||
const rowIndexes = computed(() =>
|
||||
Array.from({ length: arrayLength.value }, (_, index) => index),
|
||||
);
|
||||
|
||||
const canAdd = computed(() => fields.value.length < props.max);
|
||||
const canRemove = computed(() => fields.value.length > props.min);
|
||||
const canAdd = computed(() => arrayLength.value < props.max);
|
||||
const canRemove = computed(() => arrayLength.value > props.min);
|
||||
const gridStyle = computed(() => {
|
||||
const columns = [
|
||||
...(props.showIndex ? ['3rem'] : []),
|
||||
@@ -124,20 +124,6 @@ function removeRow(index: number) {
|
||||
void formActions.removeFieldValue(arrayPath.value, index);
|
||||
}
|
||||
|
||||
function getRowKey(row: Record<string, any>, index: number) {
|
||||
if (!isObject(row)) {
|
||||
return `${arrayPath.value}-${index}`;
|
||||
}
|
||||
const existingKey = rowKeys.get(row);
|
||||
if (existingKey) {
|
||||
return existingKey;
|
||||
}
|
||||
nextRowKey += 1;
|
||||
const key = `${arrayPath.value}-${nextRowKey}`;
|
||||
rowKeys.set(row, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function rowSchemas(index: number) {
|
||||
return props.schema.map((col) =>
|
||||
createArrayChildSchema(col as never, {
|
||||
@@ -149,6 +135,10 @@ function rowSchemas(index: number) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedRowSchemas = computed(() =>
|
||||
Array.from({ length: arrayLength.value }, (_, index) => rowSchemas(index)),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -179,8 +169,8 @@ function rowSchemas(index: number) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(entry, index) in fields"
|
||||
:key="getRowKey(entry, index)"
|
||||
v-for="index in rowIndexes"
|
||||
:key="`${arrayPath}-${index}`"
|
||||
class="border-border/60 border-b p-3 last:border-b-0 sm:grid sm:p-0"
|
||||
:style="gridStyle"
|
||||
>
|
||||
@@ -193,7 +183,7 @@ function rowSchemas(index: number) {
|
||||
</div>
|
||||
|
||||
<template
|
||||
v-for="(childSchema, childIndex) in rowSchemas(index)"
|
||||
v-for="(childSchema, childIndex) in normalizedRowSchemas[index]"
|
||||
:key="childSchema.fieldName"
|
||||
>
|
||||
<div class="min-w-0 py-2 sm:px-2">
|
||||
@@ -222,7 +212,7 @@ function rowSchemas(index: number) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="fields.length === 0"
|
||||
v-if="arrayLength === 0"
|
||||
class="text-muted-foreground py-6 text-center text-sm"
|
||||
>
|
||||
{{ emptyText }}
|
||||
|
||||
@@ -144,7 +144,7 @@ export class FormApi<
|
||||
if (this.state?.codec) {
|
||||
return encodeFormValues(
|
||||
this.state.codec,
|
||||
rawValues as Readonly<TFormValues>,
|
||||
cloneDeep(toRaw(rawValues)) as Readonly<TFormValues>,
|
||||
);
|
||||
}
|
||||
return formatFormValues(
|
||||
@@ -303,10 +303,23 @@ export class FormApi<
|
||||
if (!this.isMounted) {
|
||||
this.form = formActions;
|
||||
this.stateHandler.setConditionTrue();
|
||||
const initialValues = this.form.values
|
||||
? this.formatValues(toRaw(this.form.values))
|
||||
: {};
|
||||
this.setLatestSubmissionValues(initialValues);
|
||||
let initialValues: FormValues = {};
|
||||
if (this.form.values) {
|
||||
const rawInitialValues = toRaw(this.form.values);
|
||||
try {
|
||||
initialValues = this.formatValues(rawInitialValues);
|
||||
} catch (error) {
|
||||
if (!this.state?.codec) {
|
||||
throw error;
|
||||
}
|
||||
console.warn(
|
||||
'[Vben Form] Failed to encode initial values. Falling back to raw form values.',
|
||||
error,
|
||||
);
|
||||
initialValues = cloneDeep(rawInitialValues);
|
||||
}
|
||||
}
|
||||
this.setLatestSubmissionValues(initialValues as Partial<TSubmitValues>);
|
||||
this.componentRefMap =
|
||||
componentRefMap ?? this.componentRefMap ?? new Map();
|
||||
this.isMounted = true;
|
||||
|
||||
@@ -83,6 +83,7 @@ if (!formApi) {
|
||||
throw new Error('Form api is required in <FormField />');
|
||||
}
|
||||
const error = formApi.useFieldError(fieldName);
|
||||
const fieldValue = formApi.useFieldValue(fieldName);
|
||||
const compact = computed(() => formRenderProps.compact);
|
||||
const isInValid = computed(() => Boolean(error.value));
|
||||
const shouldApplyInvalidStyle = computed(() => {
|
||||
@@ -297,7 +298,7 @@ function createFieldSlotProps(slotProps: RuntimeFieldSlotProps) {
|
||||
...slotProps,
|
||||
componentField: {
|
||||
name: fieldName,
|
||||
modelValue: field.state.value,
|
||||
modelValue: fieldValue.value,
|
||||
onBlur: field.handleBlur,
|
||||
onChange: handleChange,
|
||||
onInput: handleChange,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { computed, shallowRef } from 'vue';
|
||||
|
||||
import { mergeWithArrayOverride } from '@vben-core/shared/utils';
|
||||
|
||||
import { batch } from '@tanstack/store';
|
||||
import { useForm } from '@tanstack/vue-form';
|
||||
|
||||
import { createRuntimeFieldComponent } from './form-runtime-field';
|
||||
@@ -289,11 +290,13 @@ export function useFormRuntime<TValues extends FormValues>(
|
||||
}
|
||||
},
|
||||
async setValues(values, shouldValidate) {
|
||||
for (const [fieldName, value] of Object.entries(values)) {
|
||||
rawForm.setFieldValue(fieldName as never, value as never, {
|
||||
dontValidate: !shouldValidate,
|
||||
});
|
||||
}
|
||||
batch(() => {
|
||||
for (const [fieldName, value] of Object.entries(values)) {
|
||||
rawForm.setFieldValue(fieldName as never, value as never, {
|
||||
dontValidate: !shouldValidate,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (shouldValidate) {
|
||||
await validate();
|
||||
}
|
||||
|
||||
@@ -678,7 +678,7 @@ export type HandleSubmitFn<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> = (
|
||||
values: TSubmitValues,
|
||||
values: NoInfer<TSubmitValues>,
|
||||
rawValues: Readonly<TFormValues>,
|
||||
) => Promise<void> | void;
|
||||
|
||||
|
||||
@@ -87,8 +87,13 @@ watch(values, (currentValues, previousValues) => {
|
||||
if (!valuesChangeReady) {
|
||||
return;
|
||||
}
|
||||
const handleValuesChange = forward.value.handleValuesChange;
|
||||
const submitOnChange = state?.value.submitOnChange;
|
||||
if (!handleValuesChange && !submitOnChange) {
|
||||
return;
|
||||
}
|
||||
const fields = state?.value.schema?.map((item) => item.fieldName) ?? [];
|
||||
if (forward.value.handleValuesChange && fields.length > 0) {
|
||||
if (handleValuesChange && fields.length > 0) {
|
||||
const changedFields = fields.filter((field) => {
|
||||
return !isEqual(
|
||||
get(currentValues, field),
|
||||
@@ -96,14 +101,14 @@ watch(values, (currentValues, previousValues) => {
|
||||
);
|
||||
});
|
||||
if (changedFields.length > 0) {
|
||||
forward.value.handleValuesChange(
|
||||
readonly(currentValues),
|
||||
changedFields,
|
||||
() => formApi.formatValues(currentValues),
|
||||
handleValuesChange(readonly(currentValues), changedFields, () =>
|
||||
formApi.formatValues(currentValues),
|
||||
);
|
||||
}
|
||||
}
|
||||
handleValuesChangeDebounced();
|
||||
if (submitOnChange) {
|
||||
handleValuesChangeDebounced();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -130,46 +135,50 @@ watch(values, (currentValues, previousValues) => {
|
||||
></slot>
|
||||
</template>
|
||||
<template #default="slotProps">
|
||||
<slot v-bind="slotProps" :form-api="formApi" :values="form.values">
|
||||
<FormActions
|
||||
v-if="forward.showDefaultActions"
|
||||
:model-value="state?.collapsed"
|
||||
@update:model-value="handleUpdateCollapsed"
|
||||
>
|
||||
<template #reset-before="resetSlotProps">
|
||||
<slot
|
||||
name="reset-before"
|
||||
v-bind="resetSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
<template #submit-before="submitSlotProps">
|
||||
<slot
|
||||
name="submit-before"
|
||||
v-bind="submitSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
<template #expand-before="expandBeforeSlotProps">
|
||||
<slot
|
||||
name="expand-before"
|
||||
v-bind="expandBeforeSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
<template #expand-after="expandAfterSlotProps">
|
||||
<slot
|
||||
name="expand-after"
|
||||
v-bind="expandAfterSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
</FormActions>
|
||||
</slot>
|
||||
<slot
|
||||
v-if="$slots.default"
|
||||
v-bind="slotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
<FormActions
|
||||
v-else-if="forward.showDefaultActions"
|
||||
:model-value="state?.collapsed"
|
||||
@update:model-value="handleUpdateCollapsed"
|
||||
>
|
||||
<template #reset-before="resetSlotProps">
|
||||
<slot
|
||||
name="reset-before"
|
||||
v-bind="resetSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
<template #submit-before="submitSlotProps">
|
||||
<slot
|
||||
name="submit-before"
|
||||
v-bind="submitSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
<template #expand-before="expandBeforeSlotProps">
|
||||
<slot
|
||||
name="expand-before"
|
||||
v-bind="expandBeforeSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
<template #expand-after="expandAfterSlotProps">
|
||||
<slot
|
||||
name="expand-after"
|
||||
v-bind="expandAfterSlotProps"
|
||||
:form-api="formApi"
|
||||
:values="form.values"
|
||||
></slot>
|
||||
</template>
|
||||
</FormActions>
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
47
playground/__tests__/unit/date-range-codec.test.ts
Normal file
47
playground/__tests__/unit/date-range-codec.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { createDateRangeCodec } from '../../src/utils/date-range-codec';
|
||||
|
||||
interface SearchFormValues extends Record<string, unknown> {
|
||||
createdAt?: [Dayjs, Dayjs];
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
const codec = createDateRangeCodec<SearchFormValues>()({
|
||||
endField: 'finishedAt',
|
||||
rangeField: 'createdAt',
|
||||
startField: 'startedAt',
|
||||
});
|
||||
|
||||
describe('date range codec', () => {
|
||||
it('encodes and decodes configurable date range fields', () => {
|
||||
const submitValues = codec.encode({
|
||||
createdAt: [dayjs('2026-07-01'), dayjs('2026-07-23')],
|
||||
keyword: 'admin',
|
||||
});
|
||||
|
||||
expect(submitValues).toEqual({
|
||||
finishedAt: '2026-07-23',
|
||||
keyword: 'admin',
|
||||
startedAt: '2026-07-01',
|
||||
});
|
||||
const formValues = codec.decode(submitValues);
|
||||
expect(formValues.keyword).toBe('admin');
|
||||
expect(
|
||||
formValues.createdAt?.map((value) => value.format('YYYY-MM-DD')),
|
||||
).toEqual(['2026-07-01', '2026-07-23']);
|
||||
});
|
||||
|
||||
it('does not reconstruct a range with a missing bound', () => {
|
||||
expect(
|
||||
codec.decode({
|
||||
finishedAt: undefined,
|
||||
keyword: 'admin',
|
||||
startedAt: '2026-07-01',
|
||||
}),
|
||||
).toEqual({ keyword: 'admin' });
|
||||
});
|
||||
});
|
||||
64
playground/src/utils/date-range-codec.ts
Normal file
64
playground/src/utils/date-range-codec.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
type DateRange = [Dayjs, Dayjs];
|
||||
|
||||
type DateRangeSubmitValues<
|
||||
TFormValues extends Record<string, unknown>,
|
||||
TRangeField extends keyof TFormValues,
|
||||
TStartField extends string,
|
||||
TEndField extends string,
|
||||
> = Omit<TFormValues, TRangeField> &
|
||||
Record<TEndField | TStartField, string | undefined>;
|
||||
|
||||
interface DateRangeCodecOptions<
|
||||
TFormValues extends Record<string, unknown>,
|
||||
TRangeField extends keyof TFormValues & string,
|
||||
TStartField extends string,
|
||||
TEndField extends string,
|
||||
> {
|
||||
endField: TEndField;
|
||||
rangeField: TRangeField;
|
||||
startField: TStartField;
|
||||
}
|
||||
|
||||
export function createDateRangeCodec<
|
||||
TFormValues extends Record<string, unknown>,
|
||||
>() {
|
||||
return function createCodec<
|
||||
TRangeField extends keyof TFormValues & string,
|
||||
TStartField extends string,
|
||||
TEndField extends string,
|
||||
>({
|
||||
endField,
|
||||
rangeField,
|
||||
startField,
|
||||
}: DateRangeCodecOptions<TFormValues, TRangeField, TStartField, TEndField>) {
|
||||
type SubmitValues = DateRangeSubmitValues<
|
||||
TFormValues,
|
||||
TRangeField,
|
||||
TStartField,
|
||||
TEndField
|
||||
>;
|
||||
|
||||
return {
|
||||
decode(values: Readonly<SubmitValues>): TFormValues {
|
||||
const { [endField]: end, [startField]: start, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
...(start && end ? { [rangeField]: [dayjs(start), dayjs(end)] } : {}),
|
||||
} as TFormValues;
|
||||
},
|
||||
encode(values: Readonly<TFormValues>): SubmitValues {
|
||||
const { [rangeField]: value, ...formValues } = values;
|
||||
const range = value as DateRange | undefined;
|
||||
return {
|
||||
...formValues,
|
||||
[endField]: range?.[1]?.format('YYYY-MM-DD'),
|
||||
[startField]: range?.[0]?.format('YYYY-MM-DD'),
|
||||
} as SubmitValues;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -121,7 +121,7 @@ const schema: VbenFormSchema<ArrayFormValues>[] = [
|
||||
defaultValue: '',
|
||||
fieldName: 'name',
|
||||
label: '姓名',
|
||||
rules: z.string().min(1, '请输入姓名'),
|
||||
rules: z.string().trim().min(1, '请输入姓名'),
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
|
||||
@@ -11,6 +11,7 @@ import dayjs from 'dayjs';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getExampleTableApi } from '#/api';
|
||||
import { createDateRangeCodec } from '#/utils/date-range-codec';
|
||||
|
||||
interface RowType {
|
||||
category: string;
|
||||
@@ -29,32 +30,16 @@ interface SearchFormValues extends Record<string, unknown> {
|
||||
productName?: string;
|
||||
}
|
||||
|
||||
function encodeSearchFormValues(values: Readonly<SearchFormValues>) {
|
||||
const { date, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
end: date?.[1]?.format('YYYY-MM-DD'),
|
||||
start: date?.[0]?.format('YYYY-MM-DD'),
|
||||
};
|
||||
}
|
||||
const searchCodec = createDateRangeCodec<SearchFormValues>()({
|
||||
endField: 'end',
|
||||
rangeField: 'date',
|
||||
startField: 'start',
|
||||
});
|
||||
|
||||
type SearchSubmitValues = ReturnType<typeof encodeSearchFormValues>;
|
||||
|
||||
function decodeSearchFormValues(
|
||||
values: Readonly<SearchSubmitValues>,
|
||||
): SearchFormValues {
|
||||
const { end, start, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
...(start && end ? { date: [dayjs(start), dayjs(end)] } : {}),
|
||||
};
|
||||
}
|
||||
type SearchSubmitValues = ReturnType<typeof searchCodec.encode>;
|
||||
|
||||
const formOptions: VbenFormProps<SearchFormValues, SearchSubmitValues> = {
|
||||
codec: {
|
||||
decode: decodeSearchFormValues,
|
||||
encode: encodeSearchFormValues,
|
||||
},
|
||||
codec: searchCodec,
|
||||
// 默认展开
|
||||
collapsed: false,
|
||||
schema: [
|
||||
|
||||
@@ -40,15 +40,19 @@ function encodeMenuFormValues(
|
||||
): MenuSubmitValues {
|
||||
const { linkSrc, ...formValues } = values;
|
||||
if (values.type === 'link') {
|
||||
const meta = { ...values.meta, link: linkSrc };
|
||||
Reflect.deleteProperty(meta, 'iframeSrc');
|
||||
return {
|
||||
...formValues,
|
||||
meta: { ...values.meta, link: linkSrc },
|
||||
meta,
|
||||
};
|
||||
}
|
||||
if (values.type === 'embedded') {
|
||||
const meta = { ...values.meta, iframeSrc: linkSrc };
|
||||
Reflect.deleteProperty(meta, 'link');
|
||||
return {
|
||||
...formValues,
|
||||
meta: { ...values.meta, iframeSrc: linkSrc },
|
||||
meta,
|
||||
};
|
||||
}
|
||||
return formValues;
|
||||
|
||||
@@ -13,11 +13,11 @@ import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, message, Modal } from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteRole, getRoleList, updateRole } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
import { createDateRangeCodec } from '#/utils/date-range-codec';
|
||||
|
||||
import { useColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
@@ -26,28 +26,13 @@ interface RoleSearchFormValues extends Record<string, unknown> {
|
||||
createTime?: [Dayjs, Dayjs];
|
||||
}
|
||||
|
||||
function encodeRoleSearchValues(values: Readonly<RoleSearchFormValues>) {
|
||||
const { createTime, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
endTime: createTime?.[1]?.format('YYYY-MM-DD'),
|
||||
startTime: createTime?.[0]?.format('YYYY-MM-DD'),
|
||||
};
|
||||
}
|
||||
const roleSearchCodec = createDateRangeCodec<RoleSearchFormValues>()({
|
||||
endField: 'endTime',
|
||||
rangeField: 'createTime',
|
||||
startField: 'startTime',
|
||||
});
|
||||
|
||||
type RoleSearchSubmitValues = ReturnType<typeof encodeRoleSearchValues>;
|
||||
|
||||
function decodeRoleSearchValues(
|
||||
values: Readonly<RoleSearchSubmitValues>,
|
||||
): RoleSearchFormValues {
|
||||
const { endTime, startTime, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
...(startTime && endTime
|
||||
? { createTime: [dayjs(startTime), dayjs(endTime)] }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
type RoleSearchSubmitValues = ReturnType<typeof roleSearchCodec.encode>;
|
||||
|
||||
const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||
connectedComponent: Form,
|
||||
@@ -56,10 +41,7 @@ const [FormDrawer, formDrawerApi] = useVbenDrawer({
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
codec: {
|
||||
decode: decodeRoleSearchValues,
|
||||
encode: encodeRoleSearchValues,
|
||||
},
|
||||
codec: roleSearchCodec,
|
||||
schema: useGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
|
||||
@@ -12,11 +12,11 @@ import { Page, Tree, useVbenDrawer } from '@vben/common-ui';
|
||||
import { Plus } from '@vben/icons';
|
||||
|
||||
import { Button, Card, InputSearch, message, Modal } from 'antdv-next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { useVbenVxeGrid, VbenTableAction } from '#/adapter/vxe-table';
|
||||
import { deleteUser, getDeptList, getUserList, updateUser } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
import { createDateRangeCodec } from '#/utils/date-range-codec';
|
||||
|
||||
import { useColumns, useGridFormSchema } from './data';
|
||||
import Detail from './modules/detail.vue';
|
||||
@@ -26,28 +26,13 @@ interface UserSearchFormValues extends Record<string, unknown> {
|
||||
createTime?: [Dayjs, Dayjs];
|
||||
}
|
||||
|
||||
function encodeUserSearchValues(values: Readonly<UserSearchFormValues>) {
|
||||
const { createTime, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
endTime: createTime?.[1]?.format('YYYY-MM-DD'),
|
||||
startTime: createTime?.[0]?.format('YYYY-MM-DD'),
|
||||
};
|
||||
}
|
||||
const userSearchCodec = createDateRangeCodec<UserSearchFormValues>()({
|
||||
endField: 'endTime',
|
||||
rangeField: 'createTime',
|
||||
startField: 'startTime',
|
||||
});
|
||||
|
||||
type UserSearchSubmitValues = ReturnType<typeof encodeUserSearchValues>;
|
||||
|
||||
function decodeUserSearchValues(
|
||||
values: Readonly<UserSearchSubmitValues>,
|
||||
): UserSearchFormValues {
|
||||
const { endTime, startTime, ...formValues } = values;
|
||||
return {
|
||||
...formValues,
|
||||
...(startTime && endTime
|
||||
? { createTime: [dayjs(startTime), dayjs(endTime)] }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
type UserSearchSubmitValues = ReturnType<typeof userSearchCodec.encode>;
|
||||
|
||||
const deptList = ref<SystemDeptApi.SystemDept[]>([]);
|
||||
const inputSearchValue = ref('');
|
||||
@@ -65,10 +50,7 @@ const [DetailDrawer, detailDrawerApi] = useVbenDrawer({
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
codec: {
|
||||
decode: decodeUserSearchValues,
|
||||
encode: encodeUserSearchValues,
|
||||
},
|
||||
codec: userSearchCodec,
|
||||
schema: useGridFormSchema(),
|
||||
submitOnChange: true,
|
||||
},
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -75,6 +75,9 @@ catalogs:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.3.3
|
||||
version: 4.3.3
|
||||
'@tanstack/store':
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0
|
||||
'@tanstack/vue-form':
|
||||
specifier: ^1.33.2
|
||||
version: 1.33.2
|
||||
@@ -1400,6 +1403,9 @@ importers:
|
||||
|
||||
packages/@core/ui-kit/form-ui:
|
||||
dependencies:
|
||||
'@tanstack/store':
|
||||
specifier: 'catalog:'
|
||||
version: 0.11.0
|
||||
'@tanstack/vue-form':
|
||||
specifier: 'catalog:'
|
||||
version: 1.33.2(vue@3.5.40(typescript@6.0.3))
|
||||
|
||||
@@ -62,6 +62,7 @@ catalog:
|
||||
'@stylistic/stylelint-plugin': ^5.2.1
|
||||
'@tailwindcss/typography': ^0.5.20
|
||||
'@tailwindcss/vite': ^4.3.3
|
||||
'@tanstack/store': ^0.11.0
|
||||
'@tanstack/vue-form': ^1.33.2
|
||||
'@tanstack/vue-query': ^5.101.2
|
||||
'@tanstack/vue-store': ^0.11.0
|
||||
|
||||
Reference in New Issue
Block a user