diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2026-01-09 12:21:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-09 12:21:08 +0900 |
| commit | 2a14025c29081bd8080b4dec0823a7625a791950 (patch) | |
| tree | e02f7fe6363457996efba020ec964bb6e4f041ed | |
| parent | Bump version to 2026.1.0-alpha.3 (diff) | |
| download | misskey-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]
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 */ |