Merge branch 'develop' into enh-14794
This commit is contained in:
commit
e4dea52f39
|
@ -8,6 +8,7 @@
|
|||
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751)
|
||||
- Enhance: ドライブでソートができるように
|
||||
- Enhance: アイコンデコレーション管理画面の改善
|
||||
- Enhance: 「単なるラッキー」の取得条件を変更
|
||||
- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 )
|
||||
- Enhance: MiAuth, OAuthの認可画面の改善
|
||||
|
@ -22,9 +23,13 @@
|
|||
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
|
||||
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
|
||||
- Fix: リンク切れを修正
|
||||
|
||||
### Server
|
||||
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
|
||||
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
|
||||
- Fix: Nested proxy requestsを検出した際にブロックするように
|
||||
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
|
||||
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
|
||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -5214,6 +5214,10 @@ export interface Locale extends ILocale {
|
|||
* アカウントを選択してください
|
||||
*/
|
||||
"pleaseSelectAccount": string;
|
||||
/**
|
||||
* 利用可能なロール
|
||||
*/
|
||||
"availableRoles": string;
|
||||
/**
|
||||
* 文字数
|
||||
*/
|
||||
|
|
|
@ -1299,6 +1299,7 @@ yourNameContainsProhibitedWordsDescription: "名前に禁止されている文
|
|||
thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示にはログインが必要と設定されています"
|
||||
lockdown: "ロックダウン"
|
||||
pleaseSelectAccount: "アカウントを選択してください"
|
||||
availableRoles: "利用可能なロール"
|
||||
textCount: "文字数"
|
||||
reset: "リセット"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.10.2-alpha.1",
|
||||
"version": "2024.10.2-alpha.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -5,11 +5,52 @@
|
|||
|
||||
import Redis from 'ioredis';
|
||||
import { loadConfig } from '../built/config.js';
|
||||
import { createPostgresDataSource } from '../built/postgres.js';
|
||||
|
||||
const config = loadConfig();
|
||||
const redis = new Redis(config.redis);
|
||||
|
||||
redis.on('connect', () => redis.disconnect());
|
||||
redis.on('error', (e) => {
|
||||
throw e;
|
||||
});
|
||||
async function connectToPostgres() {
|
||||
const source = createPostgresDataSource(config);
|
||||
await source.initialize();
|
||||
await source.destroy();
|
||||
}
|
||||
|
||||
async function connectToRedis(redisOptions) {
|
||||
return await new Promise(async (resolve, reject) => {
|
||||
const redis = new Redis({
|
||||
...redisOptions,
|
||||
lazyConnect: true,
|
||||
reconnectOnError: false,
|
||||
showFriendlyErrorStack: true,
|
||||
});
|
||||
redis.on('error', e => reject(e));
|
||||
|
||||
try {
|
||||
await redis.connect();
|
||||
resolve();
|
||||
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
|
||||
} finally {
|
||||
redis.disconnect(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If not all of these are defined, the default one gets reused.
|
||||
// so we use a Set to only try connecting once to each **uniq** redis.
|
||||
const promises = Array
|
||||
.from(new Set([
|
||||
config.redis,
|
||||
config.redisForPubsub,
|
||||
config.redisForJobQueue,
|
||||
config.redisForTimelines,
|
||||
config.redisForReactions,
|
||||
]))
|
||||
.map(connectToRedis)
|
||||
.concat([
|
||||
connectToPostgres()
|
||||
]);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
|
|
|
@ -232,6 +232,12 @@ export class ApPersonService implements OnModuleInit {
|
|||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
|
||||
const [avatar, banner] = await Promise.all([icon, image].map(img => {
|
||||
// icon and image may be arrays
|
||||
// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||
if (Array.isArray(img)) {
|
||||
img = img.find(item => item && item.url) ?? null;
|
||||
}
|
||||
|
||||
// if we have an explicitly missing image, return an
|
||||
// explicitly-null set of values
|
||||
if ((img == null) || (typeof img === 'object' && img.url == null)) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
@ -13,6 +14,49 @@ export const meta = {
|
|||
requireCredential: true,
|
||||
requireRolePolicy: 'canManageAvatarDecorations',
|
||||
kind: 'write:admin:avatar-decorations',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
roleIdsThatCanBeUsedThisDecoration: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
|
@ -32,14 +76,25 @@ export const paramDef = {
|
|||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private avatarDecorationService: AvatarDecorationService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.avatarDecorationService.create({
|
||||
const created = await this.avatarDecorationService.create({
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
url: ps.url,
|
||||
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
|
||||
}, me);
|
||||
|
||||
return {
|
||||
id: created.id,
|
||||
createdAt: this.idService.parse(created.id).date.toISOString(),
|
||||
updatedAt: null,
|
||||
name: created.name,
|
||||
description: created.description,
|
||||
url: created.url,
|
||||
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { AnnouncementsRepository, AnnouncementReadsRepository } from '@/models/_.js';
|
||||
import type { MiAnnouncement } from '@/models/Announcement.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
|
|
|
@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<div :class="$style.root" class="_gaps_s">
|
||||
<div class="_gaps_s">
|
||||
<MkFolder :withSpacer="false">
|
||||
<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
|
||||
<template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
|
||||
|
@ -151,6 +151,4 @@ function showMenu(ev: MouseEvent) {
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -160,7 +160,7 @@ async function deleteAntenna() {
|
|||
function addUser() {
|
||||
os.selectUser({ includeSelf: true }).then(user => {
|
||||
users.value = users.value.trim();
|
||||
users.value += '\n@' + Misskey.acct.toString(user as any);
|
||||
users.value += '\n@' + Misskey.acct.toString(user);
|
||||
users.value = users.value.trim();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div :class="$style.headerText">{{ i18n.ts.pleaseSelectAccount }}</div>
|
||||
</div>
|
||||
<div :class="$style.accountSelectorRoot">
|
||||
<div>
|
||||
<div :class="$style.accountSelectorLabel">{{ i18n.ts.selectAccount }}</div>
|
||||
<div :class="$style.accountSelectorList">
|
||||
<template v-for="[id, user] in users">
|
||||
|
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<slot name="consentAdditionalInfo"></slot>
|
||||
<div :class="$style.accountSelectorRoot">
|
||||
<div>
|
||||
<div :class="$style.accountSelectorLabel">
|
||||
{{ i18n.ts._auth.scopeUser }} <button class="_textButton" @click="clickBackToAccountSelect">{{ i18n.ts.switchAccount }}</button>
|
||||
</div>
|
||||
|
|
|
@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
const props = defineProps<{
|
||||
channel: Record<string, any>;
|
||||
channel: Misskey.entities.Channel;
|
||||
}>();
|
||||
|
||||
const getLastReadedAt = (): number | null => {
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:withOkButton="true"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.cropImage }}</template>
|
||||
<template #default="{ width, height }">
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
|
||||
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
|
||||
<template #header>:{{ emoji.name }}:</template>
|
||||
<template #default>
|
||||
<MkSpacer>
|
||||
|
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</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>
|
||||
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
|
||||
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
<div v-if="actions" :class="$style.buttons">
|
||||
|
@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
|
|||
text: string;
|
||||
primary?: boolean,
|
||||
danger?: boolean,
|
||||
callback: (...args: any[]) => void;
|
||||
callback: (...args: unknown[]) => void;
|
||||
}[];
|
||||
showOkButton?: boolean;
|
||||
showCancelButton?: boolean;
|
||||
|
|
|
@ -157,7 +157,7 @@ const ilFilesObserver = new IntersectionObserver(
|
|||
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
|
||||
);
|
||||
|
||||
const sortModeSelect = ref('+createdAt');
|
||||
const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
|
||||
|
||||
watch(folder, () => emit('cd', folder.value));
|
||||
watch(sortModeSelect, () => {
|
||||
|
@ -198,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
|
|||
removeFolder(folderId);
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent): any {
|
||||
function onDragover(ev: DragEvent) {
|
||||
if (!ev.dataTransfer) return;
|
||||
|
||||
// ドラッグ元が自分自身の所有するアイテムだったら
|
||||
|
@ -243,7 +243,7 @@ function onDragleave() {
|
|||
draghover.value = false;
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent): any {
|
||||
function onDrop(ev: DragEvent) {
|
||||
draghover.value = false;
|
||||
|
||||
if (!ev.dataTransfer) return;
|
||||
|
@ -332,7 +332,7 @@ function createFolder() {
|
|||
title: i18n.ts.createFolder,
|
||||
placeholder: i18n.ts.folderName,
|
||||
}).then(({ canceled, result: name }) => {
|
||||
if (canceled) return;
|
||||
if (canceled || name == null) return;
|
||||
misskeyApi('drive/folders/create', {
|
||||
name: name,
|
||||
parentId: folder.value ? folder.value.id : undefined,
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:scroll="false"
|
||||
:withOkButton="false"
|
||||
@close="cancel()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
|||
elm.title = getEmojiName(emoji);
|
||||
}
|
||||
|
||||
function nestedChosen(emoji: any, ev: MouseEvent) {
|
||||
function nestedChosen(emoji: string, ev: MouseEvent) {
|
||||
emit('chosen', emoji, ev);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
|||
elm.title = getEmojiName(emoji);
|
||||
}
|
||||
|
||||
function chosen(emoji: any, ev?: MouseEvent) {
|
||||
function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
|
||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
|
|||
// 最近使った絵文字更新
|
||||
if (!pinned.value?.includes(key)) {
|
||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji: any) => emoji !== key);
|
||||
recents = recents.filter((emoji) => emoji !== key);
|
||||
recents.unshift(key);
|
||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ export type Extension = {
|
|||
author: string;
|
||||
description?: string;
|
||||
permissions?: string[];
|
||||
config?: Record<string, any>;
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
} | {
|
||||
type: 'theme';
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@click="cancel()"
|
||||
@ok="ok()"
|
||||
@close="cancel()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
{{ title }}
|
||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue';
|
||||
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue';
|
||||
import { debounce } from 'throttle-debounce';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
|
|||
|
||||
const props = defineProps<{
|
||||
modelValue: string | number | null;
|
||||
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
|
||||
type?: InputHTMLAttributes['type'];
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@ -64,8 +64,8 @@ const props = defineProps<{
|
|||
mfmAutocomplete?: boolean | SuggestionType[],
|
||||
autocapitalize?: string;
|
||||
spellcheck?: boolean;
|
||||
inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal';
|
||||
step?: any;
|
||||
inputmode?: InputHTMLAttributes['inputmode'];
|
||||
step?: InputHTMLAttributes['step'];
|
||||
datalist?: string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
|
|
@ -227,7 +227,7 @@ const emit = defineEmits<{
|
|||
}>();
|
||||
|
||||
const inTimeline = inject<boolean>('inTimeline', false);
|
||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false));
|
||||
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
|
||||
const inChannel = inject('inChannel', null);
|
||||
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any);
|
||||
const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
|
||||
|
||||
function ok() {
|
||||
emit('done', {
|
||||
|
|
|
@ -39,7 +39,7 @@ import number from '@/filters/number.js';
|
|||
import XValue from '@/components/MkObjectView.value.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
value: any;
|
||||
value: unknown;
|
||||
}>();
|
||||
|
||||
const collapsed = reactive({});
|
||||
|
@ -50,19 +50,19 @@ if (isObject(props.value)) {
|
|||
}
|
||||
}
|
||||
|
||||
function isObject(v): boolean {
|
||||
function isObject(v: unknown): v is Record<PropertyKey, unknown> {
|
||||
return typeof v === 'object' && !Array.isArray(v) && v !== null;
|
||||
}
|
||||
|
||||
function isArray(v): boolean {
|
||||
function isArray(v: unknown): v is unknown[] {
|
||||
return Array.isArray(v);
|
||||
}
|
||||
|
||||
function isEmpty(v): boolean {
|
||||
function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] {
|
||||
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
||||
}
|
||||
|
||||
function collapsable(v): boolean {
|
||||
function collapsable(v: unknown): boolean {
|
||||
return (isObject(v) || isArray(v)) && !isEmpty(v);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:buttonsLeft="buttonsLeft"
|
||||
:buttonsRight="buttonsRight"
|
||||
:contextmenu="contextmenu"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>
|
||||
<template v-if="pageMetadata">
|
||||
|
@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import RouterView from '@/components/global/RouterView.vue';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { openingWindowsCount } from '@/os.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
|
||||
|
@ -48,7 +48,7 @@ const props = defineProps<{
|
|||
initialPath: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
|
@ -58,7 +58,7 @@ const windowRouter = routerFactory(props.initialPath);
|
|||
const contents = shallowRef<HTMLElement | null>(null);
|
||||
const pageMetadata = ref<null | PageMetadata>(null);
|
||||
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const history = ref<{ path: string; key: any; }[]>([{
|
||||
const history = ref<{ path: string; key: string; }[]>([{
|
||||
path: windowRouter.getCurrentPath(),
|
||||
key: windowRouter.getCurrentKey(),
|
||||
}]);
|
||||
|
|
|
@ -19,7 +19,7 @@ defineProps<{
|
|||
items: MenuItem[];
|
||||
align?: 'center' | string;
|
||||
width?: number;
|
||||
src?: any;
|
||||
src?: HTMLElement | null;
|
||||
returnFocusTo?: HTMLElement | null;
|
||||
}>();
|
||||
|
||||
|
|
|
@ -125,25 +125,13 @@ import { miLocalStorage } from '@/local-storage.js';
|
|||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
reply?: Misskey.entities.Note;
|
||||
renote?: Misskey.entities.Note;
|
||||
channel?: Misskey.entities.Channel; // TODO
|
||||
mention?: Misskey.entities.User;
|
||||
specified?: Misskey.entities.UserDetailed;
|
||||
initialText?: string;
|
||||
initialCw?: string;
|
||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||
initialFiles?: Misskey.entities.DriveFile[];
|
||||
initialLocalOnly?: boolean;
|
||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||
initialNote?: Misskey.entities.Note;
|
||||
instant?: boolean;
|
||||
const props = withDefaults(defineProps<PostFormProps & {
|
||||
fixed?: boolean;
|
||||
autofocus?: boolean;
|
||||
freezeAfterPosted?: boolean;
|
||||
|
@ -952,8 +940,8 @@ function showActions(ev: MouseEvent) {
|
|||
action.handler({
|
||||
text: text.value,
|
||||
cw: cw.value,
|
||||
}, (key, value: any) => {
|
||||
if (typeof key !== 'string') return;
|
||||
}, (key, value) => {
|
||||
if (typeof key !== 'string' || typeof value !== 'string') return;
|
||||
if (key === 'text') { text.value = value; }
|
||||
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div v-show="props.modelValue.length != 0" :class="$style.root">
|
||||
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{element}">
|
||||
<template #item="{ element }">
|
||||
<div
|
||||
:class="$style.file"
|
||||
role="button"
|
||||
|
@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js';
|
|||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any[];
|
||||
modelValue: Misskey.entities.DriveFile[];
|
||||
detachMediaFn?: (id: string) => void;
|
||||
}>();
|
||||
|
||||
const mock = inject<boolean>('mock', false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any[]): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
|
||||
(ev: 'detach', id: string): void;
|
||||
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
|
||||
(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
|
||||
|
@ -113,7 +113,7 @@ async function rename(file) {
|
|||
});
|
||||
}
|
||||
|
||||
async function describe(file) {
|
||||
async function describe(file: Misskey.entities.DriveFile) {
|
||||
if (mock) return;
|
||||
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
|
||||
|
|
|
@ -11,23 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkModal from '@/components/MkModal.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
reply?: Misskey.entities.Note;
|
||||
renote?: Misskey.entities.Note;
|
||||
channel?: any; // TODO
|
||||
mention?: Misskey.entities.User;
|
||||
specified?: Misskey.entities.UserDetailed;
|
||||
initialText?: string;
|
||||
initialCw?: string;
|
||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||
initialFiles?: Misskey.entities.DriveFile[];
|
||||
initialLocalOnly?: boolean;
|
||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||
initialNote?: Misskey.entities.Note;
|
||||
const props = withDefaults(defineProps<PostFormProps & {
|
||||
instant?: boolean;
|
||||
fixed?: boolean;
|
||||
autofocus?: boolean;
|
||||
|
|
|
@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T extends unknown">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
value: any;
|
||||
modelValue: T;
|
||||
value: T;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: T): void;
|
||||
}>();
|
||||
|
||||
const checked = computed(() => props.modelValue === props.value);
|
||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { getEmojiName } from '@@/js/emojilist.js';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
|
@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
|||
defineProps<{
|
||||
showing: boolean;
|
||||
reaction: string;
|
||||
users: any[]; // TODO
|
||||
users: Misskey.entities.UserLite[];
|
||||
count: number;
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
|
|
@ -24,11 +24,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
|
||||
<div v-if="needCaptcha">
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" provider="testcaptcha"/>
|
||||
</div>
|
||||
|
||||
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:width="500"
|
||||
:height="600"
|
||||
@close="onClose"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.signup }}</template>
|
||||
|
||||
|
|
|
@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
<script lang="ts">
|
||||
export type SuperMenuDef = {
|
||||
title?: string;
|
||||
items: ({
|
||||
type: 'a';
|
||||
href: string;
|
||||
target?: string;
|
||||
icon?: string;
|
||||
text: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
} | {
|
||||
type: 'button';
|
||||
icon?: string;
|
||||
text: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
action: (ev: MouseEvent) => void;
|
||||
} | {
|
||||
type: 'link';
|
||||
to: string;
|
||||
icon?: string;
|
||||
text: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
})[];
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
def: any[];
|
||||
def: SuperMenuDef[];
|
||||
grid?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:okButtonDisabled="false"
|
||||
:canClose="false"
|
||||
@close="dialog?.close()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
@ok="ok()"
|
||||
>
|
||||
<template #header>{{ title || i18n.ts.generateAccessToken }}</template>
|
||||
|
|
|
@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
|
|||
sensitive.value = info.sensitive ?? false;
|
||||
});
|
||||
|
||||
function adjustTweetHeight(message: any) {
|
||||
function adjustTweetHeight(message: MessageEvent) {
|
||||
if (message.origin !== 'https://platform.twitter.com') return;
|
||||
const embed = message.data?.['twttr.embed'];
|
||||
if (embed?.method !== 'twttr.private.resize') return;
|
||||
|
@ -193,14 +193,16 @@ function openPlayer(): void {
|
|||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
|
||||
url: requestUrl.href,
|
||||
}, {
|
||||
// TODO
|
||||
closed: () => {
|
||||
dispose();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
(window as any).addEventListener('message', adjustTweetHeight);
|
||||
window.addEventListener('message', adjustTweetHeight);
|
||||
|
||||
onUnmounted(() => {
|
||||
(window as any).removeEventListener('message', adjustTweetHeight);
|
||||
window.removeEventListener('message', adjustTweetHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="dialog"
|
||||
:width="400"
|
||||
@close="dialog?.close()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
|
||||
<template v-else #header>New announcement</template>
|
||||
|
@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
|
||||
type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
|
||||
|
||||
const props = defineProps<{
|
||||
user: Misskey.entities.User,
|
||||
announcement?: Misskey.entities.Announcement,
|
||||
announcement?: Required<AdminAnnouncementType>,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
|
||||
|
@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
|
|||
const display = ref(props.announcement ? props.announcement.display : 'dialog');
|
||||
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
async function done() {
|
||||
const params = {
|
||||
title: title.value,
|
||||
|
@ -88,7 +90,7 @@ async function done() {
|
|||
display: display.value,
|
||||
needConfirmationToRead: needConfirmationToRead.value,
|
||||
userId: props.user.id,
|
||||
};
|
||||
} satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
|
||||
|
||||
if (props.announcement) {
|
||||
await os.apiWithDialog('admin/announcements/update', {
|
||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
@click="cancel()"
|
||||
@close="cancel()"
|
||||
@ok="ok()"
|
||||
@closed="$emit('closed')"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.selectUser }}</template>
|
||||
<div>
|
||||
|
|
|
@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
|
||||
defineProps<{
|
||||
showing: boolean;
|
||||
users: any[]; // TODO
|
||||
users: Misskey.entities.UserLite[];
|
||||
count: number;
|
||||
targetElement: HTMLElement;
|
||||
}>();
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
||||
</MkSelect>
|
||||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
</header>
|
||||
<Sortable
|
||||
:modelValue="props.widgets"
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
|
||||
appear
|
||||
@afterLeave="$emit('closed')"
|
||||
@afterLeave="emit('closed')"
|
||||
>
|
||||
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
|
||||
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
|
@ -60,6 +60,13 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
type WindowButton = {
|
||||
title: string;
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
highlighted?: boolean;
|
||||
};
|
||||
|
||||
const minHeight = 50;
|
||||
const minWidth = 250;
|
||||
|
||||
|
@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{
|
|||
mini?: boolean;
|
||||
front?: boolean;
|
||||
contextmenu?: MenuItem[] | null;
|
||||
buttonsLeft?: any[];
|
||||
buttonsRight?: any[];
|
||||
buttonsLeft?: WindowButton[];
|
||||
buttonsRight?: WindowButton[];
|
||||
}>(), {
|
||||
initialWidth: 400,
|
||||
initialHeight: null,
|
||||
|
|
|
@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T extends unknown">
|
||||
import { ref, watch } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
p: () => Promise<any>;
|
||||
p: () => Promise<T>;
|
||||
}>();
|
||||
|
||||
const pending = ref(true);
|
||||
const resolved = ref(false);
|
||||
const rejected = ref(false);
|
||||
const result = ref<any>(null);
|
||||
const result = ref<T | null>(null);
|
||||
|
||||
const process = () => {
|
||||
if (props.p == null) {
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="chosen && !shouldHide" :class="$style.root">
|
||||
<div v-if="chosen && !shouldHide">
|
||||
<div
|
||||
v-if="!showMenu"
|
||||
:class="[$style.main, {
|
||||
|
@ -120,10 +120,6 @@ function reduceFrequency(): void {
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
|
||||
}
|
||||
|
||||
.main {
|
||||
text-align: center;
|
||||
|
||||
|
|
|
@ -467,8 +467,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
|||
}
|
||||
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
console.error('unrecognized ast type:', (token as any).type);
|
||||
// @ts-expect-error 存在しないASTタイプ
|
||||
console.error('unrecognized ast type:', token.type);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
|
|||
|
||||
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
|
||||
|
||||
export type RouterFlag = 'forcePage';
|
||||
|
||||
type ParsedPath = (string | {
|
||||
name: string;
|
||||
startsWith?: string;
|
||||
|
@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
|||
current: Resolved;
|
||||
currentRef: ShallowRef<Resolved>;
|
||||
currentRoute: ShallowRef<RouteDef>;
|
||||
navHook: ((path: string, flag?: any) => boolean) | null;
|
||||
navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
|
||||
|
||||
/**
|
||||
* ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
|
||||
|
@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
|
|||
|
||||
resolve(path: string): Resolved | null;
|
||||
|
||||
getCurrentPath(): any;
|
||||
getCurrentPath(): string;
|
||||
|
||||
getCurrentKey(): string;
|
||||
|
||||
push(path: string, flag?: any): void;
|
||||
push(path: string, flag?: RouterFlag): void;
|
||||
|
||||
replace(path: string, key?: string | null): void;
|
||||
|
||||
|
@ -197,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||
private currentKey = Date.now().toString();
|
||||
private redirectCount = 0;
|
||||
|
||||
public navHook: ((path: string, flag?: any) => boolean) | null = null;
|
||||
public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
|
||||
|
||||
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
|
||||
super();
|
||||
|
@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
|
|||
return this.currentKey;
|
||||
}
|
||||
|
||||
public push(path: string, flag?: any) {
|
||||
public push(path: string, flag?: RouterFlag) {
|
||||
const beforePath = this.currentPath;
|
||||
if (path === beforePath) {
|
||||
this.emit('same');
|
||||
|
|
|
@ -28,12 +28,13 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
|||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
||||
export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
||||
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
data: P = {} as any,
|
||||
data: P,
|
||||
token?: string | null | undefined,
|
||||
customErrors?: Record<string, { title?: string; text: string; }>,
|
||||
) => {
|
||||
|
@ -94,7 +95,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
|
|||
|
||||
export function promiseDialog<T extends Promise<any>>(
|
||||
promise: T,
|
||||
onSuccess?: ((res: any) => void) | null,
|
||||
onSuccess?: ((res: Awaited<T>) => void) | null,
|
||||
onFailure?: ((err: Misskey.api.APIError) => void) | null,
|
||||
text?: string,
|
||||
): T {
|
||||
|
@ -136,12 +137,12 @@ export function promiseDialog<T extends Promise<any>>(
|
|||
}
|
||||
|
||||
let popupIdCount = 0;
|
||||
export const popups = ref([]) as Ref<{
|
||||
export const popups = ref<{
|
||||
id: number;
|
||||
component: Component;
|
||||
props: Record<string, any>;
|
||||
events: Record<string, any>;
|
||||
}[]>;
|
||||
}[]>([]);
|
||||
|
||||
const zIndexes = {
|
||||
veryLow: 500000,
|
||||
|
@ -458,7 +459,7 @@ type SelectItem<C> = {
|
|||
};
|
||||
|
||||
// default が指定されていたら result は null になり得ないことを保証する overload function
|
||||
export function select<C = any>(props: {
|
||||
export function select<C = unknown>(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
default: string;
|
||||
|
@ -471,7 +472,7 @@ export function select<C = any>(props: {
|
|||
} | {
|
||||
canceled: false; result: C;
|
||||
}>;
|
||||
export function select<C = any>(props: {
|
||||
export function select<C = unknown>(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
default?: string | null;
|
||||
|
@ -484,7 +485,7 @@ export function select<C = any>(props: {
|
|||
} | {
|
||||
canceled: false; result: C | null;
|
||||
}>;
|
||||
export function select<C = any>(props: {
|
||||
export function select<C = unknown>(props: {
|
||||
title?: string;
|
||||
text?: string;
|
||||
default?: string | null;
|
||||
|
@ -687,13 +688,13 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
|
|||
}));
|
||||
}
|
||||
|
||||
export function post(props: Record<string, any> = {}): Promise<void> {
|
||||
export function post(props: PostFormProps = {}): Promise<void> {
|
||||
pleaseLogin({
|
||||
openOnRemote: (props.initialText || props.initialNote ? {
|
||||
type: 'share',
|
||||
params: {
|
||||
text: props.initialText ?? props.initialNote.text,
|
||||
visibility: props.initialVisibility ?? props.initialNote?.visibility,
|
||||
text: props.initialText ?? props.initialNote?.text ?? '',
|
||||
visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
|
||||
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
|
||||
},
|
||||
} : undefined),
|
||||
|
|
|
@ -269,6 +269,9 @@ const patronsWithIcon = [{
|
|||
}, {
|
||||
name: '如月ユカ',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/f24a042076a041b6811a2f124eb620ca.jpg',
|
||||
}, {
|
||||
name: 'Yatoigawa',
|
||||
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
|
@ -375,6 +378,8 @@ const patrons = [
|
|||
'はとぽぷさん',
|
||||
'100の人 (エスパー・イーシア)',
|
||||
'ケモナーのケシン',
|
||||
'こまつぶり',
|
||||
'まゆつな空高',
|
||||
];
|
||||
|
||||
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
@ -99,19 +99,19 @@ async function addUser() {
|
|||
const { canceled: canceled1, result: username } = await os.inputText({
|
||||
title: i18n.ts.username,
|
||||
});
|
||||
if (canceled1) return;
|
||||
if (canceled1 || username == null) return;
|
||||
|
||||
const { canceled: canceled2, result: password } = await os.inputText({
|
||||
title: i18n.ts.password,
|
||||
type: 'password',
|
||||
});
|
||||
if (canceled2) return;
|
||||
if (canceled2 || password == null) return;
|
||||
|
||||
os.apiWithDialog('admin/accounts/create', {
|
||||
username: username,
|
||||
password: password,
|
||||
}).then(res => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
220
packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
Normal file
220
packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
Normal file
|
@ -0,0 +1,220 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkWindow
|
||||
ref="windowEl"
|
||||
:initialWidth="400"
|
||||
:initialHeight="500"
|
||||
:canResize="true"
|
||||
@close="windowEl?.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="avatarDecoration" #header>{{ avatarDecoration.name }}</template>
|
||||
<template v-else #header>New decoration</template>
|
||||
|
||||
<div style="display: flex; flex-direction: column; min-height: 100%;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28" style="flex-grow: 1;">
|
||||
<div class="_gaps_m">
|
||||
<div :class="$style.preview">
|
||||
<div :class="[$style.previewItem, $style.light]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
|
||||
</div>
|
||||
<div :class="[$style.previewItem, $style.dark]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="url != '' ? [{ url }] : []" forceShowDecoration/>
|
||||
</div>
|
||||
</div>
|
||||
<MkInput v-model="name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.availableRoles }}</template>
|
||||
<template #suffix>{{ rolesThatCanBeUsedThisDecoration.length === 0 ? i18n.ts.all : rolesThatCanBeUsedThisDecoration.length }}</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<div v-for="role in rolesThatCanBeUsedThisDecoration" :key="role.id" :class="$style.roleItem">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="removeRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkButton v-if="avatarDecoration" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<div :class="$style.footer">
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.avatarDecoration ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkWindow from '@/components/MkWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { signinRequired } from '@/account.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
const props = defineProps<{
|
||||
avatarDecoration?: any,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
|
||||
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
|
||||
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
|
||||
const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
|
||||
const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
|
||||
|
||||
watch(roleIdsThatCanBeUsedThisDecoration, async () => {
|
||||
rolesThatCanBeUsedThisDecoration.value = (await Promise.all(roleIdsThatCanBeUsedThisDecoration.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
||||
}, { immediate: true });
|
||||
|
||||
async function addRole() {
|
||||
const roles = await misskeyApi('admin/roles/list');
|
||||
const currentRoleIds = rolesThatCanBeUsedThisDecoration.value.map(x => x.id);
|
||||
|
||||
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 || role == null) return;
|
||||
|
||||
rolesThatCanBeUsedThisDecoration.value.push(role);
|
||||
}
|
||||
|
||||
async function removeRole(role, ev) {
|
||||
rolesThatCanBeUsedThisDecoration.value = rolesThatCanBeUsedThisDecoration.value.filter(x => x.id !== role.id);
|
||||
}
|
||||
|
||||
async function done() {
|
||||
const params = {
|
||||
url: url.value,
|
||||
name: name.value,
|
||||
description: description.value,
|
||||
roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
|
||||
};
|
||||
|
||||
if (props.avatarDecoration) {
|
||||
await os.apiWithDialog('admin/avatar-decorations/update', {
|
||||
id: props.avatarDecoration.id,
|
||||
...params,
|
||||
});
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
id: props.avatarDecoration.id,
|
||||
...params,
|
||||
},
|
||||
});
|
||||
|
||||
windowEl.value?.close();
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/avatar-decorations/create', params);
|
||||
|
||||
emit('done', {
|
||||
created: created,
|
||||
});
|
||||
|
||||
windowEl.value?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('admin/avatar-decorations/delete', {
|
||||
id: props.avatarDecoration.id,
|
||||
}).then(() => {
|
||||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
windowEl.value?.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.preview {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: var(--MI-margin);
|
||||
}
|
||||
|
||||
.previewItem {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--MI-radius);
|
||||
|
||||
&.light {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: #222;
|
||||
}
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.role {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roleUnassign {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
z-index: 10000;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
}
|
||||
</style>
|
|
@ -5,92 +5,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps">
|
||||
<MkFolder v-for="avatarDecoration in avatarDecorations" :key="avatarDecoration.id ?? avatarDecoration._id" :defaultOpen="avatarDecoration.id == null">
|
||||
<template #label>{{ avatarDecoration.name }}</template>
|
||||
<template #caption>{{ avatarDecoration.description }}</template>
|
||||
|
||||
<div :class="$style.editorRoot">
|
||||
<div :class="$style.editorWrapper">
|
||||
<div :class="$style.preview">
|
||||
<div :class="[$style.previewItem, $style.light]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
|
||||
</div>
|
||||
<div :class="[$style.previewItem, $style.dark]">
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[avatarDecoration]" forceShowDecoration/>
|
||||
<div :class="$style.decorations">
|
||||
<div
|
||||
v-for="avatarDecoration in avatarDecorations"
|
||||
:key="avatarDecoration.id"
|
||||
v-panel
|
||||
:class="$style.decoration"
|
||||
@click="edit(avatarDecoration)"
|
||||
>
|
||||
<div :class="$style.decorationName"><MkCondensedLine :minScale="0.5">{{ avatarDecoration.name }}</MkCondensedLine></div>
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="avatarDecoration.name">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="avatarDecoration.description">
|
||||
<template #label>{{ i18n.ts.description }}</template>
|
||||
</MkTextarea>
|
||||
<MkInput v-model="avatarDecoration.url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<div class="_buttons">
|
||||
<MkButton inline primary @click="save(avatarDecoration)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton v-if="avatarDecoration.id != null" inline danger @click="del(avatarDecoration)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import { signinRequired } from '@/account.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
||||
|
||||
const $i = signinRequired();
|
||||
|
||||
function add() {
|
||||
avatarDecorations.value.unshift({
|
||||
_id: Math.random().toString(36),
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
url: '',
|
||||
});
|
||||
}
|
||||
|
||||
function del(avatarDecoration) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
|
||||
misskeyApi('admin/avatar-decorations/delete', avatarDecoration);
|
||||
});
|
||||
}
|
||||
|
||||
async function save(avatarDecoration) {
|
||||
if (avatarDecoration.id == null) {
|
||||
await os.apiWithDialog('admin/avatar-decorations/create', avatarDecoration);
|
||||
load();
|
||||
} else {
|
||||
os.apiWithDialog('admin/avatar-decorations/update', avatarDecoration);
|
||||
}
|
||||
}
|
||||
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
||||
|
||||
function load() {
|
||||
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
|
||||
|
@ -100,6 +46,37 @@ function load() {
|
|||
|
||||
load();
|
||||
|
||||
async function add(ev: MouseEvent) {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
avatarDecorations.value.unshift(result.created);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
function edit(avatarDecoration) {
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration-edit-dialog.vue')), {
|
||||
avatarDecoration: avatarDecoration,
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
const index = avatarDecorations.value.findIndex(x => x.id === avatarDecoration.id);
|
||||
avatarDecorations.value[index] = {
|
||||
...avatarDecorations.value[index],
|
||||
...result.updated,
|
||||
};
|
||||
} else if (result.deleted) {
|
||||
avatarDecorations.value = avatarDecorations.value.filter(x => x.id !== avatarDecoration.id);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
|
@ -116,53 +93,26 @@ definePageMetadata(() => ({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.editorRoot {
|
||||
container: editor / inline-size;
|
||||
}
|
||||
|
||||
.editorWrapper {
|
||||
.decorations {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
gap: var(--MI-margin);
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: var(--MI-margin);
|
||||
.decoration {
|
||||
cursor: pointer;
|
||||
padding: 16px 16px 28px 16px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 90%;
|
||||
overflow: clip;
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.previewItem {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--MI-radius);
|
||||
|
||||
&.light {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background: #222;
|
||||
}
|
||||
}
|
||||
|
||||
@container editor (min-width: 600px) {
|
||||
.editorWrapper {
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: calc(var(--MI-margin) * 2);
|
||||
}
|
||||
|
||||
.preview {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
.decorationName {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -116,7 +116,7 @@ const selectAll = () => {
|
|||
if (selectedEmojis.value.length > 0) {
|
||||
selectedEmojis.value = [];
|
||||
} else {
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id);
|
||||
selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -133,7 +133,7 @@ const add = async (ev: MouseEvent) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
emojisPaginationComponent.value.prepend(result.created);
|
||||
emojisPaginationComponent.value?.prepend(result.created);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
@ -146,12 +146,12 @@ const edit = (emoji) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
} else if (result.deleted) {
|
||||
emojisPaginationComponent.value.removeItem(emoji.id);
|
||||
emojisPaginationComponent.value?.removeItem(emoji.id);
|
||||
}
|
||||
},
|
||||
closed: () => dispose(),
|
||||
|
@ -226,7 +226,7 @@ const setCategoryBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
category: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const setLicenseBulk = async () => {
|
||||
|
@ -238,43 +238,43 @@ const setLicenseBulk = async () => {
|
|||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const addTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const removeTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const setTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
});
|
||||
if (canceled) return;
|
||||
if (canceled || result == null) return;
|
||||
await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
aliases: result.split(' '),
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const delBulk = async () => {
|
||||
|
@ -286,7 +286,7 @@ const delBulk = async () => {
|
|||
await os.apiWithDialog('admin/emoji/delete-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
emojisPaginationComponent.value?.reload();
|
||||
};
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
|
|
|
@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="windowEl"
|
||||
:initialWidth="400"
|
||||
:initialHeight="500"
|
||||
:canResize="false"
|
||||
@close="windowEl.close()"
|
||||
@closed="$emit('closed')"
|
||||
:canResize="true"
|
||||
@close="windowEl?.close()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
||||
<template v-else #header>New emoji</template>
|
||||
|
@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js';
|
|||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji?: any,
|
||||
emoji?: Misskey.entities.EmojiDetailed,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
|
||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||
const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
|
||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||
const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
|
||||
const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
|
||||
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
|
||||
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
|
||||
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||
|
@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
|
|||
|
||||
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
async function changeImage(ev) {
|
||||
async function changeImage(ev: Event) {
|
||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||
|
@ -140,7 +140,7 @@ async function addRole() {
|
|||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||
}
|
||||
|
||||
async function removeRole(role, ev) {
|
||||
async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ async function done() {
|
|||
},
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
} else {
|
||||
const created = await os.apiWithDialog('admin/emoji/add', params);
|
||||
|
||||
|
@ -180,11 +180,12 @@ async function done() {
|
|||
created: created,
|
||||
});
|
||||
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function del() {
|
||||
if (!props.emoji) return;
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.tsx.removeAreYouSure({ x: name.value }),
|
||||
|
@ -197,7 +198,7 @@ async function del() {
|
|||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
windowEl.value.close();
|
||||
windowEl.value?.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -55,13 +55,13 @@ const pagination = {
|
|||
|
||||
function accept(user) {
|
||||
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function reject(user) {
|
||||
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ function fetch() {
|
|||
return;
|
||||
}
|
||||
|
||||
let promise: Promise<any>;
|
||||
let promise: Promise<unknown>;
|
||||
|
||||
if (uri.startsWith('https://')) {
|
||||
promise = misskeyApi('ap/show', {
|
||||
|
|
|
@ -77,15 +77,15 @@ async function create() {
|
|||
|
||||
clipsCache.delete();
|
||||
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
function onClipCreated() {
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
function onClipDeleted() {
|
||||
pagingComponent.value.reload();
|
||||
pagingComponent.value?.reload();
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
|
|
@ -110,7 +110,7 @@ function addUser() {
|
|||
listId: list.value.id,
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
paginationEl.value.reload();
|
||||
paginationEl.value?.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ async function removeUser(item, ev) {
|
|||
listId: list.value.id,
|
||||
userId: item.userId,
|
||||
}).then(() => {
|
||||
paginationEl.value.removeItem(item.id);
|
||||
paginationEl.value?.removeItem(item.id);
|
||||
});
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
|
||||
<template #func>
|
||||
<button @click="choose()">
|
||||
|
@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'image' };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
|
||||
|
||||
<section style="padding: 16px;" class="_gaps_s">
|
||||
|
@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'note' };
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
|
||||
}>();
|
||||
|
||||
const id = ref<any>(props.modelValue.note);
|
||||
const id = ref(props.modelValue.note);
|
||||
const note = ref<Misskey.entities.Note | null>(null);
|
||||
|
||||
watch(id, async () => {
|
||||
if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
|
||||
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop();
|
||||
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
|
||||
}
|
||||
|
||||
if (!id.value) {
|
||||
note.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update:modelValue', {
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
|
||||
<template #func>
|
||||
<button class="_button" @click="rename()">
|
||||
|
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
|
||||
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
|
|||
|
||||
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: any,
|
||||
}>(), {
|
||||
modelValue: {},
|
||||
});
|
||||
const props = defineProps<{
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'section'; },
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const children = ref(deepClone(props.modelValue.children ?? []));
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<XContainer :draggable="true" @remove="() => $emit('remove')">
|
||||
<XContainer :draggable="true" @remove="() => emit('remove')">
|
||||
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
|
||||
|
||||
<section>
|
||||
|
@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
|
||||
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { Autocomplete } from '@/scripts/autocomplete.js';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
modelValue: Misskey.entities.PageBlock & { type: 'text' }
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
|
||||
}>();
|
||||
|
||||
let autocomplete: Autocomplete;
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)">
|
||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{element}">
|
||||
<div :class="$style.item">
|
||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||
|
|
|
@ -52,7 +52,7 @@ const props = defineProps<{
|
|||
|
||||
const scope = computed(() => props.path ? props.path.split('/') : []);
|
||||
|
||||
const keys = ref<any>(null);
|
||||
const keys = ref<[string, string][]>([]);
|
||||
|
||||
function fetchKeys() {
|
||||
misskeyApi('i/registry/keys-with-type', {
|
||||
|
|
|
@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
|
|||
|
||||
const props = defineProps<{
|
||||
game: Misskey.entities.ReversiGameDetailed;
|
||||
connection: Misskey.ChannelConnection;
|
||||
connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
|
||||
}>();
|
||||
|
||||
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
|
||||
|
@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
|
|||
game.value.user2Ready = states.user2;
|
||||
}
|
||||
|
||||
function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
|
||||
function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
|
||||
props.connection.send('updateSettings', {
|
||||
key: key,
|
||||
value: game.value[key],
|
||||
});
|
||||
}
|
||||
|
||||
function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
|
||||
function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
|
||||
if (userId === $i.id) return;
|
||||
if (game.value[key] === value) return;
|
||||
game.value[key] = value;
|
||||
|
|
|
@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
|||
const parser = new Parser();
|
||||
let aiscript: Interpreter;
|
||||
const code = ref('');
|
||||
const logs = ref<any[]>([]);
|
||||
const logs = ref<{
|
||||
id: number;
|
||||
text: string;
|
||||
print: boolean;
|
||||
}[]>([]);
|
||||
const root = ref<AsUiRoot>();
|
||||
const components = ref<Ref<AsUiComponent>[]>([]);
|
||||
const uiKey = ref(0);
|
||||
|
|
|
@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
|
|||
const backupCodes = ref<string[]>();
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function tokenDone() {
|
||||
if (token.value == null) return;
|
||||
const res = await os.apiWithDialog('i/2fa/done', {
|
||||
token: token.value.toString(),
|
||||
token: typeof token.value === 'string' ? token.value : token.value.toString(),
|
||||
});
|
||||
|
||||
backupCodes.value = res.backupCodes;
|
||||
|
@ -166,7 +167,7 @@ function downloadBackupCodes() {
|
|||
}
|
||||
|
||||
function allDone() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ function createAccount() {
|
|||
});
|
||||
}
|
||||
|
||||
async function switchAccount(account: any) {
|
||||
async function switchAccount(account: Misskey.entities.UserDetailed) {
|
||||
const fetchedAccounts = await getAccounts();
|
||||
const token = fetchedAccounts.find(x => x.id === account.id)!.token;
|
||||
switchAccountWithToken(token);
|
||||
|
|
|
@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import FormPagination from '@/components/MkPagination.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -77,7 +78,7 @@ const pagination = {
|
|||
|
||||
function revoke(token) {
|
||||
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
|
||||
list.value.reload();
|
||||
list.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
>
|
||||
<div :class="$style.name"><MkCondensedLine :minScale="0.5">{{ decoration.name }}</MkCondensedLine></div>
|
||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: decoration.url, angle, flipH, offsetX, offsetY }]" forceShowDecoration/>
|
||||
<i v-if="decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id))" :class="$style.lock" class="ti ti-lock"></i>
|
||||
<i v-if="locked" :class="$style.lock" class="ti ti-lock"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { signinRequired } from '@/account.js';
|
||||
|
||||
const $i = signinRequired();
|
||||
|
@ -37,6 +37,8 @@ const props = defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'click'): void;
|
||||
}>();
|
||||
|
||||
const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@ -67,5 +69,6 @@ const emit = defineEmits<{
|
|||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
color: var(--MI_THEME-warn);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.footer" class="_buttonsCenter">
|
||||
<MkButton v-if="usingIndex != null" primary rounded @click="update"><i class="ti ti-check"></i> {{ i18n.ts.update }}</MkButton>
|
||||
<MkButton v-if="usingIndex != null" rounded @click="detach"><i class="ti ti-x"></i> {{ i18n.ts.detach }}</MkButton>
|
||||
<MkButton v-else :disabled="exceeded" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
|
||||
<MkButton v-else :disabled="exceeded || locked" primary rounded @click="attach"><i class="ti ti-check"></i> {{ i18n.ts.attach }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
|
@ -61,6 +61,7 @@ const props = defineProps<{
|
|||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
roleIdsThatCanBeUsedThisDecoration: string[];
|
||||
};
|
||||
}>();
|
||||
|
||||
|
@ -83,6 +84,7 @@ const emit = defineEmits<{
|
|||
|
||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
|
||||
const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
|
||||
const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
|
||||
const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
|
||||
const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
|
||||
|
@ -108,7 +110,7 @@ const decorationsForPreview = computed(() => {
|
|||
});
|
||||
|
||||
function cancel() {
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
|
@ -118,7 +120,7 @@ async function update() {
|
|||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function attach() {
|
||||
|
@ -128,12 +130,12 @@ async function attach() {
|
|||
offsetX: offsetX.value,
|
||||
offsetY: offsetY.value,
|
||||
});
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
|
||||
async function detach() {
|
||||
emit('detach');
|
||||
dialog.value.close();
|
||||
dialog.value?.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -257,7 +257,7 @@ function parallaxLoop() {
|
|||
}
|
||||
|
||||
function parallax() {
|
||||
const banner = bannerEl.value as any;
|
||||
const banner = bannerEl.value;
|
||||
if (banner == null) return;
|
||||
|
||||
const top = getScrollPosition(rootEl.value);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { $i, iAmModerator } from '@/account.js';
|
|||
import MkLoading from '@/pages/_loading_.vue';
|
||||
import MkError from '@/pages/_error_.vue';
|
||||
|
||||
export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
|
||||
export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
|
||||
loader: loader,
|
||||
loadingComponent: MkLoading,
|
||||
errorComponent: MkError,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
|
||||
import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
|
||||
|
||||
import type { App, ShallowRef } from 'vue';
|
||||
|
||||
|
@ -79,7 +79,7 @@ class MainRouterProxy implements IRouter {
|
|||
return this.supplier().currentRoute;
|
||||
}
|
||||
|
||||
get navHook(): ((path: string, flag?: any) => boolean) | null {
|
||||
get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
|
||||
return this.supplier().navHook;
|
||||
}
|
||||
|
||||
|
@ -91,11 +91,11 @@ class MainRouterProxy implements IRouter {
|
|||
return this.supplier().getCurrentKey();
|
||||
}
|
||||
|
||||
getCurrentPath(): any {
|
||||
getCurrentPath(): string {
|
||||
return this.supplier().getCurrentPath();
|
||||
}
|
||||
|
||||
push(path: string, flag?: any): void {
|
||||
push(path: string, flag?: RouterFlag): void {
|
||||
this.supplier().push(path, flag);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
|
|||
export type FormItem = {
|
||||
label?: string;
|
||||
type: 'string';
|
||||
default: string | null;
|
||||
default?: string | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
|
@ -24,7 +24,7 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'number';
|
||||
default: number | null;
|
||||
default?: number | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
|
@ -32,20 +32,20 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'boolean';
|
||||
default: boolean | null;
|
||||
default?: boolean | null;
|
||||
description?: string;
|
||||
hidden?: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'enum';
|
||||
default: string | null;
|
||||
default?: string | null;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
enum: EnumItem[];
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'radio';
|
||||
default: unknown | null;
|
||||
default?: unknown | null;
|
||||
required?: boolean;
|
||||
hidden?: Hidden;
|
||||
options: {
|
||||
|
@ -55,7 +55,7 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'range';
|
||||
default: number | null;
|
||||
default?: number | null;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
step?: number;
|
||||
|
@ -66,12 +66,12 @@ export type FormItem = {
|
|||
} | {
|
||||
label?: string;
|
||||
type: 'object';
|
||||
default: Record<string, unknown> | null;
|
||||
default?: Record<string, unknown> | null;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
label?: string;
|
||||
type: 'array';
|
||||
default: unknown[] | null;
|
||||
default?: unknown[] | null;
|
||||
hidden: Hidden;
|
||||
} | {
|
||||
type: 'button';
|
||||
|
|
|
@ -17,7 +17,7 @@ export function misskeyApi<
|
|||
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||
>(
|
||||
endpoint: E,
|
||||
data: P = {} as any,
|
||||
data: P & { i?: string | null; } = {} as any,
|
||||
token?: string | null | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<_ResT> {
|
||||
|
@ -30,8 +30,8 @@ export function misskeyApi<
|
|||
|
||||
const promise = new Promise<_ResT>((resolve, reject) => {
|
||||
// Append a credential
|
||||
if ($i) (data as any).i = $i.token;
|
||||
if (token !== undefined) (data as any).i = token;
|
||||
if ($i) data.i = $i.token;
|
||||
if (token !== undefined) data.i = token;
|
||||
|
||||
// Send request
|
||||
window.fetch(`${apiUrl}/${endpoint}`, {
|
||||
|
|
|
@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
|
|||
});
|
||||
}
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
|
||||
|
||||
|
@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
|
|||
});
|
||||
}
|
||||
|
||||
export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> {
|
||||
export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
|
||||
return select(src, label, false).then(files => files[0]);
|
||||
}
|
||||
|
||||
export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
||||
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
|
||||
return select(src, label, true);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
/**
|
||||
* 配列をシャッフル (破壊的)
|
||||
*/
|
||||
export function shuffle<T extends any[]>(array: T): T {
|
||||
let currentIndex = array.length, randomIndex;
|
||||
export function shuffle<T extends unknown[]>(array: T): T {
|
||||
let currentIndex = array.length;
|
||||
let randomIndex: number;
|
||||
|
||||
// While there remain elements to shuffle.
|
||||
while (currentIndex !== 0) {
|
||||
|
|
|
@ -32,13 +32,13 @@ const mimeTypeMap = {
|
|||
|
||||
export function uploadFile(
|
||||
file: File,
|
||||
folder?: any,
|
||||
folder?: string | Misskey.entities.DriveFolder,
|
||||
name?: string,
|
||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
||||
): Promise<Misskey.entities.DriveFile> {
|
||||
if ($i == null) throw new Error('Not logged in');
|
||||
|
||||
if (folder && typeof folder === 'object') folder = folder.id;
|
||||
const _folder = typeof folder === 'string' ? folder : folder?.id;
|
||||
|
||||
if (file.size > instance.maxFileSize) {
|
||||
alert({
|
||||
|
@ -89,11 +89,11 @@ export function uploadFile(
|
|||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('i', $i.token);
|
||||
formData.append('i', $i!.token);
|
||||
formData.append('force', 'true');
|
||||
formData.append('file', resizedImage ?? file);
|
||||
formData.append('name', ctx.name);
|
||||
if (folder) formData.append('folderId', folder);
|
||||
if (_folder) formData.append('folderId', _folder);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
|
|
|
@ -11,6 +11,7 @@ import darkTheme from '@@/themes/d-green-lime.json5';
|
|||
import { miLocalStorage } from './local-storage.js';
|
||||
import type { SoundType } from '@/scripts/sound.js';
|
||||
import { Storage } from '@/pizzax.js';
|
||||
import type { Ast } from '@syuilo/aiscript';
|
||||
|
||||
interface PostFormAction {
|
||||
title: string,
|
||||
|
@ -516,7 +517,7 @@ export type Plugin = {
|
|||
token: string;
|
||||
src: string | null;
|
||||
version: string;
|
||||
ast: any[];
|
||||
ast: Ast.Node[];
|
||||
author?: string;
|
||||
description?: string;
|
||||
permissions?: string[];
|
||||
|
@ -554,13 +555,13 @@ export class ColdDeviceStorage {
|
|||
}
|
||||
|
||||
public static getAll(): Partial<typeof this.default> {
|
||||
return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => {
|
||||
return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<Partial<typeof this.default>>((acc, key) => {
|
||||
const value = localStorage.getItem(PREFIX + key);
|
||||
if (value != null) {
|
||||
acc[key] = JSON.parse(value);
|
||||
}
|
||||
return acc;
|
||||
}, {} as any);
|
||||
}, {});
|
||||
}
|
||||
|
||||
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
|
||||
|
@ -605,7 +606,7 @@ export class ColdDeviceStorage {
|
|||
get: () => {
|
||||
return valueRef.value;
|
||||
},
|
||||
set: (value: unknown) => {
|
||||
set: (value: typeof ColdDeviceStorage.default[K]) => {
|
||||
const val = value;
|
||||
ColdDeviceStorage.set(key, val);
|
||||
},
|
||||
|
|
22
packages/frontend/src/types/post-form.ts
Normal file
22
packages/frontend/src/types/post-form.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export interface PostFormProps {
|
||||
reply?: Misskey.entities.Note;
|
||||
renote?: Misskey.entities.Note;
|
||||
channel?: Misskey.entities.Channel; // TODO
|
||||
mention?: Misskey.entities.User;
|
||||
specified?: Misskey.entities.UserDetailed;
|
||||
initialText?: string;
|
||||
initialCw?: string;
|
||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||
initialFiles?: Misskey.entities.DriveFile[];
|
||||
initialLocalOnly?: boolean;
|
||||
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||
initialNote?: Misskey.entities.Note;
|
||||
instant?: boolean;
|
||||
};
|
|
@ -48,7 +48,7 @@ const fetching = ref(true);
|
|||
const key = ref(0);
|
||||
|
||||
const tick = () => {
|
||||
window.fetch(`/api/fetch-rss?url=${props.url}`, {}).then(res => {
|
||||
window.fetch(`/api/fetch-rss?url=${encodeURIComponent(props.url)}`, {}).then(res => {
|
||||
res.json().then((feed: Misskey.entities.FetchRssResponse) => {
|
||||
if (props.shuffle) {
|
||||
shuffle(feed.items);
|
||||
|
|
|
@ -68,10 +68,10 @@ const onDriveFileCreated = (file) => {
|
|||
}
|
||||
};
|
||||
|
||||
const thumbnail = (image: any): string => {
|
||||
const thumbnail = (image: Misskey.entities.DriveFile): string => {
|
||||
return defaultStore.state.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(image.url)
|
||||
: image.thumbnailUrl;
|
||||
: image.thumbnailUrl ?? image.url;
|
||||
};
|
||||
|
||||
misskeyApi('drive/stream', {
|
||||
|
|
|
@ -70,7 +70,7 @@ const items = computed(() => rawItems.value.slice(0, widgetProps.maxEntries));
|
|||
const fetching = ref(true);
|
||||
const fetchEndpoint = computed(() => {
|
||||
const url = new URL('/api/fetch-rss', base);
|
||||
url.searchParams.set('url', widgetProps.url);
|
||||
url.searchParams.set('url', encodeURIComponent(widgetProps.url));
|
||||
return url;
|
||||
});
|
||||
const intervalClear = ref<(() => void) | undefined>();
|
||||
|
|
|
@ -99,7 +99,7 @@ const items = computed(() => {
|
|||
const fetching = ref(true);
|
||||
const fetchEndpoint = computed(() => {
|
||||
const url = new URL('/api/fetch-rss', base);
|
||||
url.searchParams.set('url', widgetProps.url);
|
||||
url.searchParams.set('url', encodeURIComponent(widgetProps.url));
|
||||
return url;
|
||||
});
|
||||
const intervalClear = ref<(() => void) | undefined>();
|
||||
|
|
|
@ -121,6 +121,9 @@ type AdminAnnouncementsUpdateRequest = operations['admin___announcements___updat
|
|||
// @public (undocumented)
|
||||
type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1253,6 +1256,7 @@ declare namespace entities {
|
|||
AdminAnnouncementsListResponse,
|
||||
AdminAnnouncementsUpdateRequest,
|
||||
AdminAvatarDecorationsCreateRequest,
|
||||
AdminAvatarDecorationsCreateResponse,
|
||||
AdminAvatarDecorationsDeleteRequest,
|
||||
AdminAvatarDecorationsListRequest,
|
||||
AdminAvatarDecorationsListResponse,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2024.10.2-alpha.1",
|
||||
"version": "2024.10.2-alpha.2",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
@ -31,6 +31,7 @@ import type {
|
|||
AdminAnnouncementsListResponse,
|
||||
AdminAnnouncementsUpdateRequest,
|
||||
AdminAvatarDecorationsCreateRequest,
|
||||
AdminAvatarDecorationsCreateResponse,
|
||||
AdminAvatarDecorationsDeleteRequest,
|
||||
AdminAvatarDecorationsListRequest,
|
||||
AdminAvatarDecorationsListResponse,
|
||||
|
@ -597,7 +598,7 @@ export type Endpoints = {
|
|||
'admin/announcements/delete': { req: AdminAnnouncementsDeleteRequest; res: EmptyResponse };
|
||||
'admin/announcements/list': { req: AdminAnnouncementsListRequest; res: AdminAnnouncementsListResponse };
|
||||
'admin/announcements/update': { req: AdminAnnouncementsUpdateRequest; res: EmptyResponse };
|
||||
'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: EmptyResponse };
|
||||
'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: AdminAvatarDecorationsCreateResponse };
|
||||
'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse };
|
||||
'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse };
|
||||
'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
|
||||
|
|
|
@ -34,6 +34,7 @@ export type AdminAnnouncementsListRequest = operations['admin___announcements___
|
|||
export type AdminAnnouncementsListResponse = operations['admin___announcements___list']['responses']['200']['content']['application/json'];
|
||||
export type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
|
||||
export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
|
||||
export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
|
||||
export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
|
||||
export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json'];
|
||||
export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json'];
|
||||
|
|
|
@ -6324,9 +6324,22 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (without any results) */
|
||||
204: {
|
||||
content: never;
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
/** Format: date-time */
|
||||
updatedAt: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
roleIdsThatCanBeUsedThisDecoration: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
|
|
Loading…
Reference in a new issue