mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-24 02:14:04 +02:00
fix(frontend): popupのemit型が正しく利用できるように修正 (#16826)
* fix(frontend): popupのemit型が正しく利用できるように修正 * fix: revert unnecessary code (for testing purpose) * fix lint * fix type errors * fix types * add comment * fix * fix * fix: OverloadToUnionの仕組みを変更 * add comments, clean up * fix lint * fix types * clean up [ci skip] * fix * add comments [ci skip]
This commit is contained in:
@@ -8,13 +8,15 @@
|
||||
import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Component, Ref } from 'vue';
|
||||
import type { Component, MaybeRef } from 'vue';
|
||||
import type { ComponentEmit, ComponentProps as CP } from 'vue-component-type-helpers';
|
||||
import type { Form, GetFormResultType } from '@/utility/form.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
||||
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
||||
import type { MkDialogReturnType } from '@/components/MkDialog.vue';
|
||||
import type { OverloadToUnion } from '@/types/overload-to-union.js';
|
||||
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
|
||||
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
@@ -159,12 +161,34 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
|
||||
}
|
||||
|
||||
// props に ref を許可するようにする
|
||||
type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> };
|
||||
type PropsWithRefs<P> = { [K in keyof P]: MaybeRef<P[K]> };
|
||||
type ComponentProps<T extends Component> = PropsWithRefs<CP<T>>;
|
||||
|
||||
// 関数の引数が any[] (もっとも広義なもの) かどうかを判定し、any[] の場合は排除 (never) するヘルパー
|
||||
type FilterSpecificFunc<T> = T extends (...args: any[]) => void
|
||||
? (any[] extends Parameters<T> ? never : T)
|
||||
: T;
|
||||
|
||||
// オブジェクトの各プロパティに対して再帰的、あるいは単純に適用する型関数
|
||||
type CleanFunctions<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any
|
||||
? FilterSpecificFunc<T[K]>
|
||||
: T[K];
|
||||
};
|
||||
|
||||
// emitの関数群をオブジェクト型に変換する(InstanceType<Component>['$emit']はFunctionalComponent = ジェネリックコンポーネントでは使用できない)
|
||||
type ComponentEmitsObject<C extends Component, IE = OverloadToUnion<ComponentEmit<C>>> = CleanFunctions<{
|
||||
[K in IE extends (evName: infer U, ...args: any[]) => any ? U & PropertyKey : never]: IE extends (evName: K, ...args: infer A) => infer R
|
||||
? (...args: A) => R
|
||||
: (...args: any[]) => void;
|
||||
}>;
|
||||
|
||||
// NOTE: ジェネリック型つきのコンポーネントでは、emitsの型推論がうまく働かない(型変数を取り出すことはできないため)
|
||||
// NOTE: emitsがOverloadToUnionで対応しているオーバーロードの数を超える場合は、OverloadToUnionの個数を増やせばOK
|
||||
export function popup<T extends Component>(
|
||||
component: T,
|
||||
props: ComponentProps<T>,
|
||||
events: Partial<ComponentEmit<T>> = {},
|
||||
events: Partial<ComponentEmitsObject<T>> = {},
|
||||
): { dispose: () => void } {
|
||||
markRaw(component);
|
||||
|
||||
@@ -192,10 +216,10 @@ export function popup<T extends Component>(
|
||||
export async function popupAsyncWithDialog<T extends Component>(
|
||||
componentFetching: Promise<T>,
|
||||
props: ComponentProps<T>,
|
||||
events: Partial<ComponentEmit<T>> = {},
|
||||
events: Partial<ComponentEmitsObject<T>> = {},
|
||||
): Promise<{ dispose: () => void }> {
|
||||
let component: T;
|
||||
let closeWaiting = () => {};
|
||||
let closeWaiting = () => { };
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
closeWaiting = waiting();
|
||||
@@ -291,23 +315,19 @@ export function confirm(props: {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: const T extends ... にしたい
|
||||
// https://zenn.dev/general_link/articles/813e47b7a0eef7#const-type-parameters
|
||||
export function actions<T extends {
|
||||
type ActionsAction = {
|
||||
value: string;
|
||||
text: string;
|
||||
primary?: boolean,
|
||||
danger?: boolean,
|
||||
}[]>(props: {
|
||||
primary?: boolean;
|
||||
danger?: boolean;
|
||||
};
|
||||
|
||||
export function actions<const T extends ActionsAction[]>(props: {
|
||||
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
|
||||
title?: string;
|
||||
text?: string;
|
||||
actions: T;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: T[number]['value'];
|
||||
}> {
|
||||
}): Promise<MkDialogReturnType<T[number]['value']>> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(MkDialog, {
|
||||
...props,
|
||||
@@ -321,7 +341,7 @@ export function actions<T extends {
|
||||
})),
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
resolve(result as MkDialogReturnType<T[number]['value']>);
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
@@ -338,11 +358,7 @@ export function inputText(props: {
|
||||
default: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: string;
|
||||
}>;
|
||||
}): Promise<MkDialogReturnType<string>>;
|
||||
// min lengthが指定されてたら result は null になり得ないことを保証する overload function
|
||||
export function inputText(props: {
|
||||
type?: 'text' | 'email' | 'password' | 'url';
|
||||
@@ -353,11 +369,7 @@ export function inputText(props: {
|
||||
default?: string;
|
||||
minLength: number;
|
||||
maxLength?: number;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: string;
|
||||
}>;
|
||||
}): Promise<MkDialogReturnType<string>>;
|
||||
export function inputText(props: {
|
||||
type?: 'text' | 'email' | 'password' | 'url';
|
||||
title?: string;
|
||||
@@ -367,11 +379,7 @@ export function inputText(props: {
|
||||
default?: string | null;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: string | null;
|
||||
}>;
|
||||
}): Promise<MkDialogReturnType<string | null>>;
|
||||
export function inputText(props: {
|
||||
type?: 'text' | 'email' | 'password' | 'url';
|
||||
title?: string;
|
||||
@@ -381,11 +389,7 @@ export function inputText(props: {
|
||||
default?: string | null;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: string | null;
|
||||
}> {
|
||||
}): Promise<MkDialogReturnType<string | null>> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(MkDialog, {
|
||||
title: props.title,
|
||||
@@ -400,7 +404,7 @@ export function inputText(props: {
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
resolve(result as MkDialogReturnType<string | null>);
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
@@ -414,33 +418,21 @@ export function inputNumber(props: {
|
||||
placeholder?: string | null;
|
||||
autocomplete?: string;
|
||||
default: number;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: number;
|
||||
}>;
|
||||
}): Promise<MkDialogReturnType<number>>;
|
||||
export function inputNumber(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
placeholder?: string | null;
|
||||
autocomplete?: string;
|
||||
default?: number | null;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: number | null;
|
||||
}>;
|
||||
}): Promise<MkDialogReturnType<number | null>>;
|
||||
export function inputNumber(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
placeholder?: string | null;
|
||||
autocomplete?: string;
|
||||
default?: number | null;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: number | null;
|
||||
}> {
|
||||
}): Promise<MkDialogReturnType<number | null>> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(MkDialog, {
|
||||
title: props.title,
|
||||
@@ -453,7 +445,7 @@ export function inputNumber(props: {
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
resolve(result as MkDialogReturnType<number | null>);
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
@@ -465,11 +457,7 @@ export function inputDatetime(props: {
|
||||
text?: string;
|
||||
placeholder?: string | null;
|
||||
default?: string | null;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: Date;
|
||||
}> {
|
||||
}): Promise<MkDialogReturnType<Date>> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(MkDialog, {
|
||||
title: props.title,
|
||||
@@ -481,7 +469,7 @@ export function inputDatetime(props: {
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result != null && result.result != null ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
|
||||
resolve(result != null && typeof result.result === 'string' ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
@@ -508,11 +496,7 @@ export function select<C extends OptionValue, D extends C | null = null>(props:
|
||||
text?: string;
|
||||
default?: D;
|
||||
items: (MkSelectItem<C> | undefined)[];
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: Exclude<D, undefined> extends null ? C | null : C;
|
||||
}> {
|
||||
}): Promise<MkDialogReturnType<Exclude<D, undefined> extends null ? C | null : C>> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(MkDialog, {
|
||||
title: props.title,
|
||||
@@ -523,7 +507,7 @@ export function select<C extends OptionValue, D extends C | null = null>(props:
|
||||
},
|
||||
}, {
|
||||
done: result => {
|
||||
resolve(result ? result : { canceled: true });
|
||||
resolve(result as MkDialogReturnType<Exclude<D, undefined> extends null ? C | null : C>);
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
@@ -582,7 +566,7 @@ export function form<F extends Form>(title: string, f: F): Promise<{ canceled: t
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
|
||||
done: result => {
|
||||
resolve(result);
|
||||
resolve(result as { canceled?: false, result: GetFormResultType<F> });
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
@@ -634,16 +618,16 @@ export async function pickEmoji(anchorElement: HTMLElement, opts: ComponentProps
|
||||
});
|
||||
}
|
||||
|
||||
export async function cropImageFile(imageFile: File | Blob, options: {
|
||||
export async function cropImageFile<F extends File | Blob>(imageFile: F, options: {
|
||||
aspectRatio: number | null;
|
||||
}): Promise<File> {
|
||||
}): Promise<F> {
|
||||
return new Promise(resolve => {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
|
||||
imageFile: imageFile,
|
||||
aspectRatio: options.aspectRatio,
|
||||
}, {
|
||||
ok: x => {
|
||||
resolve(x);
|
||||
resolve(x as F);
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user