Merge branch 'vbenjs:main' into main
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { BaseFormComponentType } from '../src/types';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FormApi } from '../src/form-api';
|
||||
@@ -106,6 +108,99 @@ describe('formApi', () => {
|
||||
expect(formActions.values).toEqual(originalValuesSnapshot);
|
||||
});
|
||||
|
||||
it('should encode submissions and decode complete values with a codec', async () => {
|
||||
interface FilterFormValues {
|
||||
period: [number, number];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface FilterSubmitValues {
|
||||
endTime: number;
|
||||
startTime: number;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
const setValues = vi.fn();
|
||||
const codecFormApi = new FormApi<
|
||||
FilterFormValues,
|
||||
BaseFormComponentType,
|
||||
Record<never, never>,
|
||||
FilterSubmitValues
|
||||
>({
|
||||
codec: {
|
||||
decode(values) {
|
||||
return {
|
||||
period: [values.startTime, values.endTime],
|
||||
tags: values.tags.split(','),
|
||||
};
|
||||
},
|
||||
encode(values) {
|
||||
return {
|
||||
endTime: values.period[1],
|
||||
startTime: values.period[0],
|
||||
tags: values.tags.join(','),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
setValues,
|
||||
values: { period: [1, 2], tags: ['admin', 'user'] },
|
||||
};
|
||||
|
||||
await codecFormApi.mount(formActions, new Map());
|
||||
|
||||
expect(await codecFormApi.getValues()).toEqual({
|
||||
endTime: 2,
|
||||
startTime: 1,
|
||||
tags: 'admin,user',
|
||||
});
|
||||
expect(await codecFormApi.getValueSnapshot()).toEqual({
|
||||
rawValues: { period: [1, 2], tags: ['admin', 'user'] },
|
||||
values: { endTime: 2, startTime: 1, tags: 'admin,user' },
|
||||
});
|
||||
|
||||
await codecFormApi.setSubmitValues(
|
||||
{ endTime: 4, startTime: 3, tags: 'editor' },
|
||||
false,
|
||||
);
|
||||
expect(setValues).toHaveBeenCalledWith(
|
||||
{ period: [3, 4], tags: ['editor'] },
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should scan deprecated schema transforms once for unchanged state', async () => {
|
||||
const getChildren = vi.fn(() => []);
|
||||
const schema = {
|
||||
component: 'text',
|
||||
fieldName: 'name',
|
||||
get children() {
|
||||
return getChildren();
|
||||
},
|
||||
} as any;
|
||||
const codecFormApi = new FormApi({
|
||||
codec: {
|
||||
decode: (values) => values,
|
||||
encode: (values) => values,
|
||||
},
|
||||
schema: [schema],
|
||||
});
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
values: { name: 'Ada' },
|
||||
};
|
||||
|
||||
await codecFormApi.mount(formActions, new Map());
|
||||
expect(getChildren).toHaveBeenCalledTimes(1);
|
||||
|
||||
await codecFormApi.getValues();
|
||||
await codecFormApi.getValues();
|
||||
|
||||
expect(getChildren).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should format child schema values inside array fields', async () => {
|
||||
formApi.setState({
|
||||
schema: [
|
||||
|
||||
81
packages/@core/ui-kit/form-ui/__tests__/form-codec.test.ts
Normal file
81
packages/@core/ui-kit/form-ui/__tests__/form-codec.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { FormCodec } from '../src/types';
|
||||
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest';
|
||||
|
||||
import {
|
||||
decodeFormValues,
|
||||
encodeFormValues,
|
||||
FormCodecError,
|
||||
} from '../src/form-codec';
|
||||
|
||||
interface FilterFormValues {
|
||||
period: [number, number];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface FilterSubmitValues {
|
||||
endTime: number;
|
||||
startTime: number;
|
||||
tags: string;
|
||||
}
|
||||
|
||||
const filterCodec: FormCodec<FilterFormValues, FilterSubmitValues> = {
|
||||
decode(values) {
|
||||
return {
|
||||
period: [values.startTime, values.endTime],
|
||||
tags: values.tags ? values.tags.split(',') : [],
|
||||
};
|
||||
},
|
||||
encode(values) {
|
||||
return {
|
||||
endTime: values.period[1],
|
||||
startTime: values.period[0],
|
||||
tags: values.tags.join(','),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
describe('form codec', () => {
|
||||
it('encodes and decodes complete form values', () => {
|
||||
const submitValues = encodeFormValues(filterCodec, {
|
||||
period: [1, 2],
|
||||
tags: ['admin', 'user'],
|
||||
});
|
||||
|
||||
expect(submitValues).toEqual({
|
||||
endTime: 2,
|
||||
startTime: 1,
|
||||
tags: 'admin,user',
|
||||
});
|
||||
expect(decodeFormValues(filterCodec, submitValues)).toEqual({
|
||||
period: [1, 2],
|
||||
tags: ['admin', 'user'],
|
||||
});
|
||||
expectTypeOf(submitValues).toEqualTypeOf<FilterSubmitValues>();
|
||||
});
|
||||
|
||||
it('reports the failed codec phase without mutating inputs', () => {
|
||||
const values = Object.freeze({ period: [1, 2], tags: ['admin'] }) as {
|
||||
period: [number, number];
|
||||
tags: string[];
|
||||
};
|
||||
const codec: FormCodec<FilterFormValues, FilterSubmitValues> = {
|
||||
decode: filterCodec.decode,
|
||||
encode() {
|
||||
throw new Error('broken encoder');
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => encodeFormValues(codec, values)).toThrowError(FormCodecError);
|
||||
|
||||
let codecError: unknown;
|
||||
try {
|
||||
encodeFormValues(codec, values);
|
||||
} catch (error) {
|
||||
codecError = error;
|
||||
}
|
||||
expect(codecError).toBeInstanceOf(FormCodecError);
|
||||
expect(codecError).toMatchObject({ phase: 'encode' });
|
||||
expect(values).toEqual({ period: [1, 2], tags: ['admin'] });
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { BaseFormComponentType } from '../src/types';
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { setupVbenForm } from '../src/config';
|
||||
@@ -14,6 +16,83 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('form api compatibility', () => {
|
||||
it('keeps deprecated value transforms and warns once per API', async () => {
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const formApi = new FormApi({
|
||||
arrayToStringFields: ['tags'],
|
||||
fieldMappingTime: [['period', ['startTime', 'endTime'], null]],
|
||||
schema: [
|
||||
{
|
||||
component: 'input',
|
||||
fieldName: 'name',
|
||||
valueFormat: (value: string) => value.trim(),
|
||||
},
|
||||
],
|
||||
});
|
||||
const form = {
|
||||
meta: {},
|
||||
values: {
|
||||
name: ' Ada ',
|
||||
period: [1, 2],
|
||||
tags: ['admin', 'user'],
|
||||
},
|
||||
} as any;
|
||||
|
||||
formApi.mount(form);
|
||||
|
||||
expect(await formApi.getValues()).toEqual({
|
||||
endTime: 2,
|
||||
name: 'Ada',
|
||||
startTime: 1,
|
||||
tags: 'admin,user',
|
||||
});
|
||||
expect(warning).toHaveBeenCalledTimes(3);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[Vben Form] `schema.valueFormat` is deprecated. Use the form-level `codec` instead.',
|
||||
);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[Vben Form] `fieldMappingTime` is deprecated. Use the form-level `codec` instead.',
|
||||
);
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[Vben Form] `arrayToStringFields` is deprecated. Use the form-level `codec` instead.',
|
||||
);
|
||||
});
|
||||
|
||||
it('prefers the codec when deprecated transforms are also configured', async () => {
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const formApi = new FormApi<
|
||||
{ name: string },
|
||||
BaseFormComponentType,
|
||||
Record<never, never>,
|
||||
{ normalizedName: string }
|
||||
>({
|
||||
codec: {
|
||||
decode(values) {
|
||||
return { name: values.normalizedName };
|
||||
},
|
||||
encode(values) {
|
||||
return { normalizedName: values.name.toUpperCase() };
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
component: 'input',
|
||||
fieldName: 'name',
|
||||
valueFormat: () => 'legacy',
|
||||
},
|
||||
],
|
||||
});
|
||||
const form = { meta: {}, values: { name: 'Ada' } } as any;
|
||||
|
||||
formApi.mount(form);
|
||||
|
||||
expect(await formApi.getValues()).toEqual({ normalizedName: 'ADA' });
|
||||
expect(warning).toHaveBeenCalledOnce();
|
||||
expect(warning).toHaveBeenCalledWith(
|
||||
'[Vben Form] The form `codec` takes precedence over deprecated `valueFormat`, `fieldMappingTime`, and `arrayToStringFields` options.',
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards defineRules and warns only once in development', async () => {
|
||||
const warning = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const legacyRule = () => 'legacy error';
|
||||
|
||||
@@ -653,4 +653,181 @@ describe('useVbenForm integration', () => {
|
||||
|
||||
expect(formApi.form.getFieldError('username')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('passes formatted values and raw values to handleSubmit callback', async () => {
|
||||
const handleSubmit = vi.fn();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode(values: Readonly<{ normalizedName: string }>) {
|
||||
return { name: values.normalizedName };
|
||||
},
|
||||
encode(values: Readonly<{ name: string }>) {
|
||||
return { normalizedName: values.name.trim().toUpperCase() };
|
||||
},
|
||||
},
|
||||
handleSubmit,
|
||||
schema: [
|
||||
{
|
||||
component: TestInput,
|
||||
defaultValue: '',
|
||||
fieldName: 'name',
|
||||
rules: 'required',
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
|
||||
await formApi.setFieldValue('name', ' test ');
|
||||
await formApi.validateAndSubmit();
|
||||
await flushPromises();
|
||||
|
||||
expect(handleSubmit).toHaveBeenCalledOnce();
|
||||
expect(handleSubmit).toHaveBeenCalledWith(
|
||||
{ normalizedName: 'TEST' },
|
||||
{ name: ' test ' },
|
||||
);
|
||||
});
|
||||
|
||||
it('does not expose raw values through native form submit events', async () => {
|
||||
const onSubmit = vi.fn();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{
|
||||
component: TestInput,
|
||||
defaultValue: '',
|
||||
fieldName: 'name',
|
||||
rules: 'required',
|
||||
valueFormat: (value) => value.trim().toUpperCase(),
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form, { attrs: { onSubmit } });
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
|
||||
await formApi.setFieldValue('name', ' raw ');
|
||||
await wrapper.get('form').trigger('submit');
|
||||
await flushPromises();
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledOnce();
|
||||
expect(onSubmit).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('calls handleReset with formatted values on reset button click', async () => {
|
||||
const handleReset = vi.fn();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode(values: Readonly<{ normalizedName: string }>) {
|
||||
return { name: values.normalizedName };
|
||||
},
|
||||
encode(values: Readonly<{ name: string }>) {
|
||||
return { normalizedName: values.name.toUpperCase() };
|
||||
},
|
||||
},
|
||||
handleReset,
|
||||
schema: [
|
||||
{
|
||||
component: TestInput,
|
||||
defaultValue: 'hello',
|
||||
fieldName: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
|
||||
expect(await formApi.getValues()).toEqual({ normalizedName: 'HELLO' });
|
||||
const resetButton = wrapper.findAll('button')[0];
|
||||
expect(resetButton).toBeDefined();
|
||||
if (!resetButton) return;
|
||||
await resetButton.trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(handleReset).toHaveBeenCalledOnce();
|
||||
expect(handleReset).toHaveBeenCalledWith({ normalizedName: 'HELLO' });
|
||||
});
|
||||
|
||||
it('setValues with multiple fields emits only the final values', async () => {
|
||||
const handleValuesChange = vi.fn();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
handleValuesChange,
|
||||
schema: [
|
||||
{ component: TestInput, fieldName: 'first' },
|
||||
{ component: TestInput, fieldName: 'second' },
|
||||
{ component: TestInput, fieldName: 'third' },
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
const initialCallCount = handleValuesChange.mock.calls.length;
|
||||
|
||||
await formApi.setValues({ first: 'a', second: 'b', third: 'c' });
|
||||
await flushPromises();
|
||||
|
||||
const calls = handleValuesChange.mock.calls.slice(initialCallCount);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.[0]).toEqual({
|
||||
first: 'a',
|
||||
second: 'b',
|
||||
third: 'c',
|
||||
});
|
||||
});
|
||||
|
||||
it('retains initial values for unspecified fields on partial reset', async () => {
|
||||
const [Form, formApi] = useVbenForm({
|
||||
schema: [
|
||||
{ component: TestInput, defaultValue: 'original', fieldName: 'name' },
|
||||
{ component: TestInput, defaultValue: 'keep', fieldName: 'alias' },
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
|
||||
await formApi.setValues({ alias: 'changed', name: 'changed' });
|
||||
await formApi.reset({ values: { name: 'reset' } });
|
||||
await flushPromises();
|
||||
|
||||
expect(await formApi.getValues()).toEqual({
|
||||
alias: 'keep',
|
||||
name: 'reset',
|
||||
});
|
||||
});
|
||||
|
||||
it('applies valueFormat consistently across value APIs', async () => {
|
||||
const handleSubmit = vi.fn();
|
||||
const [Form, formApi] = useVbenForm({
|
||||
handleSubmit,
|
||||
schema: [
|
||||
{
|
||||
component: TestInput,
|
||||
fieldName: 'name',
|
||||
valueFormat: (value) => (value ? value.trim().toUpperCase() : ''),
|
||||
},
|
||||
],
|
||||
});
|
||||
const wrapper = mount(Form);
|
||||
wrappers.push(wrapper);
|
||||
await flushPromises();
|
||||
|
||||
await formApi.setFieldValue('name', ' hello ');
|
||||
await flushPromises();
|
||||
|
||||
expect(await formApi.getValues()).toEqual({ name: 'HELLO' });
|
||||
expect(await formApi.getValueSnapshot()).toEqual({
|
||||
rawValues: { name: ' hello ' },
|
||||
values: { name: 'HELLO' },
|
||||
});
|
||||
|
||||
await formApi.validateAndSubmit();
|
||||
await flushPromises();
|
||||
expect(handleSubmit).toHaveBeenCalledWith(
|
||||
{ name: 'HELLO' },
|
||||
{ name: ' hello ' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
FormFieldOptions,
|
||||
FormItemDependencies,
|
||||
FormValidationResult,
|
||||
FormValueSnapshot,
|
||||
VbenFormAdapterOptions,
|
||||
VbenFormProps,
|
||||
} from '../src/types';
|
||||
@@ -22,6 +23,12 @@ interface AccountFormValues {
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
interface AccountSubmitValues {
|
||||
email: string;
|
||||
nickname: string;
|
||||
roles: string;
|
||||
}
|
||||
|
||||
describe('form public types', () => {
|
||||
it('keeps the compatibility alias and stable method signatures', () => {
|
||||
expectTypeOf<FormActions>().toEqualTypeOf<FormContextApi>();
|
||||
@@ -142,6 +149,96 @@ describe('form public types', () => {
|
||||
>().toEqualTypeOf<AccountFormValues>();
|
||||
});
|
||||
|
||||
it('keeps form and submit values distinct with a codec', () => {
|
||||
const options: VbenFormProps<
|
||||
BaseFormComponentType,
|
||||
Record<never, never>,
|
||||
AccountFormValues,
|
||||
AccountSubmitValues
|
||||
> = {
|
||||
codec: {
|
||||
decode(values) {
|
||||
return {
|
||||
email: values.email,
|
||||
profile: { nickname: values.nickname },
|
||||
roles: values.roles.split(','),
|
||||
};
|
||||
},
|
||||
encode(values) {
|
||||
return {
|
||||
email: values.email,
|
||||
nickname: values.profile.nickname,
|
||||
roles: values.roles.join(','),
|
||||
};
|
||||
},
|
||||
},
|
||||
handleSubmit(values, rawValues) {
|
||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||
expectTypeOf(rawValues).toEqualTypeOf<Readonly<AccountFormValues>>();
|
||||
},
|
||||
handleReset(values) {
|
||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||
},
|
||||
handleValuesChange(values, _fieldsChanged, getFormattedValues) {
|
||||
expectTypeOf(values).toEqualTypeOf<Readonly<AccountFormValues>>();
|
||||
expectTypeOf(getFormattedValues()).toEqualTypeOf<AccountSubmitValues>();
|
||||
},
|
||||
schema: [],
|
||||
};
|
||||
const [, formApi] = useVbenForm<
|
||||
AccountFormValues,
|
||||
BaseFormComponentType,
|
||||
Record<never, never>,
|
||||
AccountSubmitValues
|
||||
>(options);
|
||||
|
||||
expectTypeOf(
|
||||
formApi.getValues(),
|
||||
).resolves.toEqualTypeOf<AccountSubmitValues>();
|
||||
expectTypeOf(
|
||||
formApi.getRawValues(),
|
||||
).resolves.toEqualTypeOf<AccountFormValues>();
|
||||
expectTypeOf(formApi.getValueSnapshot()).resolves.toEqualTypeOf<
|
||||
FormValueSnapshot<AccountFormValues, AccountSubmitValues>
|
||||
>();
|
||||
expectTypeOf(formApi.setSubmitValues)
|
||||
.parameter(0)
|
||||
.toEqualTypeOf<AccountSubmitValues>();
|
||||
});
|
||||
|
||||
it('infers submit values from an inline codec', () => {
|
||||
const [, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode(values) {
|
||||
expectTypeOf(values).toEqualTypeOf<Readonly<AccountSubmitValues>>();
|
||||
return {
|
||||
email: values.email,
|
||||
profile: { nickname: values.nickname },
|
||||
roles: values.roles.split(','),
|
||||
};
|
||||
},
|
||||
encode(values: Readonly<AccountFormValues>) {
|
||||
return {
|
||||
email: values.email,
|
||||
nickname: values.profile.nickname,
|
||||
roles: values.roles.join(','),
|
||||
};
|
||||
},
|
||||
},
|
||||
handleReset(values) {
|
||||
expectTypeOf(values).toEqualTypeOf<AccountSubmitValues>();
|
||||
},
|
||||
schema: [],
|
||||
});
|
||||
|
||||
expectTypeOf(
|
||||
formApi.getValues(),
|
||||
).resolves.toEqualTypeOf<AccountSubmitValues>();
|
||||
expectTypeOf(
|
||||
formApi.getRawValues(),
|
||||
).resolves.toEqualTypeOf<AccountFormValues>();
|
||||
});
|
||||
|
||||
it('exposes canonical names alongside deprecated aliases', () => {
|
||||
expectTypeOf<FormContextApi['reset']>().toEqualTypeOf<
|
||||
FormContextApi['resetForm']
|
||||
|
||||
@@ -29,14 +29,16 @@ import {
|
||||
|
||||
import { warnDeprecatedOnce } from './deprecation';
|
||||
import { resolveFieldNamePath } from './field-name';
|
||||
import { decodeFormValues, encodeFormValues } from './form-codec';
|
||||
import { updateFormSchemaList } from './form-render/schema';
|
||||
import { formatFormValues } from './form-value-transform';
|
||||
|
||||
type FormApiProps<
|
||||
TValues extends FormValues,
|
||||
TFormValues extends FormValues,
|
||||
T extends BaseFormComponentType,
|
||||
P extends Record<string, any>,
|
||||
> = VbenFormProps<T, P, TValues>;
|
||||
TSubmitValues extends FormValues,
|
||||
> = VbenFormProps<T, P, TFormValues, TSubmitValues>;
|
||||
|
||||
type FormApiSchema<
|
||||
TValues extends FormValues,
|
||||
@@ -45,10 +47,11 @@ type FormApiSchema<
|
||||
> = FormSchema<T, P, TValues>;
|
||||
|
||||
function getDefaultState<
|
||||
TValues extends FormValues,
|
||||
TFormValues extends FormValues,
|
||||
T extends BaseFormComponentType,
|
||||
P extends Record<string, any>,
|
||||
>(): FormApiProps<TValues, T, P> {
|
||||
TSubmitValues extends FormValues,
|
||||
>(): FormApiProps<TFormValues, T, P, TSubmitValues> {
|
||||
return {
|
||||
actionWrapperClass: '',
|
||||
collapsed: false,
|
||||
@@ -73,18 +76,19 @@ function getDefaultState<
|
||||
}
|
||||
|
||||
export class FormApi<
|
||||
TValues extends FormValues = FormValues,
|
||||
TFormValues extends FormValues = FormValues,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> {
|
||||
// private api: Pick<VbenFormProps, 'handleReset' | 'handleSubmit'>;
|
||||
public form = {} as FormActions<TValues>;
|
||||
public form = {} as FormActions<TFormValues>;
|
||||
isMounted = false;
|
||||
|
||||
public state: FormApiProps<TValues, T, P> | null = null;
|
||||
public state: FormApiProps<TFormValues, T, P, TSubmitValues> | null = null;
|
||||
stateHandler: StateHandler;
|
||||
|
||||
public store: Store<FormApiProps<TValues, T, P>>;
|
||||
public store: Store<FormApiProps<TFormValues, T, P, TSubmitValues>>;
|
||||
|
||||
/**
|
||||
* 组件实例映射
|
||||
@@ -92,16 +96,22 @@ export class FormApi<
|
||||
private componentRefMap: Map<string, unknown> = new Map();
|
||||
|
||||
// 最后一次点击提交时的表单值
|
||||
private latestSubmissionValues: null | Partial<TValues> = null;
|
||||
private latestSubmissionValues: null | Partial<TSubmitValues> = null;
|
||||
|
||||
private prevState: FormApiProps<TValues, T, P> | null = null;
|
||||
private legacyTransformWarningState: null | Pick<
|
||||
FormApiProps<TFormValues, T, P, TSubmitValues>,
|
||||
'arrayToStringFields' | 'codec' | 'fieldMappingTime' | 'schema'
|
||||
> = null;
|
||||
|
||||
constructor(options: FormApiProps<TValues, T, P> = {}) {
|
||||
private prevState: FormApiProps<TFormValues, T, P, TSubmitValues> | null =
|
||||
null;
|
||||
|
||||
constructor(options: FormApiProps<TFormValues, T, P, TSubmitValues> = {}) {
|
||||
const { ...storeState } = options;
|
||||
|
||||
const defaultState = getDefaultState<TValues, T, P>();
|
||||
const defaultState = getDefaultState<TFormValues, T, P, TSubmitValues>();
|
||||
|
||||
this.store = new Store<FormApiProps<TValues, T, P>>({
|
||||
this.store = new Store<FormApiProps<TFormValues, T, P, TSubmitValues>>({
|
||||
...defaultState,
|
||||
...storeState,
|
||||
});
|
||||
@@ -118,21 +128,31 @@ export class FormApi<
|
||||
}
|
||||
|
||||
async clearValidation(
|
||||
fieldNames?: FormFieldName<TValues> | FormFieldName<TValues>[],
|
||||
fieldNames?: FormFieldName<TFormValues> | FormFieldName<TFormValues>[],
|
||||
) {
|
||||
const form = await this.getForm();
|
||||
form.clearValidation(fieldNames);
|
||||
}
|
||||
|
||||
formatValues<TResult extends FormValues = TValues>(
|
||||
formatValues(rawValues: Readonly<TFormValues>): TSubmitValues;
|
||||
/** @deprecated Declare the submit type on `useVbenForm` instead. */
|
||||
formatValues<TResult extends FormValues>(
|
||||
rawValues: Readonly<FormValues>,
|
||||
) {
|
||||
): TResult;
|
||||
formatValues(rawValues: Readonly<FormValues>): FormValues {
|
||||
this.warnLegacyValueTransforms();
|
||||
if (this.state?.codec) {
|
||||
return encodeFormValues(
|
||||
this.state.codec,
|
||||
rawValues as Readonly<TFormValues>,
|
||||
);
|
||||
}
|
||||
return formatFormValues(
|
||||
toRaw(rawValues),
|
||||
this.state?.schema ?? [],
|
||||
this.state?.fieldMappingTime,
|
||||
this.state?.arrayToStringFields,
|
||||
) as TResult;
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,41 +216,52 @@ export class FormApi<
|
||||
return this.latestSubmissionValues || {};
|
||||
}
|
||||
|
||||
async getRawValues<TResult extends FormValues = TValues>() {
|
||||
async getRawValues(): Promise<TFormValues>;
|
||||
/** @deprecated Declare the form value type on `useVbenForm` instead. */
|
||||
async getRawValues<TResult extends FormValues>(): Promise<TResult>;
|
||||
async getRawValues(): Promise<FormValues> {
|
||||
const form = await this.getForm();
|
||||
return cloneDeep(toRaw(form.values ?? {})) as unknown as TResult;
|
||||
return cloneDeep(toRaw(form.values ?? {}));
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
async getValues<TResult extends FormValues = TValues>() {
|
||||
async getValues(): Promise<TSubmitValues>;
|
||||
/** @deprecated Declare the submit type on `useVbenForm` instead. */
|
||||
async getValues<TResult extends FormValues>(): Promise<TResult>;
|
||||
async getValues(): Promise<FormValues> {
|
||||
const form = await this.getForm();
|
||||
return this.formatValues<TResult>(toRaw(form.values ?? {}));
|
||||
return this.formatValues(toRaw(form.values ?? {}));
|
||||
}
|
||||
|
||||
async getValueSnapshot<TResult extends FormValues = TValues>(): Promise<
|
||||
async getValueSnapshot(): Promise<
|
||||
FormValueSnapshot<TFormValues, TSubmitValues>
|
||||
>;
|
||||
/** @deprecated Declare form and submit value types on `useVbenForm`. */
|
||||
async getValueSnapshot<TResult extends FormValues>(): Promise<
|
||||
FormValueSnapshot<TResult>
|
||||
> {
|
||||
const rawValues = await this.getRawValues<TResult>();
|
||||
>;
|
||||
async getValueSnapshot(): Promise<FormValueSnapshot> {
|
||||
const rawValues = await this.getRawValues();
|
||||
return {
|
||||
rawValues,
|
||||
values: this.formatValues<TResult>(rawValues),
|
||||
values: this.formatValues(rawValues),
|
||||
};
|
||||
}
|
||||
|
||||
async isFieldValid(fieldName: FormFieldName<TValues>) {
|
||||
async isFieldValid(fieldName: FormFieldName<TFormValues>) {
|
||||
const form = await this.getForm();
|
||||
return form.isFieldValid(fieldName);
|
||||
}
|
||||
|
||||
merge(formApi: FormApi<any, any, any>) {
|
||||
merge(formApi: FormApi<any, any, any, any>) {
|
||||
const chain = [this, formApi];
|
||||
const proxy = new Proxy(formApi, {
|
||||
get(target: any, prop: any) {
|
||||
if (prop === 'merge') {
|
||||
return (nextFormApi: FormApi<any, any, any>) => {
|
||||
return (nextFormApi: FormApi<any, any, any, any>) => {
|
||||
chain.push(nextFormApi);
|
||||
return proxy;
|
||||
};
|
||||
@@ -266,7 +297,7 @@ export class FormApi<
|
||||
}
|
||||
|
||||
mount(
|
||||
formActions: FormActions<TValues>,
|
||||
formActions: FormActions<TFormValues>,
|
||||
componentRefMap?: Map<string, unknown>,
|
||||
) {
|
||||
if (!this.isMounted) {
|
||||
@@ -275,9 +306,7 @@ export class FormApi<
|
||||
const initialValues = this.form.values
|
||||
? this.formatValues(toRaw(this.form.values))
|
||||
: {};
|
||||
this.setLatestSubmissionValues({
|
||||
...initialValues,
|
||||
} as Partial<TValues>);
|
||||
this.setLatestSubmissionValues(initialValues);
|
||||
this.componentRefMap =
|
||||
componentRefMap ?? this.componentRefMap ?? new Map();
|
||||
this.isMounted = true;
|
||||
@@ -302,13 +331,16 @@ export class FormApi<
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
async reset(state?: FormResetState<TValues>, opts?: FormResetOptions) {
|
||||
async reset(state?: FormResetState<TFormValues>, opts?: FormResetOptions) {
|
||||
const form = await this.getForm();
|
||||
return form.reset(state, opts);
|
||||
}
|
||||
|
||||
/** @deprecated Use `reset` instead. */
|
||||
async resetForm(state?: FormResetState<TValues>, opts?: FormResetOptions) {
|
||||
async resetForm(
|
||||
state?: FormResetState<TFormValues>,
|
||||
opts?: FormResetOptions,
|
||||
) {
|
||||
warnDeprecatedOnce(
|
||||
'form-api-reset-form',
|
||||
'[Vben Form] `formApi.resetForm()` is deprecated. Use `formApi.reset()` instead.',
|
||||
@@ -359,27 +391,27 @@ export class FormApi<
|
||||
}
|
||||
}
|
||||
|
||||
async setFieldValue<TFieldName extends FormFieldName<TValues>>(
|
||||
async setFieldValue<TFieldName extends FormFieldName<TFormValues>>(
|
||||
field: TFieldName,
|
||||
value: FormFieldValue<TValues, NoInfer<TFieldName>>,
|
||||
value: FormFieldValue<TFormValues, NoInfer<TFieldName>>,
|
||||
shouldValidate?: boolean,
|
||||
) {
|
||||
const form = await this.getForm();
|
||||
await form.setFieldValue(field, value, shouldValidate);
|
||||
}
|
||||
|
||||
setLatestSubmissionValues(values: null | Partial<TValues>) {
|
||||
setLatestSubmissionValues(values: null | Partial<TSubmitValues>) {
|
||||
this.latestSubmissionValues = {
|
||||
...toRaw(values),
|
||||
} as Partial<TValues>;
|
||||
} as Partial<TSubmitValues>;
|
||||
}
|
||||
|
||||
setState(
|
||||
stateOrFn:
|
||||
| ((
|
||||
prev: FormApiProps<TValues, T, P>,
|
||||
) => Partial<FormApiProps<TValues, T, P>>)
|
||||
| Partial<FormApiProps<TValues, T, P>>,
|
||||
prev: FormApiProps<TFormValues, T, P, TSubmitValues>,
|
||||
) => Partial<FormApiProps<TFormValues, T, P, TSubmitValues>>)
|
||||
| Partial<FormApiProps<TFormValues, T, P, TSubmitValues>>,
|
||||
) {
|
||||
if (isFunction(stateOrFn)) {
|
||||
this.store.setState((prev) => {
|
||||
@@ -390,6 +422,21 @@ export class FormApi<
|
||||
}
|
||||
}
|
||||
|
||||
async setSubmitValues(
|
||||
values: TSubmitValues,
|
||||
filterFields: boolean = true,
|
||||
shouldValidate: boolean = false,
|
||||
) {
|
||||
const codec = this.state?.codec;
|
||||
if (!codec) {
|
||||
throw new Error(
|
||||
'[Vben Form] `setSubmitValues()` requires a form `codec`.',
|
||||
);
|
||||
}
|
||||
const formValues = decodeFormValues(codec, values);
|
||||
await this.setValues(formValues, filterFields, shouldValidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表单值
|
||||
* @param fields record
|
||||
@@ -397,7 +444,7 @@ export class FormApi<
|
||||
* @param shouldValidate
|
||||
*/
|
||||
async setValues(
|
||||
fields: Partial<TValues>,
|
||||
fields: Partial<TFormValues>,
|
||||
filterFields: boolean = true,
|
||||
shouldValidate: boolean = false,
|
||||
) {
|
||||
@@ -445,8 +492,8 @@ export class FormApi<
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const filteredFields = filterValue(fields) as Partial<TValues>;
|
||||
form.setValues(filteredFields as Partial<TValues>, shouldValidate);
|
||||
const filteredFields = filterValue(fields) as Partial<TFormValues>;
|
||||
form.setValues(filteredFields as Partial<TFormValues>, shouldValidate);
|
||||
}
|
||||
|
||||
async submit(e?: Event) {
|
||||
@@ -475,8 +522,8 @@ export class FormApi<
|
||||
this.stateHandler.reset();
|
||||
}
|
||||
|
||||
updateSchema(schema: Partial<FormApiSchema<TValues, T, P>>[]) {
|
||||
const updated: Partial<FormApiSchema<TValues, T, P>>[] = [...schema];
|
||||
updateSchema(schema: Partial<FormApiSchema<TFormValues, T, P>>[]) {
|
||||
const updated: Partial<FormApiSchema<TFormValues, T, P>>[] = [...schema];
|
||||
const hasField = updated.every(
|
||||
(item) => Reflect.has(item, 'fieldName') && item.fieldName,
|
||||
);
|
||||
@@ -523,7 +570,7 @@ export class FormApi<
|
||||
return this.validateAndSubmit();
|
||||
}
|
||||
|
||||
async validateField(fieldName: FormFieldName<TValues>) {
|
||||
async validateField(fieldName: FormFieldName<TFormValues>) {
|
||||
const form = await this.getForm();
|
||||
const validateResult = await form.validateField(fieldName);
|
||||
|
||||
@@ -568,9 +615,74 @@ export class FormApi<
|
||||
for (const schema of deletedSchema) {
|
||||
this.form?.setFieldValue?.(
|
||||
schema.fieldName,
|
||||
undefined as FormFieldValue<TValues, string>,
|
||||
undefined as FormFieldValue<TFormValues, string>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private warnLegacyValueTransforms() {
|
||||
const warningState = {
|
||||
arrayToStringFields: this.state?.arrayToStringFields,
|
||||
codec: this.state?.codec,
|
||||
fieldMappingTime: this.state?.fieldMappingTime,
|
||||
schema: this.state?.schema ?? [],
|
||||
};
|
||||
const previousState = this.legacyTransformWarningState;
|
||||
if (
|
||||
previousState &&
|
||||
previousState.arrayToStringFields === warningState.arrayToStringFields &&
|
||||
previousState.codec === warningState.codec &&
|
||||
previousState.fieldMappingTime === warningState.fieldMappingTime &&
|
||||
previousState.schema === warningState.schema
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.legacyTransformWarningState = warningState;
|
||||
|
||||
const hasValueFormat = (
|
||||
items: FormApiSchema<TFormValues, T, P>[],
|
||||
): boolean => {
|
||||
return items.some((schema) => {
|
||||
if (schema.valueFormat) {
|
||||
return true;
|
||||
}
|
||||
const children = 'children' in schema ? schema.children : undefined;
|
||||
return Array.isArray(children) && hasValueFormat(children);
|
||||
});
|
||||
};
|
||||
const usesValueFormat = hasValueFormat(warningState.schema);
|
||||
const usesFieldMappingTime =
|
||||
(warningState.fieldMappingTime?.length ?? 0) > 0;
|
||||
const usesArrayToStringFields =
|
||||
(warningState.arrayToStringFields?.length ?? 0) > 0;
|
||||
const usesLegacyTransform =
|
||||
usesValueFormat || usesFieldMappingTime || usesArrayToStringFields;
|
||||
|
||||
if (warningState.codec && usesLegacyTransform) {
|
||||
warnDeprecatedOnce(
|
||||
'form-codec-legacy-transform-conflict',
|
||||
'[Vben Form] The form `codec` takes precedence over deprecated `valueFormat`, `fieldMappingTime`, and `arrayToStringFields` options.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (usesValueFormat) {
|
||||
warnDeprecatedOnce(
|
||||
'form-schema-value-format',
|
||||
'[Vben Form] `schema.valueFormat` is deprecated. Use the form-level `codec` instead.',
|
||||
);
|
||||
}
|
||||
if (usesFieldMappingTime) {
|
||||
warnDeprecatedOnce(
|
||||
'form-field-mapping-time',
|
||||
'[Vben Form] `fieldMappingTime` is deprecated. Use the form-level `codec` instead.',
|
||||
);
|
||||
}
|
||||
if (usesArrayToStringFields) {
|
||||
warnDeprecatedOnce(
|
||||
'form-array-to-string-fields',
|
||||
'[Vben Form] `arrayToStringFields` is deprecated. Use the form-level `codec` instead.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
packages/@core/ui-kit/form-ui/src/form-codec.ts
Normal file
40
packages/@core/ui-kit/form-ui/src/form-codec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { FormCodec, FormValues } from './types';
|
||||
|
||||
export type FormCodecPhase = 'decode' | 'encode';
|
||||
|
||||
export class FormCodecError extends Error {
|
||||
override readonly cause: unknown;
|
||||
readonly phase: FormCodecPhase;
|
||||
|
||||
constructor(phase: FormCodecPhase, cause: unknown) {
|
||||
super(`[Vben Form] Failed to ${phase} form values.`);
|
||||
this.name = 'FormCodecError';
|
||||
this.cause = cause;
|
||||
this.phase = phase;
|
||||
}
|
||||
}
|
||||
|
||||
export function decodeFormValues<
|
||||
TFormValues extends FormValues,
|
||||
TSubmitValues extends FormValues,
|
||||
>(
|
||||
codec: FormCodec<TFormValues, TSubmitValues>,
|
||||
values: Readonly<TSubmitValues>,
|
||||
) {
|
||||
try {
|
||||
return codec.decode(values);
|
||||
} catch (error) {
|
||||
throw new FormCodecError('decode', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeFormValues<
|
||||
TFormValues extends FormValues,
|
||||
TSubmitValues extends FormValues,
|
||||
>(codec: FormCodec<TFormValues, TSubmitValues>, values: Readonly<TFormValues>) {
|
||||
try {
|
||||
return codec.encode(values);
|
||||
} catch (error) {
|
||||
throw new FormCodecError('encode', error);
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ const formComponent = 'form';
|
||||
const formComponentProps = computed(() => {
|
||||
return props.form
|
||||
? {
|
||||
onSubmit: props.form.handleSubmit((val) => emits('submit', val)),
|
||||
onSubmit: props.form.handleSubmit(() => emits('submit', undefined)),
|
||||
}
|
||||
: {
|
||||
onSubmit: (event: Event) => {
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
|
||||
import { computed, shallowRef } from 'vue';
|
||||
|
||||
import { mergeWithArrayOverride } from '@vben-core/shared/utils';
|
||||
|
||||
import { useForm } from '@tanstack/vue-form';
|
||||
|
||||
import { createRuntimeFieldComponent } from './form-runtime-field';
|
||||
@@ -218,7 +220,19 @@ export function useFormRuntime<TValues extends FormValues>(
|
||||
invalidateFieldValidation(fieldName);
|
||||
}
|
||||
manualErrors.value = new Map();
|
||||
rawForm.reset(resetState?.values as TValues | undefined, options);
|
||||
const partialValues = resetState?.values;
|
||||
let resetValues: TValues | undefined;
|
||||
if (partialValues) {
|
||||
resetValues = options?.force
|
||||
? (partialValues as TValues)
|
||||
: (mergeWithArrayOverride(
|
||||
partialValues,
|
||||
rawForm.options.defaultValues ?? defaultValues,
|
||||
) as TValues);
|
||||
}
|
||||
rawForm.reset(resetValues, {
|
||||
keepDefaultValues: options?.keepDefaultValues,
|
||||
});
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
@@ -244,13 +258,13 @@ export function useFormRuntime<TValues extends FormValues>(
|
||||
typeof fieldName
|
||||
>;
|
||||
},
|
||||
handleSubmit(callback) {
|
||||
return async (event) => {
|
||||
handleSubmit(callback?) {
|
||||
return async (event?: Event) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
const result = await validate();
|
||||
if (result.valid) {
|
||||
await callback(values.value);
|
||||
await callback?.(values.value as TValues);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
export { setupVbenForm } from './config';
|
||||
export { FormCodecError } from './form-codec';
|
||||
|
||||
export type { FormCodecPhase } from './form-codec';
|
||||
|
||||
export type {
|
||||
BaseFormComponentType,
|
||||
ExtendedFormApi,
|
||||
FormActions,
|
||||
FormCodec,
|
||||
FormContextApi,
|
||||
FormLayout,
|
||||
FormSchemaContext,
|
||||
FormValues,
|
||||
FormValueSnapshot,
|
||||
VbenFormActionSlotProps,
|
||||
VbenFormComponent,
|
||||
VbenFormDefaultSlotProps,
|
||||
|
||||
@@ -9,6 +9,16 @@ import type { FormApi } from './form-api';
|
||||
|
||||
export type FormValues = Record<string, any>;
|
||||
|
||||
export interface FormCodec<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> {
|
||||
/** 将提交值转换为表单组件值。 */
|
||||
decode: (values: Readonly<TSubmitValues>) => TFormValues;
|
||||
/** 将表单组件值转换为提交值。 */
|
||||
encode: (values: Readonly<TFormValues>) => TSubmitValues;
|
||||
}
|
||||
|
||||
export type FormFieldName<TValues extends FormValues = FormValues> =
|
||||
| Extract<keyof TValues, string>
|
||||
| (Record<never, never> & string);
|
||||
@@ -116,9 +126,12 @@ export interface FormValidationResult {
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export interface FormValueSnapshot<TValues extends FormValues = FormValues> {
|
||||
rawValues: Readonly<TValues>;
|
||||
values: TValues;
|
||||
export interface FormValueSnapshot<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> {
|
||||
rawValues: Readonly<TFormValues>;
|
||||
values: TSubmitValues;
|
||||
}
|
||||
|
||||
export interface FormResetState<TValues extends FormValues = FormValues> {
|
||||
@@ -141,7 +154,7 @@ export interface FormContextApi<TValues extends FormValues = FormValues> {
|
||||
fieldName: TFieldName,
|
||||
) => FormFieldValue<TValues, TFieldName>;
|
||||
handleSubmit: (
|
||||
callback: (values: TValues) => Promise<void> | void,
|
||||
callback?: (values: TValues) => Promise<void> | void,
|
||||
) => (event?: Event) => Promise<void>;
|
||||
isFieldValid: (fieldName: string) => boolean;
|
||||
readonly meta: FormMeta;
|
||||
@@ -205,8 +218,9 @@ export interface VbenFormActionSlotProps<
|
||||
TValues extends FormValues = FormValues,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> {
|
||||
formApi: ExtendedFormApi<TValues, T, P>;
|
||||
formApi: ExtendedFormApi<TValues, T, P, TSubmitValues>;
|
||||
values: TValues;
|
||||
}
|
||||
|
||||
@@ -214,7 +228,8 @@ export interface VbenFormDefaultSlotProps<
|
||||
TValues extends FormValues = FormValues,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
> extends VbenFormActionSlotProps<TValues, T, P> {
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> extends VbenFormActionSlotProps<TValues, T, P, TSubmitValues> {
|
||||
shapes: FormShape[];
|
||||
}
|
||||
|
||||
@@ -223,7 +238,8 @@ export interface VbenFormFieldSlotProps<
|
||||
TFieldName extends KnownFormFieldName<TValues> = KnownFormFieldName<TValues>,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
> extends VbenFormActionSlotProps<TValues, T, P> {
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> extends VbenFormActionSlotProps<TValues, T, P, TSubmitValues> {
|
||||
componentField: FormComponentField<TValues[TFieldName], TFieldName>;
|
||||
disabled: boolean;
|
||||
field: FormRuntimeField<TValues[TFieldName]>;
|
||||
@@ -236,12 +252,19 @@ type VbenFormFieldSlots<
|
||||
TValues extends FormValues,
|
||||
T extends BaseFormComponentType,
|
||||
P extends Record<string, any>,
|
||||
TSubmitValues extends FormValues,
|
||||
> =
|
||||
string extends Extract<keyof TValues, string>
|
||||
? Record<string, ((props: any) => any) | undefined>
|
||||
: {
|
||||
[TFieldName in KnownFormFieldName<TValues>]?: (
|
||||
props: VbenFormFieldSlotProps<TValues, TFieldName, T, P>,
|
||||
props: VbenFormFieldSlotProps<
|
||||
TValues,
|
||||
TFieldName,
|
||||
T,
|
||||
P,
|
||||
TSubmitValues
|
||||
>,
|
||||
) => any;
|
||||
};
|
||||
|
||||
@@ -249,21 +272,33 @@ export type VbenFormSlots<
|
||||
TValues extends FormValues = FormValues,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
> = VbenFormFieldSlots<TValues, T, P> & {
|
||||
default?: (props: VbenFormDefaultSlotProps<TValues, T, P>) => any;
|
||||
'expand-after'?: (props: VbenFormActionSlotProps<TValues, T, P>) => any;
|
||||
'expand-before'?: (props: VbenFormActionSlotProps<TValues, T, P>) => any;
|
||||
'reset-before'?: (props: VbenFormActionSlotProps<TValues, T, P>) => any;
|
||||
'submit-before'?: (props: VbenFormActionSlotProps<TValues, T, P>) => any;
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> = VbenFormFieldSlots<TValues, T, P, TSubmitValues> & {
|
||||
default?: (
|
||||
props: VbenFormDefaultSlotProps<TValues, T, P, TSubmitValues>,
|
||||
) => any;
|
||||
'expand-after'?: (
|
||||
props: VbenFormActionSlotProps<TValues, T, P, TSubmitValues>,
|
||||
) => any;
|
||||
'expand-before'?: (
|
||||
props: VbenFormActionSlotProps<TValues, T, P, TSubmitValues>,
|
||||
) => any;
|
||||
'reset-before'?: (
|
||||
props: VbenFormActionSlotProps<TValues, T, P, TSubmitValues>,
|
||||
) => any;
|
||||
'submit-before'?: (
|
||||
props: VbenFormActionSlotProps<TValues, T, P, TSubmitValues>,
|
||||
) => any;
|
||||
};
|
||||
|
||||
export type VbenFormComponent<
|
||||
TValues extends FormValues = FormValues,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> = new () => {
|
||||
$props: VbenFormProps<T, P, TValues>;
|
||||
$slots: VbenFormSlots<TValues, T, P>;
|
||||
$props: VbenFormProps<T, P, TValues, TSubmitValues>;
|
||||
$slots: VbenFormSlots<TValues, T, P, TSubmitValues>;
|
||||
};
|
||||
|
||||
export interface FormSchemaContext<TValues extends FormValues = FormValues> {
|
||||
@@ -512,6 +547,7 @@ type MappedComponentProps<P, TValues extends FormValues = FormValues> =
|
||||
* 把一个字段拆分写入到其他字段,例如 `startTime` / `endTime`
|
||||
* - 返回其他值:会将当前字段恢复/写回为该返回值
|
||||
* - `setValue` 回调签名为 `(key, nextValue) => void`
|
||||
* @deprecated Use the form-level `codec` instead.
|
||||
*/
|
||||
export type FormValueFormat<TValues extends FormValues = FormValues> = (
|
||||
value: any,
|
||||
@@ -548,6 +584,7 @@ interface FormSchemaBody<TValues extends FormValues = FormValues> extends Omit<
|
||||
* 获取表单值时格式化当前字段。
|
||||
* - 返回值不为 `undefined` 时,会回写到当前 fieldName
|
||||
* - 返回值为 `undefined` 时,可通过 setValue 写入一个或多个目标字段
|
||||
* @deprecated Use the form-level `codec` instead.
|
||||
*/
|
||||
valueFormat?: FormValueFormat<TValues>;
|
||||
}
|
||||
@@ -637,15 +674,19 @@ export interface VbenFormFieldArrayProps<
|
||||
showIndex?: boolean;
|
||||
}
|
||||
|
||||
export type HandleSubmitFn<TValues extends FormValues = FormValues> = (
|
||||
values: TValues,
|
||||
rawValues: Readonly<TValues>,
|
||||
export type HandleSubmitFn<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> = (
|
||||
values: TSubmitValues,
|
||||
rawValues: Readonly<TFormValues>,
|
||||
) => Promise<void> | void;
|
||||
|
||||
export type HandleResetFn<TValues extends FormValues = FormValues> = (
|
||||
values: TValues,
|
||||
export type HandleResetFn<TSubmitValues extends FormValues = FormValues> = (
|
||||
values: TSubmitValues,
|
||||
) => Promise<void> | void;
|
||||
|
||||
/** @deprecated Use the form-level `codec` instead. */
|
||||
export type FieldMappingTimeItem = [
|
||||
string,
|
||||
[string, string],
|
||||
@@ -657,8 +698,10 @@ export type FieldMappingTimeItem = [
|
||||
)?,
|
||||
];
|
||||
|
||||
/** @deprecated Use the form-level `codec` instead. */
|
||||
export type FieldMappingTime = FieldMappingTimeItem[];
|
||||
|
||||
/** @deprecated Use the form-level `codec` instead. */
|
||||
export type ArrayToStringFields = Array<
|
||||
| [string[], string?] // 嵌套数组格式,可选分隔符
|
||||
| string // 单个字段,使用默认分隔符
|
||||
@@ -682,6 +725,7 @@ export interface FormRenderProps<
|
||||
> {
|
||||
/**
|
||||
* 表单字段数组映射字符串配置 默认使用","
|
||||
* @deprecated Use the form-level `codec` instead.
|
||||
*/
|
||||
arrayToStringFields?: ArrayToStringFields;
|
||||
/**
|
||||
@@ -717,6 +761,7 @@ export interface FormRenderProps<
|
||||
componentMap: Record<BaseFormComponentType, Component>;
|
||||
/**
|
||||
* 表单字段映射到时间格式
|
||||
* @deprecated Use the form-level `codec` instead.
|
||||
*/
|
||||
fieldMappingTime?: FieldMappingTime;
|
||||
/**
|
||||
@@ -757,6 +802,7 @@ export interface VbenFormProps<
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
TValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> extends Omit<
|
||||
FormRenderProps<T, P, TValues>,
|
||||
'componentBindEventMap' | 'componentMap' | 'form'
|
||||
@@ -780,6 +826,7 @@ export interface VbenFormProps<
|
||||
actionWrapperClass?: ClassType;
|
||||
/**
|
||||
* 表单字段数组映射字符串配置 默认使用","
|
||||
* @deprecated Use the form-level `codec` instead.
|
||||
*/
|
||||
arrayToStringFields?: ArrayToStringFields;
|
||||
|
||||
@@ -787,8 +834,11 @@ export interface VbenFormProps<
|
||||
* submitOnChange改变时防抖时间 | 默认300ms
|
||||
*/
|
||||
changeDebouncedTime?: number;
|
||||
/** 表单组件值与提交值之间的双向编解码器。 */
|
||||
codec?: FormCodec<TValues, TSubmitValues>;
|
||||
/**
|
||||
* 表单字段映射
|
||||
* @deprecated Use the form-level `codec` instead.
|
||||
*/
|
||||
fieldMappingTime?: FieldMappingTime;
|
||||
/**
|
||||
@@ -798,18 +848,18 @@ export interface VbenFormProps<
|
||||
/**
|
||||
* 表单重置回调
|
||||
*/
|
||||
handleReset?: HandleResetFn<TValues>;
|
||||
handleReset?: HandleResetFn<NoInfer<TSubmitValues>>;
|
||||
/**
|
||||
* 表单提交回调
|
||||
*/
|
||||
handleSubmit?: HandleSubmitFn<TValues>;
|
||||
handleSubmit?: HandleSubmitFn<TValues, TSubmitValues>;
|
||||
/**
|
||||
* 表单值变化回调
|
||||
*/
|
||||
handleValuesChange?: (
|
||||
values: Readonly<TValues>,
|
||||
fieldsChanged: string[],
|
||||
getFormattedValues: () => TValues,
|
||||
getFormattedValues: () => TSubmitValues,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
@@ -851,9 +901,12 @@ export type ExtendedFormApi<
|
||||
TValues extends FormValues = FormValues,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
> = FormApi<TValues, T, P> & {
|
||||
useStore: <TResult = NoInfer<VbenFormProps<T, P, TValues>>>(
|
||||
selector?: (state: NoInfer<VbenFormProps<T, P, TValues>>) => TResult,
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> = FormApi<TValues, T, P, TSubmitValues> & {
|
||||
useStore: <TResult = NoInfer<VbenFormProps<T, P, TValues, TSubmitValues>>>(
|
||||
selector?: (
|
||||
state: NoInfer<VbenFormProps<T, P, TValues, TSubmitValues>>,
|
||||
) => TResult,
|
||||
) => Readonly<Ref<TResult>>;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,11 @@ type UseVbenFormReturn<
|
||||
TValues extends FormValues,
|
||||
T extends BaseFormComponentType,
|
||||
P extends Record<string, any>,
|
||||
> = readonly [VbenFormComponent<TValues, T, P>, ExtendedFormApi<TValues, T, P>];
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
> = readonly [
|
||||
VbenFormComponent<TValues, T, P, TSubmitValues>,
|
||||
ExtendedFormApi<TValues, T, P, TSubmitValues>,
|
||||
];
|
||||
|
||||
export function useVbenForm<
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
@@ -28,14 +32,17 @@ export function useVbenForm<
|
||||
TValues extends FormValues,
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
>(options: VbenFormProps<T, P, TValues>): UseVbenFormReturn<TValues, T, P>;
|
||||
TSubmitValues extends FormValues = TValues,
|
||||
>(
|
||||
options: VbenFormProps<T, P, TValues, TSubmitValues>,
|
||||
): UseVbenFormReturn<TValues, T, P, TSubmitValues>;
|
||||
|
||||
export function useVbenForm(
|
||||
options: VbenFormProps<any, any, any>,
|
||||
): UseVbenFormReturn<any, any, any> {
|
||||
options: VbenFormProps<any, any, any, any>,
|
||||
): UseVbenFormReturn<any, any, any, any> {
|
||||
const IS_REACTIVE = isReactive(options);
|
||||
const api = new FormApi<any, any, any>(options);
|
||||
const extendedApi = api as ExtendedFormApi<any, any, any>;
|
||||
const api = new FormApi<any, any, any, any>(options);
|
||||
const extendedApi = api as ExtendedFormApi<any, any, any, any>;
|
||||
extendedApi.useStore = (selector: any) => {
|
||||
return useSelector(api.store, selector);
|
||||
};
|
||||
@@ -65,5 +72,10 @@ export function useVbenForm(
|
||||
);
|
||||
}
|
||||
|
||||
return [Form, extendedApi] as unknown as UseVbenFormReturn<any, any, any>;
|
||||
return [Form, extendedApi] as unknown as UseVbenFormReturn<
|
||||
any,
|
||||
any,
|
||||
any,
|
||||
any
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
|
||||
// 通过 extends 会导致热更新卡死,所以重复写了一遍
|
||||
interface Props extends VbenFormProps {
|
||||
formApi?: ExtendedFormApi<any, any, any>;
|
||||
formApi?: ExtendedFormApi<any, any, any, any>;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
@@ -43,18 +43,20 @@ const close = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const handleViewAll = () => {
|
||||
function handleViewAll() {
|
||||
emit('viewAll');
|
||||
close();
|
||||
};
|
||||
}
|
||||
|
||||
const handleMakeAll = () => {
|
||||
function handleMakeAll() {
|
||||
emit('makeAll');
|
||||
};
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
function handleClear() {
|
||||
emit('clear');
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ toggle });
|
||||
</script>
|
||||
<template>
|
||||
<VbenPopover v-model:open="open" content-class="relative right-2 w-90 p-0">
|
||||
|
||||
@@ -8,6 +8,8 @@ import { $t } from '@vben/locales';
|
||||
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
import SelectItem from './select-item.vue';
|
||||
|
||||
interface Item {
|
||||
key: string;
|
||||
label: string;
|
||||
@@ -67,14 +69,13 @@ onUnmounted(() => {
|
||||
sortableInstance = null;
|
||||
});
|
||||
|
||||
function setPosition(key: string, event: Event) {
|
||||
const value = (event.target as HTMLSelectElement).value as
|
||||
| 'auto'
|
||||
| 'fixed'
|
||||
| 'header'
|
||||
| 'none'
|
||||
| 'user-dropdown';
|
||||
emit('updatePosition', key, value);
|
||||
function setPosition(key: string, value: string | undefined) {
|
||||
if (!value) return;
|
||||
emit(
|
||||
'updatePosition',
|
||||
key,
|
||||
value as 'auto' | 'fixed' | 'header' | 'none' | 'user-dropdown',
|
||||
);
|
||||
nextTick(() => {
|
||||
emit(
|
||||
'updateOrder',
|
||||
@@ -91,25 +92,21 @@ function setPosition(key: string, event: Event) {
|
||||
v-for="item in sortableList"
|
||||
:key="item.key"
|
||||
:data-key="item.key"
|
||||
class="bg-accent flex items-center gap-2 rounded-md px-2 py-1.5"
|
||||
class="bg-accent flex items-center rounded-md pl-2"
|
||||
>
|
||||
<GripVertical
|
||||
class="drag-handle size-4 shrink-0 cursor-grab text-muted-foreground active:cursor-grabbing"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate text-sm">{{ item.label }}</span>
|
||||
<select
|
||||
:value="item.position"
|
||||
class="bg-background h-7 w-28 shrink-0 rounded border px-1 text-xs"
|
||||
@change="(e) => setPosition(item.key, e)"
|
||||
<SelectItem
|
||||
:items="item.positionItems ?? positionItems"
|
||||
:model-value="item.position"
|
||||
class="min-w-0 flex-1"
|
||||
@update:model-value="
|
||||
(v: string | undefined) => setPosition(item.key, v)
|
||||
"
|
||||
>
|
||||
<option
|
||||
v-for="opt in item.positionItems ?? positionItems"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</SelectItem>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hiddenList.length > 0" class="pt-2">
|
||||
@@ -119,26 +116,22 @@ function setPosition(key: string, event: Event) {
|
||||
<div
|
||||
v-for="item in hiddenList"
|
||||
:key="item.key"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1"
|
||||
class="flex items-center rounded-md"
|
||||
>
|
||||
<span
|
||||
class="text-muted-foreground min-w-0 flex-1 truncate text-sm line-through decoration-dotted"
|
||||
<SelectItem
|
||||
:items="item.positionItems ?? positionItems"
|
||||
:model-value="item.position"
|
||||
class="min-w-0 flex-1"
|
||||
@update:model-value="
|
||||
(v: string | undefined) => setPosition(item.key, v)
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<select
|
||||
:value="item.position"
|
||||
class="bg-background h-7 w-28 shrink-0 rounded border px-1 text-xs"
|
||||
@change="(e) => setPosition(item.key, e)"
|
||||
>
|
||||
<option
|
||||
v-for="opt in item.positionItems ?? positionItems"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
<span
|
||||
class="text-muted-foreground truncate line-through decoration-dotted"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</option>
|
||||
</select>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</SelectItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,10 +94,10 @@ const positionMap: Record<string, string> = {
|
||||
languageToggle: 'widgetLanguageToggleButtonPosition',
|
||||
timezone: 'widgetTimezoneButtonPosition',
|
||||
fullscreen: 'widgetFullscreenButtonPosition',
|
||||
refresh: 'widgetRefreshButtonPosition',
|
||||
notification: 'widgetNotificationButtonPosition',
|
||||
lockScreenBtn: 'widgetLockScreenButtonPosition',
|
||||
logoutBtn: 'widgetLogoutButtonPosition',
|
||||
refresh: 'widgetRefreshButtonPosition',
|
||||
};
|
||||
|
||||
const labelMap: Record<string, string> = {
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
VbenIconButton,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
withDefaults(defineProps<{ showButton?: boolean }>(), { showButton: true });
|
||||
|
||||
const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');
|
||||
|
||||
const timezoneStore = useTimezoneStore();
|
||||
@@ -47,17 +49,20 @@ const [Modal, modalApi] = useVbenModal({
|
||||
},
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
function open() {
|
||||
modalApi.open();
|
||||
};
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VbenIconButton
|
||||
v-if="showButton"
|
||||
:tooltip="$t('ui.widgets.timezone.setTimezone')"
|
||||
class="hover:animate-[shrink_0.3s_ease-in-out]"
|
||||
@click="handleClick"
|
||||
@click="open"
|
||||
>
|
||||
<TimezoneIcon class="size-4 text-foreground" />
|
||||
</VbenIconButton>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { computed, ref, useTemplateRef, watch } from 'vue';
|
||||
import { SUPPORT_LANGUAGES } from '@vben/constants';
|
||||
import { useHoverToggle, useRefresh } from '@vben/hooks';
|
||||
import {
|
||||
createIconifyIcon,
|
||||
Languages,
|
||||
LockKeyhole,
|
||||
LogOut,
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
VbenIconButton,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
||||
import { useFullscreen, useMagicKeys, whenever } from '@vueuse/core';
|
||||
|
||||
import { GlobalSearch } from '../global-search';
|
||||
import { LockScreenModal } from '../lock-screen';
|
||||
@@ -101,9 +102,11 @@ const {
|
||||
globalLogoutShortcutKey,
|
||||
globalLockScreenShortcutKey,
|
||||
globalSearchShortcutKey,
|
||||
isDark,
|
||||
preferencesButtonPosition,
|
||||
} = usePreferences();
|
||||
const { refresh } = useRefresh();
|
||||
const { toggle: toggleFullscreen } = useFullscreen();
|
||||
const accessStore = useAccessStore();
|
||||
const [LockModal, lockModalApi] = useVbenModal({
|
||||
connectedComponent: LockScreenModal,
|
||||
@@ -118,6 +121,10 @@ const refTrigger = useTemplateRef('refTrigger');
|
||||
const refContent = useTemplateRef('refContent');
|
||||
const refPreferences = useTemplateRef('refPreferences');
|
||||
const refGlobalSearch = useTemplateRef('refGlobalSearch');
|
||||
const refTimezone = useTemplateRef('refTimezone');
|
||||
const refNotification = useTemplateRef('refNotification');
|
||||
|
||||
const TimezoneIcon = createIconifyIcon('fluent-mdl2:world-clock');
|
||||
const [openPopover, hoverWatcher] = useHoverToggle(
|
||||
[refTrigger, refContent],
|
||||
() => props.hoverDelay,
|
||||
@@ -247,6 +254,28 @@ function handleGlobalSearch() {
|
||||
refGlobalSearch.value?.open();
|
||||
}
|
||||
|
||||
// 主题切换
|
||||
function handleThemeToggleSelect(event?: Event) {
|
||||
event?.preventDefault();
|
||||
updatePreferences({ theme: { mode: isDark.value ? 'light' : 'dark' } });
|
||||
}
|
||||
|
||||
// 时区
|
||||
function handleTimezoneSelect() {
|
||||
refTimezone.value?.open();
|
||||
}
|
||||
|
||||
// 全屏切换
|
||||
function handleFullscreenSelect() {
|
||||
toggleFullscreen();
|
||||
}
|
||||
|
||||
// 通知
|
||||
function handleNotificationSelect(event?: Event) {
|
||||
event?.preventDefault();
|
||||
refNotification.value?.toggle();
|
||||
}
|
||||
|
||||
// 语言切换 - 阻止 Radix 默认关闭外层 dropdown,就地展开/收起 locale 列表
|
||||
const showLanguageList = ref(false);
|
||||
function handleLanguageToggleSelect(event?: Event) {
|
||||
@@ -322,6 +351,12 @@ if (preferences.shortcutKeys.enable) {
|
||||
:show-button="false"
|
||||
/>
|
||||
|
||||
<TimezoneButton
|
||||
v-if="showTimezoneInDropdown"
|
||||
ref="refTimezone"
|
||||
:show-button="false"
|
||||
/>
|
||||
|
||||
<DropdownMenu v-model:open="openPopover" :modal="false">
|
||||
<DropdownMenuTrigger ref="refTrigger" :disabled="props.trigger === 'hover'">
|
||||
<div class="mr-2 ml-1 cursor-pointer rounded-full p-1.5 hover:bg-accent">
|
||||
@@ -428,6 +463,7 @@ if (preferences.shortcutKeys.enable) {
|
||||
<DropdownMenuItem
|
||||
v-if="showThemeToggleInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@select="handleThemeToggleSelect"
|
||||
>
|
||||
<ThemeToggle class="mr-2" />
|
||||
{{ $t('preferences.theme.title') }}
|
||||
@@ -461,22 +497,27 @@ if (preferences.shortcutKeys.enable) {
|
||||
<DropdownMenuItem
|
||||
v-if="showTimezoneInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@select="handleTimezoneSelect"
|
||||
>
|
||||
<TimezoneButton class="mr-2" />
|
||||
<VbenIconButton class="mr-2" @click="handleTimezoneSelect">
|
||||
<TimezoneIcon class="size-4" />
|
||||
</VbenIconButton>
|
||||
{{ $t('ui.widgets.timezone.setTimezone') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showFullscreenInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@select="handleFullscreenSelect"
|
||||
>
|
||||
<VbenFullScreen class="mr-2" />
|
||||
<VbenFullScreen class="mr-2" @click.stop />
|
||||
{{ $t('preferences.widget.fullscreen') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="showNotificationInDropdown"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@select="handleNotificationSelect"
|
||||
>
|
||||
<Notification class="mr-2" />
|
||||
<Notification ref="refNotification" class="mr-2" />
|
||||
{{ $t('preferences.widget.notification') }}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { VxeGridInstance } from 'vxe-table';
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
ExtendedFormApi,
|
||||
FormValues,
|
||||
} from '@vben-core/form-ui';
|
||||
|
||||
import type { VxeGridProps } from './types';
|
||||
@@ -34,14 +35,16 @@ export class VxeGridApi<
|
||||
T extends Record<string, any> = any,
|
||||
D extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> {
|
||||
public formApi = {} as ExtendedFormApi;
|
||||
public formApi = {} as ExtendedFormApi<TFormValues, D, P, TSubmitValues>;
|
||||
|
||||
// private prevState: null | VxeGridProps = null;
|
||||
public grid = {} as VxeGridInstance<T>;
|
||||
public state: null | VxeGridProps<T, D, P> = null;
|
||||
public state: null | VxeGridProps<T, D, P, TFormValues, TSubmitValues> = null;
|
||||
|
||||
public store: Store<VxeGridProps<T, D, P>>;
|
||||
public store: Store<VxeGridProps<T, D, P, TFormValues, TSubmitValues>>;
|
||||
|
||||
/**
|
||||
* 已读行 helper(在 mount 中初始化,业务能力全部封装在 useViewedRow 中)
|
||||
@@ -52,12 +55,26 @@ export class VxeGridApi<
|
||||
|
||||
private stateHandler: StateHandler;
|
||||
|
||||
constructor(options: VxeGridProps<T, D, P> = {} as VxeGridProps<T, D, P>) {
|
||||
constructor(
|
||||
options: VxeGridProps<
|
||||
T,
|
||||
D,
|
||||
P,
|
||||
TFormValues,
|
||||
TSubmitValues
|
||||
> = {} as VxeGridProps<T, D, P, TFormValues, TSubmitValues>,
|
||||
) {
|
||||
const storeState = { ...options };
|
||||
|
||||
const defaultState = getDefaultState();
|
||||
this.store = new Store<VxeGridProps<T, D, P>>(
|
||||
mergeWithArrayOverride(storeState, defaultState) as VxeGridProps<T, D, P>,
|
||||
this.store = new Store<VxeGridProps<T, D, P, TFormValues, TSubmitValues>>(
|
||||
mergeWithArrayOverride(storeState, defaultState) as VxeGridProps<
|
||||
T,
|
||||
D,
|
||||
P,
|
||||
TFormValues,
|
||||
TSubmitValues
|
||||
>,
|
||||
);
|
||||
|
||||
this.store.subscribe((state) => {
|
||||
@@ -106,7 +123,10 @@ export class VxeGridApi<
|
||||
this.viewedRowHelper?.markAsViewed(record);
|
||||
}
|
||||
|
||||
mount(instance: null | VxeGridInstance, formApi: ExtendedFormApi) {
|
||||
mount(
|
||||
instance: null | VxeGridInstance,
|
||||
formApi: ExtendedFormApi<TFormValues, D, P, TSubmitValues>,
|
||||
) {
|
||||
if (!this.isMounted && instance) {
|
||||
this.grid = instance;
|
||||
this.formApi = formApi;
|
||||
@@ -138,7 +158,11 @@ export class VxeGridApi<
|
||||
this.viewedRowHelper?.removeKeys(keys);
|
||||
}
|
||||
|
||||
setGridOptions(options: Partial<VxeGridProps<T, D, P>['gridOptions']>) {
|
||||
setGridOptions(
|
||||
options: Partial<
|
||||
VxeGridProps<T, D, P, TFormValues, TSubmitValues>['gridOptions']
|
||||
>,
|
||||
) {
|
||||
this.setState({
|
||||
gridOptions: options,
|
||||
});
|
||||
@@ -154,8 +178,10 @@ export class VxeGridApi<
|
||||
|
||||
setState(
|
||||
stateOrFn:
|
||||
| ((prev: VxeGridProps<T, D, P>) => Partial<VxeGridProps<T, D, P>>)
|
||||
| Partial<VxeGridProps<T, D, P>>,
|
||||
| ((
|
||||
prev: VxeGridProps<T, D, P, TFormValues, TSubmitValues>,
|
||||
) => Partial<VxeGridProps<T, D, P, TFormValues, TSubmitValues>>)
|
||||
| Partial<VxeGridProps<T, D, P, TFormValues, TSubmitValues>>,
|
||||
) {
|
||||
if (isFunction(stateOrFn)) {
|
||||
this.store.setState((prev) => {
|
||||
|
||||
@@ -10,7 +10,11 @@ import type { Ref } from 'vue';
|
||||
|
||||
import type { ClassType, DeepPartial } from '@vben/types';
|
||||
|
||||
import type { BaseFormComponentType, VbenFormProps } from '@vben-core/form-ui';
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
FormValues,
|
||||
VbenFormProps,
|
||||
} from '@vben-core/form-ui';
|
||||
|
||||
import type { VxeGridApi } from './api';
|
||||
|
||||
@@ -124,6 +128,8 @@ export interface VxeGridProps<
|
||||
T extends Record<string, any> = any,
|
||||
D extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> {
|
||||
/**
|
||||
* 数据
|
||||
@@ -156,7 +162,7 @@ export interface VxeGridProps<
|
||||
/**
|
||||
* 表单配置
|
||||
*/
|
||||
formOptions?: VbenFormProps<D, P>;
|
||||
formOptions?: VbenFormProps<D, P, TFormValues, TSubmitValues>;
|
||||
/**
|
||||
* 显示搜索表单
|
||||
*/
|
||||
@@ -175,9 +181,13 @@ export type ExtendedVxeGridApi<
|
||||
D extends Record<string, any> = any,
|
||||
F extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
> = VxeGridApi<D, F, P> & {
|
||||
useStore: <S = NoInfer<VxeGridProps<D, F, P>>>(
|
||||
selector?: (state: NoInfer<VxeGridProps<D, F, P>>) => S,
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> = VxeGridApi<D, F, P, TFormValues, TSubmitValues> & {
|
||||
useStore: <S = NoInfer<VxeGridProps<D, F, P, TFormValues, TSubmitValues>>>(
|
||||
selector?: (
|
||||
state: NoInfer<VxeGridProps<D, F, P, TFormValues, TSubmitValues>>,
|
||||
) => S,
|
||||
) => Readonly<Ref<S>>;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { VxeGridSlots, VxeGridSlotTypes } from 'vxe-table';
|
||||
|
||||
import type { SlotsType } from 'vue';
|
||||
|
||||
import type { BaseFormComponentType } from '@vben-core/form-ui';
|
||||
import type { BaseFormComponentType, FormValues } from '@vben-core/form-ui';
|
||||
|
||||
import type { ExtendedVxeGridApi, VxeGridProps } from './types';
|
||||
|
||||
@@ -23,24 +23,28 @@ export function useVbenVxeGrid<
|
||||
T extends Record<string, any> = any,
|
||||
D extends BaseFormComponentType = BaseFormComponentType,
|
||||
P extends Record<string, any> = Record<never, never>,
|
||||
>(options: VxeGridProps<T, D, P>) {
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
>(options: VxeGridProps<T, D, P, TFormValues, TSubmitValues>) {
|
||||
// const IS_REACTIVE = isReactive(options);
|
||||
const api = new VxeGridApi<T, D, P>(options);
|
||||
const extendedApi: ExtendedVxeGridApi<T, D, P> = api as ExtendedVxeGridApi<
|
||||
T,
|
||||
D,
|
||||
P
|
||||
>;
|
||||
const api = new VxeGridApi<T, D, P, TFormValues, TSubmitValues>(options);
|
||||
const extendedApi: ExtendedVxeGridApi<T, D, P, TFormValues, TSubmitValues> =
|
||||
api as ExtendedVxeGridApi<T, D, P, TFormValues, TSubmitValues>;
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useStore(api.store, selector);
|
||||
};
|
||||
|
||||
const Grid = defineComponent(
|
||||
(props: VxeGridProps<T, D, P>, { attrs, slots }) => {
|
||||
(
|
||||
props: VxeGridProps<T, D, P, TFormValues, TSubmitValues>,
|
||||
{ attrs, slots },
|
||||
) => {
|
||||
onBeforeUnmount(() => {
|
||||
api.unmount();
|
||||
});
|
||||
api.setState({ ...props, ...attrs } as Partial<VxeGridProps<T, D, P>>);
|
||||
api.setState({ ...props, ...attrs } as Partial<
|
||||
VxeGridProps<T, D, P, TFormValues, TSubmitValues>
|
||||
>);
|
||||
return () =>
|
||||
h(
|
||||
VxeGrid,
|
||||
@@ -48,7 +52,7 @@ export function useVbenVxeGrid<
|
||||
...props,
|
||||
...attrs,
|
||||
api: extendedApi as ExtendedVxeGridApi,
|
||||
},
|
||||
} as any,
|
||||
slots,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -194,24 +194,24 @@
|
||||
"title": "小部件",
|
||||
"globalSearch": "全局搜索",
|
||||
"globalSearchPosition": "全局搜索位置",
|
||||
"fullscreen": "启用全屏",
|
||||
"fullscreen": "全屏",
|
||||
"fullscreenPosition": "全屏按钮位置",
|
||||
"themeToggle": "启用主题切换",
|
||||
"themeToggle": "主题切换",
|
||||
"themeTogglePosition": "主题切换位置",
|
||||
"languageToggle": "语言切换",
|
||||
"languageTogglePosition": "语言切换位置",
|
||||
"notification": "启用通知",
|
||||
"notification": "通知",
|
||||
"notificationPosition": "通知位置",
|
||||
"sidebarToggle": "启用侧边栏切换",
|
||||
"lockScreen": "启用锁屏",
|
||||
"lockScreen": "锁屏",
|
||||
"lockScreenPosition": "锁屏按钮位置",
|
||||
"logoutButtonPosition": "退出按钮位置",
|
||||
"header": "顶栏",
|
||||
"userDropdown": "用户下拉窗",
|
||||
"hidden": "已隐藏",
|
||||
"refresh": "启用刷新",
|
||||
"refresh": "刷新",
|
||||
"refreshPosition": "刷新按钮位置",
|
||||
"timezone": "启用时区",
|
||||
"timezone": "时区",
|
||||
"timezonePosition": "时区按钮位置"
|
||||
},
|
||||
"antd": {
|
||||
|
||||
Reference in New Issue
Block a user