summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorzyoshoka <107108195+zyoshoka@users.noreply.github.com>2024-02-28 18:26:38 +0900
committerGitHub <noreply@github.com>2024-02-28 18:26:38 +0900
commit29350c9f334f426567e71eed479ae60ab4dea690 (patch)
tree1e7571a3ebeff1046fdf0984c04fc0cc373a9956 /packages
parentfix(backend): リノート時のHTLへのストリーミングの意図しな... (diff)
downloadmisskey-29350c9f334f426567e71eed479ae60ab4dea690.tar.gz
misskey-29350c9f334f426567e71eed479ae60ab4dea690.tar.bz2
misskey-29350c9f334f426567e71eed479ae60ab4dea690.zip
refactor(frontend): `os.ts`周りのリファクタリング (#13186)
* refactor(frontend): `os.ts`周りのリファクタリング * refactor: apiWithDialogのdataの型付け * refactor: 不要なas anyを除去 * refactor: 返り値の型を明記、`selectDriveFolder`は`File`のほうに合わせるよう返り値を変更 * refactor: 返り値の型を改善 * refactor: フォームの型を改善 * refactor: 良い感じのimportに修正 * refactor: フォームの返り値の型を改善 * refactor: `popup()`の`props`に`ref`な値を入れるのを許可するように * fix: `os.input`系と`os.select`の返り値の型がおかしい問題とそれによるバグを修正 * Update CHANGELOG.md * Update CHANGELOG.md --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/src/account.ts2
-rw-r--r--packages/frontend/src/components/MkDialog.vue29
-rw-r--r--packages/frontend/src/components/MkDriveSelectDialog.vue8
-rw-r--r--packages/frontend/src/components/MkEmojiPickerDialog.vue4
-rw-r--r--packages/frontend/src/components/MkEmojiPickerWindow.vue49
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue56
-rw-r--r--packages/frontend/src/os.ts302
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue2
-rw-r--r--packages/frontend/src/pages/notifications.vue4
-rw-r--r--packages/frontend/src/pages/settings/drive.vue2
-rw-r--r--packages/frontend/src/pages/settings/emoji-picker.vue2
-rw-r--r--packages/frontend/src/pages/settings/preferences-backups.vue2
-rw-r--r--packages/frontend/src/scripts/form.ts17
-rw-r--r--packages/frontend/src/ui/deck.vue20
-rw-r--r--packages/frontend/src/widgets/WidgetSlideshow.vue4
15 files changed, 254 insertions, 249 deletions
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index e606fe368c..7f20e0b1a2 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -290,7 +290,7 @@ export async function openAccountMenu(opts: {
text: i18n.ts.profile,
to: `/@${ $i.username }`,
avatar: $i,
- }, { type: 'divider' }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
+ }, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
type: 'parent' as const,
icon: 'ti ti-plus',
text: i18n.ts.addAccount,
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 4b7584faaa..4577d37c08 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -38,11 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
</template>
- <template v-else>
- <optgroup v-for="groupedItem in select.groupedItems" :label="groupedItem.label">
- <option v-for="item in groupedItem.items" :value="item.value">{{ item.text }}</option>
- </optgroup>
- </template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
@@ -64,7 +59,7 @@ import MkSelect from '@/components/MkSelect.vue';
import { i18n } from '@/i18n.js';
type Input = {
- type: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
+ type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
placeholder?: string | null;
autocomplete?: string;
default: string | number | null;
@@ -74,22 +69,17 @@ type Input = {
type Select = {
items: {
- value: string;
+ value: any;
text: string;
}[];
- groupedItems: {
- label: string;
- items: {
- value: string;
- text: string;
- }[];
- }[];
default: string | null;
};
+type Result = string | number | true | null;
+
const props = withDefaults(defineProps<{
type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
- title: string;
+ title?: string;
text?: string;
input?: Input;
select?: Select;
@@ -113,7 +103,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', v: { canceled: boolean; result: any }): void;
+ (ev: 'done', v: { canceled: true } | { canceled: false, result: Result }): void;
(ev: 'closed'): void;
}>();
@@ -139,8 +129,11 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
return null;
});
-function done(canceled: boolean, result?) {
- emit('done', { canceled, result });
+// overload function を使いたいので lint エラーを無視する
+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 });
modal.value?.close();
}
diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue
index 77b5532f79..f1ecc27123 100644
--- a/packages/frontend/src/components/MkDriveSelectDialog.vue
+++ b/packages/frontend/src/components/MkDriveSelectDialog.vue
@@ -39,13 +39,13 @@ withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', r?: Misskey.entities.DriveFile[]): void;
+ (ev: 'done', r?: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
(ev: 'closed'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
-const selected = ref<Misskey.entities.DriveFile[]>([]);
+const selected = ref<Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]>([]);
function ok() {
emit('done', selected.value);
@@ -57,7 +57,7 @@ function cancel() {
dialog.value?.close();
}
-function onChangeSelection(files: Misskey.entities.DriveFile[]) {
- selected.value = files;
+function onChangeSelection(v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]) {
+ selected.value = v;
}
</script>
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 59f4b51522..adcea839ee 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', v: any): void;
+ (ev: 'done', v: string): void;
(ev: 'close'): void;
(ev: 'closed'): void;
}>();
@@ -64,7 +64,7 @@ const emit = defineEmits<{
const modal = shallowRef<InstanceType<typeof MkModal>>();
const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
-function chosen(emoji: any) {
+function chosen(emoji: string) {
emit('done', emoji);
if (props.choseAndClose) {
modal.value?.close();
diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue
deleted file mode 100644
index 6952943345..0000000000
--- a/packages/frontend/src/components/MkEmojiPickerWindow.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<MkWindow
- ref="window"
- :initialWidth="300"
- :initialHeight="290"
- :canResize="true"
- :mini="true"
- :front="true"
- @closed="emit('closed')"
->
- <MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
-</MkWindow>
-</template>
-
-<script lang="ts" setup>
-import { } from 'vue';
-import * as Misskey from 'misskey-js';
-import MkWindow from '@/components/MkWindow.vue';
-import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
-
-withDefaults(defineProps<{
- src?: HTMLElement;
- showPinned?: boolean;
- asReactionPicker?: boolean;
- targetNote?: Misskey.entities.Note
-}>(), {
- showPinned: true,
-});
-
-const emit = defineEmits<{
- (ev: 'chosen', v: any): void;
- (ev: 'closed'): void;
-}>();
-
-function chosen(emoji: any) {
- emit('chosen', emoji);
-}
-</script>
-
-<style lang="scss" module>
-.picker {
- height: 100%;
-}
-</style>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 0d8734799c..deedc5badb 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -21,37 +21,37 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :marginMin="20" :marginMax="32">
<div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
- <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
- <MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
- <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
+ <template v-for="(v, k) in Object.fromEntries(Object.entries(form).filter(([_, v]) => !('hidden' in v) || 'hidden' in v && !v.hidden))">
+ <MkInput v-if="v.type === 'number'" v-model="values[k]" type="number" :step="v.step || 1">
+ <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
+ <template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput>
- <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" :mfmAutocomplete="form[item].treatAsMfm">
- <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
+ <MkInput v-else-if="v.type === 'string' && !v.multiline" v-model="values[k]" type="text" :mfmAutocomplete="v.treatAsMfm">
+ <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
+ <template v-if="v.description" #caption>{{ v.description }}</template>
</MkInput>
- <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" :mfmAutocomplete="form[item].treatAsMfm" :mfmPreview="form[item].treatAsMfm">
- <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
+ <MkTextarea v-else-if="v.type === 'string' && v.multiline" v-model="values[k]" :mfmAutocomplete="v.treatAsMfm" :mfmPreview="v.treatAsMfm">
+ <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
+ <template v-if="v.description" #caption>{{ v.description }}</template>
</MkTextarea>
- <MkSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
- <span v-text="form[item].label || item"></span>
- <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
+ <MkSwitch v-else-if="v.type === 'boolean'" v-model="values[k]">
+ <span v-text="v.label || k"></span>
+ <template v-if="v.description" #caption>{{ v.description }}</template>
</MkSwitch>
- <MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
- <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <option v-for="option in form[item].enum" :key="option.value" :value="option.value">{{ option.label }}</option>
+ <MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
+ <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
+ <option v-for="option in v.enum" :key="option.value" :value="option.value">{{ option.label }}</option>
</MkSelect>
- <MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
- <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <option v-for="option in form[item].options" :key="option.value" :value="option.value">{{ option.label }}</option>
+ <MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
+ <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
+ <option v-for="option in v.options" :key="option.value" :value="option.value">{{ option.label }}</option>
</MkRadios>
- <MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
- <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <template v-if="form[item].description" #caption>{{ form[item].description }}</template>
+ <MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
+ <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
+ <template v-if="v.description" #caption>{{ v.description }}</template>
</MkRange>
- <MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
- <span v-text="form[item].content || item"></span>
+ <MkButton v-else-if="v.type === 'button'" @click="v.action($event, values)">
+ <span v-text="v.content || k"></span>
</MkButton>
</template>
</div>
@@ -72,19 +72,21 @@ import MkSelect from './MkSelect.vue';
import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue';
+import type { Form } from '@/scripts/form.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
const props = defineProps<{
title: string;
- form: any;
+ form: Form;
}>();
const emit = defineEmits<{
(ev: 'done', v: {
- canceled?: boolean;
- result?: any;
+ canceled: true;
+ } | {
+ result: Record<string, any>;
}): void;
(ev: 'closed'): void;
}>();
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index a4fde6b701..c561e84a23 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -7,9 +7,9 @@
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3';
-import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js';
-import type { ComponentProps } from 'vue-component-type-helpers';
+import type { ComponentProps as CP } from 'vue-component-type-helpers';
+import type { Form, GetFormResultType } from '@/scripts/form.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
@@ -19,7 +19,6 @@ import MkToast from '@/components/MkToast.vue';
import MkDialog from '@/components/MkDialog.vue';
import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
-import MkEmojiPickerWindow from '@/components/MkEmojiPickerWindow.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
import { MenuItem } from '@/types/menu.js';
@@ -28,15 +27,15 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
export const openingWindowsCount = ref(0);
-export const apiWithDialog = ((
- endpoint: string,
- data: Record<string, any> = {},
+export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
+ endpoint: E,
+ data: P = {} as any,
token?: string | null | undefined,
) => {
const promise = misskeyApi(endpoint, data, token);
promiseDialog(promise, null, async (err) => {
- let title = null;
- let text = err.message + '\n' + (err as any).id;
+ let title: string | undefined;
+ let text = err.message + '\n' + err.id;
if (err.code === 'INTERNAL_ERROR') {
title = i18n.ts.internalServerError;
text = i18n.ts.internalServerErrorDescription;
@@ -88,7 +87,7 @@ export const apiWithDialog = ((
export function promiseDialog<T extends Promise<any>>(
promise: T,
onSuccess?: ((res: any) => void) | null,
- onFailure?: ((err: Error) => void) | null,
+ onFailure?: ((err: Misskey.api.APIError) => void) | null,
text?: string,
): T {
const showing = ref(true);
@@ -149,14 +148,30 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
type ComponentEmit<T> = T extends new () => { $props: infer Props }
- ? EmitsExtractor<Props>
- : never;
+ ? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
+ ? Record<string, unknown> // *.ts ファイルから型がうまく取れないとき用(これがないと {} になって型エラーがうるさい)
+ : EmitsExtractor<Props>
+ : T extends (...args: any) => any
+ ? ReturnType<T> extends { [x: string]: any; __ctx?: { [x: string]: any; props: infer Props } }
+ ? [keyof Pick<T, Extract<keyof T, `on${string}`>>] extends [never]
+ ? Record<string, unknown>
+ : EmitsExtractor<Props>
+ : never
+ : never;
+
+// props に ref を許可するようにする
+type ComponentProps<T extends Component> = { [K in keyof CP<T>]: CP<T>[K] | Ref<CP<T>[K]> };
type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
};
-export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events: ComponentEmit<T> = {} as ComponentEmit<T>, disposeEvent?: keyof ComponentEmit<T>) {
+export async function popup<T extends Component>(
+ component: T,
+ props: ComponentProps<T>,
+ events: ComponentEmit<T> = {} as ComponentEmit<T>,
+ disposeEvent?: keyof ComponentEmit<T>,
+): Promise<{ dispose: () => void }> {
markRaw(component);
const id = ++popupIdCount;
@@ -197,12 +212,12 @@ export function toast(message: string) {
export function alert(props: {
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
}): Promise<void> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, props, {
- done: result => {
+ done: () => {
resolve();
},
}, 'closed');
@@ -211,12 +226,12 @@ export function alert(props: {
export function confirm(props: {
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
okText?: string;
cancelText?: string;
}): Promise<{ canceled: boolean }> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
...props,
showCancelButton: true,
@@ -237,13 +252,15 @@ export function actions<T extends {
danger?: boolean,
}[]>(props: {
type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
actions: T;
-}): Promise<{ canceled: true; result: undefined; } | {
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: T[number]['value'];
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
...props,
actions: props.actions.map(a => ({
@@ -262,19 +279,50 @@ export function actions<T extends {
});
}
+// default が指定されていたら result は null になり得ないことを保証する overload function
export function inputText(props: {
type?: 'text' | 'email' | 'password' | 'url';
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
placeholder?: string | null;
autocomplete?: string;
- default?: string | null;
+ default: string;
minLength?: number;
maxLength?: number;
-}): Promise<{ canceled: true; result: undefined; } | {
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: string;
+}>;
+export function inputText(props: {
+ type?: 'text' | 'email' | 'password' | 'url';
+ title?: string;
+ text?: string;
+ placeholder?: string | null;
+ autocomplete?: string;
+ default?: string | null;
+ minLength?: number;
+ maxLength?: number;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: string | null;
+}>;
+export function inputText(props: {
+ type?: 'text' | 'email' | 'password' | 'url';
+ title?: string;
+ text?: string;
+ placeholder?: string | null;
+ autocomplete?: string;
+ default?: string | null;
+ minLength?: number;
+ maxLength?: number;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: string | null;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
@@ -282,7 +330,7 @@ export function inputText(props: {
type: props.type,
placeholder: props.placeholder,
autocomplete: props.autocomplete,
- default: props.default,
+ default: props.default ?? null,
minLength: props.minLength,
maxLength: props.maxLength,
},
@@ -294,16 +342,41 @@ export function inputText(props: {
});
}
+// default が指定されていたら result は null になり得ないことを保証する overload function
export function inputNumber(props: {
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
placeholder?: string | null;
autocomplete?: string;
- default?: number | null;
-}): Promise<{ canceled: true; result: undefined; } | {
+ default: number;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: 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;
+}>;
+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;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
@@ -311,7 +384,7 @@ export function inputNumber(props: {
type: 'number',
placeholder: props.placeholder,
autocomplete: props.autocomplete,
- default: props.default,
+ default: props.default ?? null,
},
}, {
done: result => {
@@ -322,34 +395,38 @@ export function inputNumber(props: {
}
export function inputDate(props: {
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
placeholder?: string | null;
- default?: Date | null;
-}): Promise<{ canceled: true; result: undefined; } | {
+ default?: string | null;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: Date;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
input: {
type: 'date',
placeholder: props.placeholder,
- default: props.default,
+ default: props.default ?? null,
},
}, {
done: result => {
- resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true });
+ resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
},
}, 'closed');
});
}
-export function authenticateDialog(): Promise<{ canceled: true; result: undefined; } | {
+export function authenticateDialog(): Promise<{
+ canceled: true; result: undefined;
+} | {
canceled: false; result: { password: string; token: string | null; };
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkPasswordDialog, {}, {
done: result => {
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
@@ -358,34 +435,53 @@ export function authenticateDialog(): Promise<{ canceled: true; result: undefine
});
}
+// default が指定されていたら result は null になり得ないことを保証する overload function
export function select<C = any>(props: {
- title?: string | null;
- text?: string | null;
+ title?: string;
+ text?: string;
+ default: string;
+ items: {
+ value: C;
+ text: string;
+ }[];
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: C;
+}>;
+export function select<C = any>(props: {
+ title?: string;
+ text?: string;
default?: string | null;
-} & ({
items: {
value: C;
text: string;
}[];
+}): Promise<{
+ canceled: true; result: undefined;
} | {
- groupedItems: {
- label: string;
- items: {
- value: C;
- text: string;
- }[];
+ canceled: false; result: C | null;
+}>;
+export function select<C = any>(props: {
+ title?: string;
+ text?: string;
+ default?: string | null;
+ items: {
+ value: C;
+ text: string;
}[];
-})): Promise<{ canceled: true; result: undefined; } | {
- canceled: false; result: C;
+}): Promise<{
+ canceled: true; result: undefined;
+} | {
+ canceled: false; result: C | null;
}> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(MkDialog, {
title: props.title,
text: props.text,
select: {
items: props.items,
- groupedItems: props.groupedItems,
- default: props.default,
+ default: props.default ?? null,
},
}, {
done: result => {
@@ -396,7 +492,7 @@ export function select<C = any>(props: {
}
export function success(): Promise<void> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
const showing = ref(true);
window.setTimeout(() => {
showing.value = false;
@@ -411,7 +507,7 @@ export function success(): Promise<void> {
}
export function waiting(): Promise<void> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
const showing = ref(true);
popup(MkWaitingDialog, {
success: false,
@@ -422,9 +518,9 @@ export function waiting(): Promise<void> {
});
}
-export function form(title, form) {
- return new Promise((resolve, reject) => {
- popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form }, {
+export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true } | { result: GetFormResultType<F> }> {
+ return new Promise(resolve => {
+ popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
done: result => {
resolve(result);
},
@@ -433,7 +529,7 @@ export function form(title, form) {
}
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
includeSelf: opts.includeSelf,
localOnly: opts.localOnly,
@@ -446,7 +542,7 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
}
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'file',
multiple,
@@ -460,23 +556,23 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti
});
}
-export async function selectDriveFolder(multiple: boolean) {
- return new Promise((resolve, reject) => {
+export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'folder',
multiple,
}, {
done: folders => {
if (folders) {
- resolve(multiple ? folders : folders[0]);
+ resolve(folders);
}
},
}, 'closed');
});
}
-export async function pickEmoji(src: HTMLElement | null, opts) {
- return new Promise((resolve, reject) => {
+export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> {
+ return new Promise(resolve => {
popup(MkEmojiPickerDialog, {
src,
...opts,
@@ -492,7 +588,7 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
aspectRatio: number;
uploadFolder?: string | null;
}): Promise<Misskey.entities.DriveFile> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
file: image,
aspectRatio: options.aspectRatio,
@@ -505,67 +601,13 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
});
}
-type AwaitType<T> =
- T extends Promise<infer U> ? U :
- T extends (...args: any[]) => Promise<infer V> ? V :
- T;
-let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
-let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
-export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
- if (openingEmojiPicker) return;
-
- activeTextarea = initialTextarea;
-
- const textareas = document.querySelectorAll('textarea, input');
- for (const textarea of Array.from(textareas)) {
- textarea.addEventListener('focus', () => {
- activeTextarea = textarea;
- });
- }
-
- const observer = new MutationObserver(records => {
- for (const record of records) {
- for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) {
- const textareas = node.querySelectorAll('textarea, input') as NodeListOf<NonNullable<typeof activeTextarea>>;
- for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) {
- if (document.activeElement === textarea) activeTextarea = textarea;
- textarea.addEventListener('focus', () => {
- activeTextarea = textarea;
- });
- }
- }
- }
- });
-
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- attributes: false,
- characterData: false,
- });
-
- openingEmojiPicker = await popup(MkEmojiPickerWindow, {
- src,
- ...opts,
- }, {
- chosen: emoji => {
- insertTextAtCursor(activeTextarea, emoji);
- },
- closed: () => {
- openingEmojiPicker!.dispose();
- openingEmojiPicker = null;
- observer.disconnect();
- },
- });
-}
-
-export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement | EventTarget | null, options?: {
+export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
align?: string;
width?: number;
viaKeyboard?: boolean;
onClosing?: () => void;
}): Promise<void> {
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
let dispose;
popup(MkPopupMenu, {
items,
@@ -587,9 +629,9 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
});
}
-export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent): Promise<void> {
+export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
ev.preventDefault();
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
let dispose;
popup(MkContextMenu, {
items,
@@ -608,7 +650,7 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent)
export function post(props: Record<string, any> = {}): Promise<void> {
showMovedDialog();
- return new Promise((resolve, reject) => {
+ return new Promise(resolve => {
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
// Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 12e9416f72..16769ef360 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -135,7 +135,7 @@ async function addRole() {
const { canceled, result: role } = await os.select({
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
});
- if (canceled) return;
+ if (canceled || role == null) return;
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
}
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index 7db6fa5395..28f5838296 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -52,7 +52,7 @@ const directNotesPagination = {
function setFilter(ev) {
const typeItems = notificationTypes.map(t => ({
text: i18n.ts._notification._types[t],
- active: includeTypes.value && includeTypes.value.includes(t),
+ active: (includeTypes.value && includeTypes.value.includes(t)) ?? false,
action: () => {
includeTypes.value = [t];
},
@@ -63,7 +63,7 @@ function setFilter(ev) {
action: () => {
includeTypes.value = null;
},
- }, { type: 'divider' }, ...typeItems] : typeItems;
+ }, { type: 'divider' as const }, ...typeItems] : typeItems;
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index cd38f9850f..1919f80864 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -113,7 +113,7 @@ if (defaultStore.state.uploadFolder) {
function chooseUploadFolder() {
os.selectDriveFolder(false).then(async folder => {
- defaultStore.set('uploadFolder', folder ? folder.id : null);
+ defaultStore.set('uploadFolder', folder[0] ? folder[0].id : null);
os.success();
if (defaultStore.state.uploadFolder) {
uploadFolder.value = await misskeyApi('drive/folders/show', {
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index 79969427ec..ce296ec183 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -213,7 +213,7 @@ async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
os.pickEmoji(getHTMLElement(ev), {
showPinned: false,
}).then(it => {
- const emoji = it as string;
+ const emoji = it;
if (!itemsRef.value.includes(emoji)) {
itemsRef.value.push(emoji);
}
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 676159d1b5..942de19d82 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -203,6 +203,7 @@ async function saveNew(): Promise<void> {
const { canceled, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName,
+ default: '',
});
if (canceled) return;
@@ -371,6 +372,7 @@ async function rename(id: string): Promise<void> {
const { canceled: cancel1, result: name } = await os.inputText({
title: ts._preferencesBackups.inputName,
+ default: '',
});
if (cancel1 || profiles.value[id].name === name) return;
diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts
index 26a027f461..b0db404f28 100644
--- a/packages/frontend/src/scripts/form.ts
+++ b/packages/frontend/src/scripts/form.ts
@@ -12,29 +12,37 @@ export type FormItem = {
label?: string;
type: 'string';
default: string | null;
+ description?: string;
+ required?: boolean;
hidden?: boolean;
multiline?: boolean;
+ treatAsMfm?: boolean;
} | {
label?: string;
type: 'number';
default: number | null;
+ description?: string;
+ required?: boolean;
hidden?: boolean;
step?: number;
} | {
label?: string;
type: 'boolean';
default: boolean | null;
+ description?: string;
hidden?: boolean;
} | {
label?: string;
type: 'enum';
default: string | null;
+ required?: boolean;
hidden?: boolean;
enum: EnumItem[];
} | {
label?: string;
type: 'radio';
default: unknown | null;
+ required?: boolean;
hidden?: boolean;
options: {
label: string;
@@ -44,9 +52,12 @@ export type FormItem = {
label?: string;
type: 'range';
default: number | null;
- step: number;
+ description?: string;
+ required?: boolean;
+ step?: number;
min: number;
max: number;
+ textConverter?: (value: number) => string;
} | {
label?: string;
type: 'object';
@@ -57,6 +68,10 @@ export type FormItem = {
type: 'array';
default: unknown[] | null;
hidden: boolean;
+} | {
+ type: 'button';
+ content?: string;
+ action: (ev: MouseEvent, v: any) => void;
};
export type Form = Record<string, FormItem>;
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 92d2e23d9b..bdb62dca15 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -117,6 +117,7 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-column.vue';
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import { mainRouter } from '@/router/main.js';
+import { MenuItem } from '@/types/menu.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
@@ -221,21 +222,19 @@ document.documentElement.style.scrollBehavior = 'auto';
loadDeck();
function changeProfile(ev: MouseEvent) {
- const items = ref([{
+ let items: MenuItem[] = [{
text: deckStore.state.profile,
- active: true.valueOf,
- }]);
+ active: true,
+ action: () => {},
+ }];
getProfiles().then(profiles => {
- items.value = [{
- text: deckStore.state.profile,
- active: true.valueOf,
- }, ...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
+ items.push(...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
text: k,
action: () => {
deckStore.set('profile', k);
unisonReload();
},
- }))), { type: 'divider' }, {
+ }))), { type: 'divider' as const }, {
text: i18n.ts._deck.newProfile,
icon: 'ti ti-plus',
action: async () => {
@@ -248,9 +247,10 @@ function changeProfile(ev: MouseEvent) {
deckStore.set('profile', name);
unisonReload();
},
- }];
+ });
+ }).then(() => {
+ os.popupMenu(items, ev.currentTarget ?? ev.target);
});
- os.popupMenu(items, ev.currentTarget ?? ev.target);
}
async function deleteProfile() {
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 7a3671a240..b8efd3bda9 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -93,10 +93,10 @@ const fetch = () => {
const choose = () => {
os.selectDriveFolder(false).then(folder => {
- if (folder == null) {
+ if (folder[0] == null) {
return;
}
- widgetProps.folderId = folder.id;
+ widgetProps.folderId = folder[0].id;
save();
fetch();
});