summaryrefslogtreecommitdiff
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
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]
-rw-r--r--packages/backend/src/core/GlobalEventService.ts6
-rw-r--r--packages/backend/src/core/entities/EmojiEntityService.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/list.ts34
-rw-r--r--packages/frontend/src/components/MkAnnouncementDialog.vue6
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue14
-rw-r--r--packages/frontend/src/components/MkDialog.vue11
-rw-r--r--packages/frontend/src/components/MkDrive.folder.vue10
-rw-r--r--packages/frontend/src/components/MkDrive.vue10
-rw-r--r--packages/frontend/src/components/MkImgPreviewDialog.vue5
-rw-r--r--packages/frontend/src/components/MkNotificationSelectWindow.vue2
-rw-r--r--packages/frontend/src/components/MkUpdated.vue6
-rw-r--r--packages/frontend/src/components/MkYouTubePlayer.vue6
-rw-r--r--packages/frontend/src/os.ts124
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue15
-rw-r--r--packages/frontend/src/pages/drive.file.info.vue5
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue24
-rw-r--r--packages/frontend/src/pages/settings/drive.vue5
-rw-r--r--packages/frontend/src/types/overload-to-union.ts31
-rw-r--r--packages/frontend/src/utility/autocomplete.ts3
-rw-r--r--packages/frontend/src/utility/drive.ts16
-rw-r--r--packages/frontend/src/utility/get-drive-file-menu.ts5
-rw-r--r--packages/frontend/src/widgets/WidgetSlideshow.vue6
-rw-r--r--packages/frontend/src/widgets/widget.ts4
-rw-r--r--packages/misskey-js/src/autogen/types.ts11
24 files changed, 196 insertions, 167 deletions
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index f4c747b139..da5982abf6 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -38,11 +38,7 @@ export interface BroadcastTypes {
emojis: Packed<'EmojiDetailed'>[];
};
emojiDeleted: {
- emojis: {
- id?: string;
- name: string;
- [other: string]: any;
- }[];
+ emojis: Packed<'EmojiDetailed'>[];
};
announcementCreated: {
announcement: Packed<'Announcement'>;
diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts
index 490d3f2511..309de3b08f 100644
--- a/packages/backend/src/core/entities/EmojiEntityService.ts
+++ b/packages/backend/src/core/entities/EmojiEntityService.ts
@@ -41,7 +41,7 @@ export class EmojiEntityService {
@bindThis
public packSimpleMany(
- emojis: any[],
+ emojis: (MiEmoji['id'] | MiEmoji)[],
) {
return Promise.all(emojis.map(x => this.packSimple(x)));
}
@@ -69,7 +69,7 @@ export class EmojiEntityService {
@bindThis
public packDetailedMany(
- emojis: any[],
+ emojis: (MiEmoji['id'] | MiEmoji)[],
): Promise<Packed<'EmojiDetailed'>[]> {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 34d200455e..658367409c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
- optional: false, nullable: false,
- properties: {
- id: {
- type: 'string',
- optional: false, nullable: false,
- format: 'id',
- },
- aliases: {
- type: 'array',
- optional: false, nullable: false,
- items: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
- name: {
- type: 'string',
- optional: false, nullable: false,
- },
- category: {
- type: 'string',
- optional: false, nullable: true,
- },
- host: {
- type: 'string',
- optional: false, nullable: true,
- description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
- },
- url: {
- type: 'string',
- optional: false, nullable: false,
- },
- },
+ ref: 'EmojiDetailed',
},
},
} as const;
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 81c92bfb5c..da0f618e95 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="$emit('closed')" @click="onBgClick">
+<MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="emit('closed')" @click="onBgClick">
<div ref="rootEl" :class="$style.root">
<div :class="$style.header">
<span :class="$style.icon">
@@ -44,6 +44,10 @@ const props = defineProps<{
announcement: Misskey.entities.Announcement;
}>();
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
const rootEl = useTemplateRef('rootEl');
const bottomEl = useTemplateRef('bottomEl');
const modal = useTemplateRef('modal');
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 6c07eac47a..1fad936d16 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkModalWindow>
</template>
-<script lang="ts" setup>
+<script lang="ts" setup generic="F extends File | Blob">
import { onMounted, useTemplateRef, ref, onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs';
@@ -38,13 +38,13 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
- imageFile: File | Blob;
+ imageFile: F;
aspectRatio: number | null;
uploadFolder?: string | null;
}>();
const emit = defineEmits<{
- (ev: 'ok', cropped: File | Blob): void;
+ (ev: 'ok', cropped: F): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
@@ -74,8 +74,14 @@ async function ok() {
});
const f = await promise;
+ let finalFile: F;
+ if (props.imageFile instanceof File) {
+ finalFile = new File([f], props.imageFile.name, { type: f.type }) as F;
+ } else {
+ finalFile = f as F;
+ }
- emit('ok', f);
+ emit('ok', finalFile);
if (dialogEl.value != null) dialogEl.value.close();
}
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 705301a6a6..bea0392d2d 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -41,6 +41,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkModal>
</template>
+<script lang="ts">
+export type Result = string | number | true | null;
+export type MkDialogReturnType<T = Result> = { canceled: true, result: undefined } | { canceled: false, result: T };
+</script>
+
<script lang="ts" setup>
import { ref, useTemplateRef, computed } from 'vue';
import MkModal from '@/components/MkModal.vue';
@@ -65,8 +70,6 @@ type Select = {
default: OptionValue | null;
};
-type Result = string | number | true | null;
-
const props = withDefaults(defineProps<{
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
title?: string;
@@ -93,7 +96,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
+ (ev: 'done', v: MkDialogReturnType): void;
(ev: 'closed'): void;
}>();
@@ -131,7 +134,7 @@ function done(canceled: true): void;
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare
function done(canceled: boolean, result?: Result): void { // eslint-disable-line no-redeclare
- emit('done', { canceled, result } as { canceled: true } | { canceled: false, result: Result });
+ emit('done', { canceled, result } as MkDialogReturnType);
modal.value?.close();
}
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index d7dd12408c..8b2609852c 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -231,17 +231,17 @@ function rename() {
}
function move() {
- selectDriveFolder(null).then(folder => {
- if (folder[0] && folder[0].id === props.folder.id) return;
+ selectDriveFolder(null).then(({ canceled, folders }) => {
+ if (canceled || (folders[0] && folders[0].id === props.folder.id)) return;
misskeyApi('drive/folders/update', {
folderId: props.folder.id,
- parentId: folder[0] ? folder[0].id : null,
+ parentId: folders[0] ? folders[0].id : null,
}).then(() => {
globalEvents.emit('driveFoldersUpdated', [{
...props.folder,
- parentId: folder[0] ? folder[0].id : null,
- parent: folder[0] ?? null,
+ parentId: folders[0] ? folders[0].id : null,
+ parent: folders[0] ?? null,
}]);
});
});
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 71ffb252df..6e286f4882 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -577,17 +577,19 @@ function cd(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder
async function moveFilesBulk() {
if (selectedFiles.value.length === 0) return;
- const toFolder = await selectDriveFolder(folder.value ? folder.value.id : null);
+ const { canceled, folders } = await selectDriveFolder(folder.value ? folder.value.id : null);
+
+ if (canceled) return;
await os.apiWithDialog('drive/files/move-bulk', {
fileIds: selectedFiles.value.map(f => f.id),
- folderId: toFolder[0] ? toFolder[0].id : null,
+ folderId: folders[0] ? folders[0].id : null,
});
globalEvents.emit('driveFilesUpdated', selectedFiles.value.map(x => ({
...x,
- folderId: toFolder[0] ? toFolder[0].id : null,
- folder: toFolder[0] ?? null,
+ folderId: folders[0] ? folders[0].id : null,
+ folder: folders[0] ?? null,
})));
}
diff --git a/packages/frontend/src/components/MkImgPreviewDialog.vue b/packages/frontend/src/components/MkImgPreviewDialog.vue
index e17a1651cf..32b36727b0 100644
--- a/packages/frontend/src/components/MkImgPreviewDialog.vue
+++ b/packages/frontend/src/components/MkImgPreviewDialog.vue
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@close="close"
@esc="close"
@click="close"
+ @closed="emit('closed')"
>
<template #header>{{ file.name }}</template>
<div :class="$style.container">
@@ -27,6 +28,10 @@ defineProps<{
file: Misskey.entities.DriveFile;
}>();
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
const modal = ref<typeof MkModalWindow | null>(null);
function close() {
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 7205e516d2..5300abd0cf 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -42,7 +42,7 @@ import { i18n } from '@/i18n.js';
type TypesMap = Record<typeof notificationTypes[number], Ref<boolean>>;
const emit = defineEmits<{
- (ev: 'done', v: { excludeTypes: string[] }): void,
+ (ev: 'done', v: { excludeTypes: typeof notificationTypes[number][] }): void,
(ev: 'closed'): void,
}>();
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index eba8e5472c..09cf595eab 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModal ref="modal" preferType="dialog" :zPriority="'middle'" @click="modal?.close()" @closed="$emit('closed')">
+<MkModal ref="modal" preferType="dialog" :zPriority="'middle'" @click="modal?.close()" @closed="emit('closed')">
<div :class="$style.root">
<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
<div :class="$style.version">✨{{ version }}🚀</div>
@@ -26,6 +26,10 @@ import { confetti } from '@/utility/confetti.js';
const modal = useTemplateRef('modal');
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
const isBeta = version.includes('-beta') || version.includes('-alpha') || version.includes('-rc');
function whatIsNew() {
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 2375bcc9eb..20c4475779 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true">
+<MkWindow :initialWidth="640" :initialHeight="402" :canResize="true" :closeButton="true" @closed="emit('closed')">
<template #header>
<i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i>
<span>{{ title ?? 'YouTube' }}</span>
@@ -34,6 +34,10 @@ const props = defineProps<{
url: string;
}>();
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
const requestUrl = new URL(props.url);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
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(),
});
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 0f306896c9..9bc8992155 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -71,7 +71,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, defineAsyncComponent, markRaw, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { computed, markRaw, ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -116,7 +117,7 @@ const selectAll = () => {
}
};
-const toggleSelect = (emoji) => {
+const toggleSelect = (emoji: Misskey.entities.EmojiDetailed) => {
if (selectedEmojis.value.includes(emoji.id)) {
selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
} else {
@@ -124,19 +125,23 @@ const toggleSelect = (emoji) => {
}
};
-const add = async (ev: MouseEvent) => {
+const add = async () => {
const { dispose } = await os.popupAsyncWithDialog(import('./emoji-edit-dialog.vue').then(x => x.default), {
}, {
done: result => {
if (result.created) {
- paginator.prepend(result.created);
+ const nowIso = (new Date()).toISOString();
+ paginator.prepend({
+ ...result.created,
+ createdAt: nowIso,
+ });
}
},
closed: () => dispose(),
});
};
-const edit = async (emoji) => {
+const edit = async (emoji: Misskey.entities.EmojiDetailed) => {
const { dispose } = await os.popupAsyncWithDialog(import('./emoji-edit-dialog.vue').then(x => x.default), {
emoji: emoji,
}, {
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index e3cc1d988e..6b57684188 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -131,10 +131,11 @@ function move() {
const f = file.value;
- selectDriveFolder(null).then(folder => {
+ selectDriveFolder(null).then(({ canceled, folders }) => {
+ if (canceled) return;
misskeyApi('drive/files/update', {
fileId: f.id,
- folderId: folder[0] ? folder[0].id : null,
+ folderId: folders[0] ? folders[0].id : null,
}).then(async () => {
await _fetch_();
});
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index ea4863950d..4b6c5e1c51 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
</div>
</MkFolder>
- <MkSwitch v-model="isSensitive">isSensitive</MkSwitch>
+ <MkSwitch v-model="isSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
<MkButton v-if="emoji" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
@@ -99,7 +99,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
- (ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
+ (ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.EmojiDetailed; created?: Misskey.entities.EmojiDetailed }): void,
(ev: 'closed'): void
}>();
@@ -157,19 +157,29 @@ async function done() {
localOnly: localOnly.value,
roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id),
fileId: file.value ? file.value.id : undefined,
- };
+ } satisfies Misskey.entities.AdminEmojiUpdateRequest;
if (props.emoji) {
+ const emojiDetailed = {
+ id: props.emoji.id,
+ aliases: params.aliases,
+ name: params.name,
+ category: params.category,
+ host: props.emoji.host,
+ url: file.value ? file.value.url : props.emoji.url,
+ license: params.license,
+ isSensitive: params.isSensitive,
+ localOnly: params.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: params.roleIdsThatCanBeUsedThisEmojiAsReaction,
+ } satisfies Misskey.entities.EmojiDetailed;
+
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
...params,
});
emit('done', {
- updated: {
- id: props.emoji.id,
- ...params,
- },
+ updated: emojiDetailed,
});
windowEl.value?.close();
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 8d443921a9..b170d17a5a 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -296,8 +296,9 @@ if (prefer.s.uploadFolder) {
}
function chooseUploadFolder() {
- selectDriveFolder(null).then(async folder => {
- prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
+ selectDriveFolder(null).then(async ({ canceled, folders }) => {
+ if (canceled) return;
+ prefer.commit('uploadFolder', folders[0] ? folders[0].id : null);
os.success();
if (prefer.s.uploadFolder) {
uploadFolder.value = await misskeyApi('drive/folders/show', {
diff --git a/packages/frontend/src/types/overload-to-union.ts b/packages/frontend/src/types/overload-to-union.ts
new file mode 100644
index 0000000000..3cf16a5f3c
--- /dev/null
+++ b/packages/frontend/src/types/overload-to-union.ts
@@ -0,0 +1,31 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+type FlattenAndDedup<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : never;
+
+// 10個で足りなかった場合は増やす
+export type OverloadToUnion<T> = FlattenAndDedup<T extends {
+ (...args: infer A1): infer R1;
+ (...args: infer A2): infer R2;
+ (...args: infer A3): infer R3;
+ (...args: infer A4): infer R4;
+ (...args: infer A5): infer R5;
+ (...args: infer A6): infer R6;
+ (...args: infer A7): infer R7;
+ (...args: infer A8): infer R8;
+ (...args: infer A9): infer R9;
+ (...args: infer A10): infer R10;
+} ? (
+ ((...args: A1) => R1) |
+ ((...args: A2) => R2) |
+ ((...args: A3) => R3) |
+ ((...args: A4) => R4) |
+ ((...args: A5) => R5) |
+ ((...args: A6) => R6) |
+ ((...args: A7) => R7) |
+ ((...args: A8) => R8) |
+ ((...args: A9) => R9) |
+ ((...args: A10) => R10)
+) : never>;
diff --git a/packages/frontend/src/utility/autocomplete.ts b/packages/frontend/src/utility/autocomplete.ts
index 82109af1a0..c0b1865617 100644
--- a/packages/frontend/src/utility/autocomplete.ts
+++ b/packages/frontend/src/utility/autocomplete.ts
@@ -213,10 +213,11 @@ export class Autocomplete {
const _y = ref(y);
const _q = ref(q);
- const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
+ const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
textarea: this.textarea,
close: this.close,
type: type,
+ //@ts-expect-error popupは今のところジェネリック型のコンポーネントに対応していない
q: _q,
x: _x,
y: _y,
diff --git a/packages/frontend/src/utility/drive.ts b/packages/frontend/src/utility/drive.ts
index 4fe2042e78..7578ed36e6 100644
--- a/packages/frontend/src/utility/drive.ts
+++ b/packages/frontend/src/utility/drive.ts
@@ -301,14 +301,26 @@ export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFi
});
}
-export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<(Misskey.entities.DriveFolder | null)[]> {
+export async function selectDriveFolder(initialFolder: Misskey.entities.DriveFolder['id'] | null): Promise<{
+ canceled: false;
+ folders: (Misskey.entities.DriveFolder | null)[];
+} | {
+ canceled: true;
+ folders: undefined;
+}> {
return new Promise((resolve, reject) => {
let dispose: () => void;
os.popupAsyncWithDialog(import('@/components/MkDriveFolderSelectDialog.vue').then(x => x.default), {
initialFolder,
}, {
done: folders => {
- resolve(folders);
+ resolve(folders == null ? {
+ canceled: true,
+ folders: undefined,
+ } : {
+ canceled: false,
+ folders,
+ });
},
closed: () => dispose(),
}).then(d => dispose = d.dispose, reject);
diff --git a/packages/frontend/src/utility/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts
index 53ca4389bf..fea7f8f1d3 100644
--- a/packages/frontend/src/utility/get-drive-file-menu.ts
+++ b/packages/frontend/src/utility/get-drive-file-menu.ts
@@ -44,10 +44,11 @@ async function describe(file: Misskey.entities.DriveFile) {
}
function move(file: Misskey.entities.DriveFile) {
- selectDriveFolder(null).then(folder => {
+ selectDriveFolder(null).then(({ canceled, folders }) => {
+ if (canceled) return;
misskeyApi('drive/files/update', {
fileId: file.id,
- folderId: folder[0] ? folder[0].id : null,
+ folderId: folders[0] ? folders[0].id : null,
});
});
}
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 13ca278c23..b812c89e08 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -96,11 +96,11 @@ const fetch = () => {
};
const choose = () => {
- selectDriveFolder(null).then(folder => {
- if (folder[0] == null) {
+ selectDriveFolder(null).then(({ folders, canceled }) => {
+ if (canceled || folders[0] == null) {
return;
}
- widgetProps.folderId = folder[0].id;
+ widgetProps.folderId = folders[0].id;
save();
fetch();
});
diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts
index 90adea538f..603cf8a31d 100644
--- a/packages/frontend/src/widgets/widget.ts
+++ b/packages/frontend/src/widgets/widget.ts
@@ -80,8 +80,8 @@ export const useWidgetPropsManager = <F extends FormWithDefault>(
form: form,
currentSettings: widgetProps,
}, {
- saved: (newProps: GetFormResultType<F>) => {
- resolve({ canceled: false, result: newProps });
+ saved: (newProps) => {
+ resolve({ canceled: false, result: newProps as GetFormResultType<F> });
},
canceled: () => {
resolve({ canceled: true });
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index f3648e5112..87dc70a4db 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -8226,16 +8226,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- 'application/json': {
- /** Format: id */
- id: string;
- aliases: string[];
- name: string;
- category: string | null;
- /** @description The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files. */
- host: string | null;
- url: string;
- }[];
+ 'application/json': components['schemas']['EmojiDetailed'][];
};
};
/** @description Client error */