mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-05 01:14:04 +02:00
enhance: アバターデコレーションへのカテゴリの導入 (#17034)
* feat(backend): AvatarDecorationにcategoryを追加し、関連APIのプロパティ・戻り値にも反映 * feat(frontend): アバターデコレーションのカテゴリ設定機能 * chore(frontend): 管理画面とユーザー側の画面で、アバターデコレーションのグループ化のコードをある程度統一 * CHANGELOGを更新 * fix: group-avatar-decorations.tsを使用するよう修正 * chore: コーディング規約への準拠 * 型エラーを解消
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
|
- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey)
|
||||||
- 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました
|
- 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました
|
||||||
|
- Enhance: アバターデコレーションにカテゴリを設定できるように
|
||||||
- Fix: ユーザーハッシュタグページでユーザーの読み込みが重複する問題を修正
|
- Fix: ユーザーハッシュタグページでユーザーの読み込みが重複する問題を修正
|
||||||
- 依存関係の更新
|
- 依存関係の更新
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AddCategoryToAvatarDecorations1766652173085 {
|
||||||
|
name = 'AddCategoryToAvatarDecorations1766652173085';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {QueryRunner} queryRunner
|
||||||
|
*/
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "avatar_decoration" ADD "category" character varying(128)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {QueryRunner} queryRunner
|
||||||
|
*/
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "avatar_decoration" DROP COLUMN "category"');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -36,4 +36,9 @@ export class MiAvatarDecoration {
|
|||||||
array: true, length: 128, default: '{}',
|
array: true, length: 128, default: '{}',
|
||||||
})
|
})
|
||||||
public roleIdsThatCanBeUsedThisDecoration: string[];
|
public roleIdsThatCanBeUsedThisDecoration: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128, nullable: true,
|
||||||
|
})
|
||||||
|
public category: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export const meta = {
|
|||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@@ -68,6 +72,7 @@ export const paramDef = {
|
|||||||
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
|
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
category: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
required: ['name', 'description', 'url'],
|
required: ['name', 'description', 'url'],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -84,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
description: ps.description,
|
description: ps.description,
|
||||||
url: ps.url,
|
url: ps.url,
|
||||||
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
|
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
|
||||||
|
category: ps.category,
|
||||||
}, me);
|
}, me);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -94,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
description: created.description,
|
description: created.description,
|
||||||
url: created.url,
|
url: created.url,
|
||||||
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
|
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
|
||||||
|
category: created.category,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ export const meta = {
|
|||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -95,6 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
description: avatarDecoration.description,
|
description: avatarDecoration.description,
|
||||||
url: avatarDecoration.url,
|
url: avatarDecoration.url,
|
||||||
roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
|
roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
|
||||||
|
category: avatarDecoration.category,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const paramDef = {
|
|||||||
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
|
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
category: { type: 'string', nullable: true },
|
||||||
},
|
},
|
||||||
required: ['id'],
|
required: ['id'],
|
||||||
} as const;
|
} as const;
|
||||||
@@ -45,6 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
description: ps.description,
|
description: ps.description,
|
||||||
url: ps.url,
|
url: ps.url,
|
||||||
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
|
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
|
||||||
|
category: ps.category,
|
||||||
}, me);
|
}, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ export const meta = {
|
|||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -76,6 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
description: decoration.description,
|
description: decoration.description,
|
||||||
url: decoration.url,
|
url: decoration.url,
|
||||||
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
|
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
|
||||||
|
category: decoration.category,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkInput v-model="url">
|
<MkInput v-model="url">
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
<MkInput v-model="category" :datalist="props.categories || []">
|
||||||
|
<template #label>{{ i18n.ts.category }}</template>
|
||||||
|
</MkInput>
|
||||||
<MkTextarea v-model="description">
|
<MkTextarea v-model="description">
|
||||||
<template #label>{{ i18n.ts.description }}</template>
|
<template #label>{{ i18n.ts.description }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
@@ -79,6 +82,7 @@ const $i = ensureSignin();
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
avatarDecoration?: Misskey.entities.AdminAvatarDecorationsListResponse[number],
|
avatarDecoration?: Misskey.entities.AdminAvatarDecorationsListResponse[number],
|
||||||
|
categories?: string[],
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -89,6 +93,7 @@ const emit = defineEmits<{
|
|||||||
const windowEl = useTemplateRef('windowEl');
|
const windowEl = useTemplateRef('windowEl');
|
||||||
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
|
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
|
||||||
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
|
const name = ref<string>(props.avatarDecoration ? props.avatarDecoration.name : '');
|
||||||
|
const category = ref<string>(props.avatarDecoration?.category ? props.avatarDecoration.category : '');
|
||||||
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
|
const description = ref<string>(props.avatarDecoration ? props.avatarDecoration.description : '');
|
||||||
const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
|
const roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
|
||||||
const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
|
const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
|
||||||
@@ -118,6 +123,7 @@ async function done() {
|
|||||||
url: url.value,
|
url: url.value,
|
||||||
name: name.value,
|
name: name.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
|
category: category.value,
|
||||||
roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
|
roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
|
||||||
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
<div class="_spacer" style="--MI_SPACER-w: 900px;">
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
|
<MkFoldableSection v-for="category in Object.keys(groupedDecorations)" :key="category" :expanded="true">
|
||||||
|
<template #header>{{ category || i18n.ts.other }}</template>
|
||||||
<div :class="$style.decorations">
|
<div :class="$style.decorations">
|
||||||
<div
|
<div
|
||||||
v-for="avatarDecoration in avatarDecorations"
|
v-for="avatarDecoration in groupedDecorations[category]"
|
||||||
:key="avatarDecoration.id"
|
:key="avatarDecoration.id"
|
||||||
v-panel
|
v-panel
|
||||||
:class="$style.decoration"
|
:class="$style.decoration"
|
||||||
@@ -19,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
|
<MkAvatar style="width: 60px; height: 60px;" :user="$i" :decorations="[{ url: avatarDecoration.url }]" forceShowDecoration/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageWithHeader>
|
</PageWithHeader>
|
||||||
@@ -32,10 +35,13 @@ import * as os from '@/os.js';
|
|||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
|
import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
|
||||||
|
const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value));
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
|
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
|
||||||
@@ -47,6 +53,7 @@ load();
|
|||||||
|
|
||||||
async function add(ev: PointerEvent) {
|
async function add(ev: PointerEvent) {
|
||||||
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
|
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
|
||||||
|
categories: Object.keys(groupedDecorations.value),
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (result.created) {
|
if (result.created) {
|
||||||
@@ -60,6 +67,7 @@ async function add(ev: PointerEvent) {
|
|||||||
async function edit(avatarDecoration: Misskey.entities.AdminAvatarDecorationsListResponse[number]) {
|
async function edit(avatarDecoration: Misskey.entities.AdminAvatarDecorationsListResponse[number]) {
|
||||||
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
|
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
|
||||||
avatarDecoration: avatarDecoration,
|
avatarDecoration: avatarDecoration,
|
||||||
|
categories: Object.keys(groupedDecorations.value),
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (result.updated) {
|
if (result.updated) {
|
||||||
|
|||||||
@@ -29,15 +29,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
|
<MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
<MkFoldableSection v-for="category in Object.keys(groupedDecorations)" :key="category" :expanded="true">
|
||||||
|
<template #header>{{ category || i18n.ts.other }}</template>
|
||||||
<div :class="$style.decorations">
|
<div :class="$style.decorations">
|
||||||
<XDecoration
|
<XDecoration
|
||||||
v-for="avatarDecoration in avatarDecorations"
|
v-for="avatarDecoration in groupedDecorations[category]"
|
||||||
:key="avatarDecoration.id"
|
:key="avatarDecoration.id"
|
||||||
:decoration="avatarDecoration"
|
:decoration="avatarDecoration"
|
||||||
@click="openDecoration(avatarDecoration)"
|
@click="openDecoration(avatarDecoration)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<MkLoading/>
|
<MkLoading/>
|
||||||
@@ -52,17 +54,20 @@ import * as Misskey from 'misskey-js';
|
|||||||
import XDecoration from './avatar-decoration.decoration.vue';
|
import XDecoration from './avatar-decoration.decoration.vue';
|
||||||
import XDialog from './avatar-decoration.dialog.vue';
|
import XDialog from './avatar-decoration.dialog.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { ensureSignin } from '@/i.js';
|
import { ensureSignin } from '@/i.js';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
import { definePage } from '@/page.js';
|
import { definePage } from '@/page.js';
|
||||||
|
import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
|
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
|
||||||
|
const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value));
|
||||||
|
|
||||||
misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
|
misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
|
||||||
avatarDecorations.value = _avatarDecorations;
|
avatarDecorations.value = _avatarDecorations;
|
||||||
|
|||||||
25
packages/frontend/src/utility/group-avatar-decorations.ts
Normal file
25
packages/frontend/src/utility/group-avatar-decorations.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
type AvatarDecorationBase = { category?: string | null | undefined };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アバターデコレーションをカテゴリごとにグループ化します。
|
||||||
|
* @param decorations アバターデコレーションの配列
|
||||||
|
* @returns カテゴリごとにグループ化されたアバターデコレーションオブジェクト
|
||||||
|
*/
|
||||||
|
export function groupAvatarDecorations<T extends AvatarDecorationBase>(decorations: T[]) {
|
||||||
|
const grouped: Record<string, T[]> = {};
|
||||||
|
|
||||||
|
for (const decoration of decorations) {
|
||||||
|
const category = decoration.category ?? '';
|
||||||
|
if (!(category in grouped)) {
|
||||||
|
grouped[category] = [];
|
||||||
|
}
|
||||||
|
grouped[category].push(decoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
@@ -6964,6 +6964,7 @@ export interface operations {
|
|||||||
description: string;
|
description: string;
|
||||||
url: string;
|
url: string;
|
||||||
roleIdsThatCanBeUsedThisDecoration?: string[];
|
roleIdsThatCanBeUsedThisDecoration?: string[];
|
||||||
|
category?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -6985,6 +6986,7 @@ export interface operations {
|
|||||||
description: string;
|
description: string;
|
||||||
url: string;
|
url: string;
|
||||||
roleIdsThatCanBeUsedThisDecoration: string[];
|
roleIdsThatCanBeUsedThisDecoration: string[];
|
||||||
|
category: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -7136,6 +7138,7 @@ export interface operations {
|
|||||||
description: string;
|
description: string;
|
||||||
url: string;
|
url: string;
|
||||||
roleIdsThatCanBeUsedThisDecoration: string[];
|
roleIdsThatCanBeUsedThisDecoration: string[];
|
||||||
|
category?: string | null;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -7196,6 +7199,7 @@ export interface operations {
|
|||||||
description?: string;
|
description?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
roleIdsThatCanBeUsedThisDecoration?: string[];
|
roleIdsThatCanBeUsedThisDecoration?: string[];
|
||||||
|
category?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -23633,6 +23637,7 @@ export interface operations {
|
|||||||
description: string;
|
description: string;
|
||||||
url: string;
|
url: string;
|
||||||
roleIdsThatCanBeUsedThisDecoration: string[];
|
roleIdsThatCanBeUsedThisDecoration: string[];
|
||||||
|
category?: string | null;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user