1
0
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:
かっこかり
2026-01-09 12:21:08 +09:00
committed by GitHub
parent 75b5dc1cd8
commit 2a14025c29
24 changed files with 196 additions and 167 deletions

View File

@@ -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(),
});