summaryrefslogtreecommitdiff
path: root/packages/frontend/src/os.ts
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2026-01-09 12:21:08 +0900
committerGitHub <noreply@github.com>2026-01-09 12:21:08 +0900
commit2a14025c29081bd8080b4dec0823a7625a791950 (patch)
treee02f7fe6363457996efba020ec964bb6e4f041ed /packages/frontend/src/os.ts
parentBump version to 2026.1.0-alpha.3 (diff)
downloadmisskey-2a14025c29081bd8080b4dec0823a7625a791950.tar.gz
misskey-2a14025c29081bd8080b4dec0823a7625a791950.tar.bz2
misskey-2a14025c29081bd8080b4dec0823a7625a791950.zip
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]
Diffstat (limited to 'packages/frontend/src/os.ts')
-rw-r--r--packages/frontend/src/os.ts124
1 files changed, 54 insertions, 70 deletions
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 3fb204c2b2..73f18bc6b5 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -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(),
});