rewrite MkFormDialog
This commit is contained in:
parent
bb9a58ce34
commit
3e43819ba1
3 changed files with 228 additions and 107 deletions
|
@ -8,7 +8,7 @@
|
||||||
@click="cancel()"
|
@click="cancel()"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@close="cancel()"
|
@close="cancel()"
|
||||||
@closed="$emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
@ -17,86 +17,84 @@
|
||||||
<MkSpacer :margin-min="20" :margin-max="32">
|
<MkSpacer :margin-min="20" :margin-max="32">
|
||||||
<div class="_formRoot">
|
<div class="_formRoot">
|
||||||
<template
|
<template
|
||||||
v-for="item in Object.keys(form).filter(
|
v-for="[formItem, formItemName] in unHiddenForms()"
|
||||||
(item) => !form[item].hidden,
|
|
||||||
)"
|
|
||||||
>
|
>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-if="form[item].type === 'number'"
|
v-if="formItem.type === 'number'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
type="number"
|
type="number"
|
||||||
:step="form[item].step || 1"
|
:step="formItem.step || 1"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormInput
|
<FormInput
|
||||||
v-else-if="
|
v-else-if="
|
||||||
form[item].type === 'string' &&
|
formItem.type === 'string' &&
|
||||||
!form[item].multiline
|
!formItem.multiline
|
||||||
"
|
"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
type="text"
|
type="text"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormInput>
|
</FormInput>
|
||||||
<FormTextarea
|
<FormTextarea
|
||||||
v-else-if="
|
v-else-if="
|
||||||
form[item].type === 'string' && form[item].multiline
|
formItem.type === 'string' && formItem.multiline
|
||||||
"
|
"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
<FormSwitch
|
<FormSwitch
|
||||||
v-else-if="form[item].type === 'boolean'"
|
v-else-if="formItem.type === 'boolean'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<span v-text="form[item].label || item"></span>
|
<span v-text="formItem.label || formItemName"></span>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormSwitch>
|
</FormSwitch>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
v-else-if="form[item].type === 'enum'"
|
v-else-if="formItem.type === 'enum'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label>
|
||||||
><span v-text="form[item].label || item"></span
|
<span v-text="formItem.label || formItemName"></span>
|
||||||
><span v-if="form[item].required === false">
|
<span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
|
||||||
>
|
>
|
||||||
|
</template>
|
||||||
<option
|
<option
|
||||||
v-for="item in form[item].enum"
|
v-for="item in formItem.enum"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
>
|
>
|
||||||
|
@ -104,18 +102,18 @@
|
||||||
</option>
|
</option>
|
||||||
</FormSelect>
|
</FormSelect>
|
||||||
<FormRadios
|
<FormRadios
|
||||||
v-else-if="form[item].type === 'radio'"
|
v-else-if="formItem.type === 'radio'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="item in form[item].options"
|
v-for="item in formItem.options"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
:value="item.value"
|
:value="item.value"
|
||||||
>
|
>
|
||||||
|
@ -123,30 +121,30 @@
|
||||||
</option>
|
</option>
|
||||||
</FormRadios>
|
</FormRadios>
|
||||||
<FormRange
|
<FormRange
|
||||||
v-else-if="form[item].type === 'range'"
|
v-else-if="formItem.type === 'range'"
|
||||||
v-model="values[item]"
|
v-model="values[formItemName]"
|
||||||
:min="form[item].min"
|
:min="formItem.min"
|
||||||
:max="form[item].max"
|
:max="formItem.max"
|
||||||
:step="form[item].step"
|
:step="formItem.step"
|
||||||
:text-converter="form[item].textConverter"
|
:text-converter="formItem.textConverter"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
>
|
>
|
||||||
<template #label
|
<template #label
|
||||||
><span v-text="form[item].label || item"></span
|
><span v-text="formItem.label || formItemName"></span
|
||||||
><span v-if="form[item].required === false">
|
><span v-if="formItem.required === false">
|
||||||
({{ i18n.ts.optional }})</span
|
({{ i18n.ts.optional }})</span
|
||||||
></template
|
></template
|
||||||
>
|
>
|
||||||
<template v-if="form[item].description" #caption>{{
|
<template v-if="formItem.description" #caption>{{
|
||||||
form[item].description
|
formItem.description
|
||||||
}}</template>
|
}}</template>
|
||||||
</FormRange>
|
</FormRange>
|
||||||
<MkButton
|
<MkButton
|
||||||
v-else-if="form[item].type === 'button'"
|
v-else-if="formItem.type === 'button'"
|
||||||
class="_formBlock"
|
class="_formBlock"
|
||||||
@click="form[item].action($event, values)"
|
@click="formItem.action($event, values)"
|
||||||
>
|
>
|
||||||
<span v-text="form[item].content || item"></span>
|
<span v-text="formItem.content || formItemName"></span>
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,8 +152,8 @@
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from "vue";
|
import { ref } from "vue";
|
||||||
import FormInput from "./form/input.vue";
|
import FormInput from "./form/input.vue";
|
||||||
import FormTextarea from "./form/textarea.vue";
|
import FormTextarea from "./form/textarea.vue";
|
||||||
import FormSwitch from "./form/switch.vue";
|
import FormSwitch from "./form/switch.vue";
|
||||||
|
@ -165,59 +163,50 @@ import MkButton from "./MkButton.vue";
|
||||||
import FormRadios from "./form/radios.vue";
|
import FormRadios from "./form/radios.vue";
|
||||||
import XModalWindow from "@/components/MkModalWindow.vue";
|
import XModalWindow from "@/components/MkModalWindow.vue";
|
||||||
import { i18n } from "@/i18n";
|
import { i18n } from "@/i18n";
|
||||||
|
import type { FormItemType } from "@/types/form";
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
title: string;
|
||||||
XModalWindow,
|
form: Record<string, FormItemType>;
|
||||||
FormInput,
|
}>();
|
||||||
FormTextarea,
|
|
||||||
FormSwitch,
|
|
||||||
FormSelect,
|
|
||||||
FormRange,
|
|
||||||
MkButton,
|
|
||||||
FormRadios,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
// biome-ignore lint/suspicious/noExplicitAny: To prevent overly complex types we have to use any here
|
||||||
title: {
|
type ValueType = Record<string, any>;
|
||||||
type: String,
|
|
||||||
required: true,
|
const emit = defineEmits<{
|
||||||
|
done: [
|
||||||
|
status: {
|
||||||
|
result?: Record<string, FormItemType["default"]>;
|
||||||
|
canceled?: true;
|
||||||
},
|
},
|
||||||
form: {
|
];
|
||||||
type: Object,
|
closed: [];
|
||||||
required: true,
|
}>();
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ["done"],
|
const values = ref<ValueType>({});
|
||||||
|
const dialog = ref<InstanceType<typeof XModalWindow> | null>(null);
|
||||||
|
|
||||||
data() {
|
for (const item in props.form) {
|
||||||
return {
|
values.value[item] = props.form[item].default ?? null;
|
||||||
values: {},
|
}
|
||||||
i18n,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
function unHiddenForms(): [FormItemType, string][] {
|
||||||
for (const item in this.form) {
|
return Object.keys(props.form)
|
||||||
this.values[item] = this.form[item].default ?? null;
|
.filter((itemName) => !props.form[itemName].hidden)
|
||||||
}
|
.map((itemName) => [props.form[itemName], itemName]);
|
||||||
},
|
}
|
||||||
|
|
||||||
methods: {
|
function ok() {
|
||||||
ok() {
|
emit("done", {
|
||||||
this.$emit("done", {
|
result: values.value,
|
||||||
result: this.values,
|
});
|
||||||
});
|
dialog.value!.close();
|
||||||
this.$refs.dialog.close();
|
}
|
||||||
},
|
|
||||||
|
|
||||||
cancel() {
|
function cancel() {
|
||||||
this.$emit("done", {
|
emit("done", {
|
||||||
canceled: true,
|
canceled: true,
|
||||||
});
|
});
|
||||||
this.$refs.dialog.close();
|
dialog.value!.close();
|
||||||
},
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,6 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
|
||||||
import { apiUrl, url } from "@/config";
|
import { apiUrl, url } from "@/config";
|
||||||
import { me } from "@/me";
|
import { me } from "@/me";
|
||||||
import type { MenuItem } from "@/types/menu";
|
import type { MenuItem } from "@/types/menu";
|
||||||
|
import type { FormItemType, GetFormResultType } from "@/types/form";
|
||||||
|
|
||||||
export const pendingApiRequestsCount = ref(0);
|
export const pendingApiRequestsCount = ref(0);
|
||||||
|
|
||||||
|
@ -611,8 +612,16 @@ export function waiting(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function form(title, form) {
|
export function form<T extends Record<string, FormItemType>>(
|
||||||
return new Promise((resolve, reject) => {
|
title: string,
|
||||||
|
form: T,
|
||||||
|
) {
|
||||||
|
return new Promise<{
|
||||||
|
result?: {
|
||||||
|
[K in keyof T]: GetFormResultType<T[K]["type"]>;
|
||||||
|
};
|
||||||
|
canceled?: true;
|
||||||
|
}>((resolve, _reject) => {
|
||||||
popup(
|
popup(
|
||||||
defineAsyncComponent({
|
defineAsyncComponent({
|
||||||
loader: () => import("@/components/MkFormDialog.vue"),
|
loader: () => import("@/components/MkFormDialog.vue"),
|
||||||
|
@ -622,7 +631,7 @@ export function form(title, form) {
|
||||||
{ title, form },
|
{ title, form },
|
||||||
{
|
{
|
||||||
done: (result) => {
|
done: (result) => {
|
||||||
resolve(result);
|
resolve(result as never);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"closed",
|
"closed",
|
||||||
|
@ -631,7 +640,7 @@ export function form(title, form) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectUser() {
|
export async function selectUser() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<entities.UserDetailed>((resolve, reject) => {
|
||||||
popup(
|
popup(
|
||||||
defineAsyncComponent({
|
defineAsyncComponent({
|
||||||
loader: () => import("@/components/MkUserSelectDialog.vue"),
|
loader: () => import("@/components/MkUserSelectDialog.vue"),
|
||||||
|
|
123
packages/client/src/types/form.ts
Normal file
123
packages/client/src/types/form.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
export type BaseFormItem = {
|
||||||
|
hidden?: boolean;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormItemTextInput = BaseFormItem & {
|
||||||
|
type: "string";
|
||||||
|
default?: string | null;
|
||||||
|
multiline?: false;
|
||||||
|
};
|
||||||
|
export type FormItemTextarea = BaseFormItem & {
|
||||||
|
type: "string";
|
||||||
|
default?: string | null;
|
||||||
|
multiline: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormItemText = FormItemTextInput | FormItemTextarea;
|
||||||
|
|
||||||
|
export type FormItemNumber = BaseFormItem & {
|
||||||
|
type: "number";
|
||||||
|
default?: number | null;
|
||||||
|
step?: number | null;
|
||||||
|
};
|
||||||
|
export type FormItemEmail = BaseFormItem & {
|
||||||
|
type: "email";
|
||||||
|
default?: string | null;
|
||||||
|
};
|
||||||
|
export type FormItemPassword = BaseFormItem & {
|
||||||
|
type: "password";
|
||||||
|
default?: never;
|
||||||
|
__result_typedef?: string;
|
||||||
|
};
|
||||||
|
export type FormItemUrl = BaseFormItem & {
|
||||||
|
type: "url";
|
||||||
|
default?: string | null;
|
||||||
|
};
|
||||||
|
export type FormItemDate = BaseFormItem & {
|
||||||
|
type: "date";
|
||||||
|
default?: Date | null;
|
||||||
|
};
|
||||||
|
export type FormItemTime = BaseFormItem & {
|
||||||
|
type: "time";
|
||||||
|
default?: number | Date | null;
|
||||||
|
};
|
||||||
|
export type FormItemSearch = BaseFormItem & {
|
||||||
|
type: "search";
|
||||||
|
default?: string | null;
|
||||||
|
};
|
||||||
|
export type FormItemSwitch = BaseFormItem & {
|
||||||
|
type: "boolean";
|
||||||
|
default?: boolean | null;
|
||||||
|
};
|
||||||
|
export type FormItemSelect = BaseFormItem & {
|
||||||
|
type: "enum";
|
||||||
|
default?: string | null;
|
||||||
|
enum: {
|
||||||
|
value: string | number | symbol | undefined;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
export type FormItemRadios = BaseFormItem & {
|
||||||
|
type: "radio";
|
||||||
|
default?: string | number | symbol | undefined | null;
|
||||||
|
options: {
|
||||||
|
label: string;
|
||||||
|
value: string | number | symbol | undefined;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
export type FormItemRange = BaseFormItem & {
|
||||||
|
type: "range";
|
||||||
|
default?: number | null;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step?: number;
|
||||||
|
textConverter?: (value: number) => string;
|
||||||
|
};
|
||||||
|
export type FormItemButton = BaseFormItem & {
|
||||||
|
type: "button";
|
||||||
|
content?: string;
|
||||||
|
action: (event, values) => unknown;
|
||||||
|
default?: never;
|
||||||
|
};
|
||||||
|
export type FormItemObject = BaseFormItem & {
|
||||||
|
type: "object";
|
||||||
|
default: Record<string, unknown> | null;
|
||||||
|
hidden: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormItemInputArray = [
|
||||||
|
FormItemTextInput,
|
||||||
|
FormItemNumber,
|
||||||
|
FormItemEmail,
|
||||||
|
FormItemPassword,
|
||||||
|
FormItemUrl,
|
||||||
|
FormItemDate,
|
||||||
|
FormItemTime,
|
||||||
|
FormItemSearch,
|
||||||
|
];
|
||||||
|
|
||||||
|
export type FormItemTypeArray = [
|
||||||
|
...FormItemInputArray,
|
||||||
|
FormItemTextarea,
|
||||||
|
FormItemSwitch,
|
||||||
|
FormItemSelect,
|
||||||
|
FormItemButton,
|
||||||
|
FormItemRadios,
|
||||||
|
FormItemRange,
|
||||||
|
FormItemObject,
|
||||||
|
];
|
||||||
|
|
||||||
|
export type FormItemInput = FormItemInputArray[number];
|
||||||
|
|
||||||
|
export type FormItemType = FormItemTypeArray[number];
|
||||||
|
|
||||||
|
export type GetFormItemByType<T extends FormItemType["type"], F extends FormItemType = FormItemType> = F extends {type: T} ? F : never;
|
||||||
|
|
||||||
|
type NonUndefindAble<T> = T extends undefined ? never : T;
|
||||||
|
|
||||||
|
export type GetFormResultType<T extends FormItemType["type"], I extends FormItemType = GetFormItemByType<T>> = NonUndefindAble<
|
||||||
|
"__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"]
|
||||||
|
>
|
Loading…
Reference in a new issue