1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-13 17:35:40 +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:
るちーか
2026-04-15 20:29:17 +09:00
committed by GitHub
parent c95aef7535
commit 360e805638
12 changed files with 116 additions and 20 deletions

View File

@@ -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"');
}
};

View File

@@ -36,4 +36,9 @@ export class MiAvatarDecoration {
array: true, length: 128, default: '{}',
})
public roleIdsThatCanBeUsedThisDecoration: string[];
@Column('varchar', {
length: 128, nullable: true,
})
public category: string | null;
}

View File

@@ -55,6 +55,10 @@ export const meta = {
format: 'id',
},
},
category: {
type: 'string',
optional: false, nullable: true,
},
},
},
} as const;
@@ -68,6 +72,7 @@ export const paramDef = {
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
category: { type: 'string', nullable: true },
},
required: ['name', 'description', 'url'],
} as const;
@@ -84,6 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
category: ps.category,
}, me);
return {
@@ -94,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
description: created.description,
url: created.url,
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
category: created.category,
};
});
}

View File

@@ -60,6 +60,10 @@ export const meta = {
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,
url: avatarDecoration.url,
roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
category: avatarDecoration.category,
}));
});
}

View File

@@ -30,6 +30,7 @@ export const paramDef = {
roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: {
type: 'string',
} },
category: { type: 'string', nullable: true },
},
required: ['id'],
} as const;
@@ -45,6 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
category: ps.category,
}, me);
});
}

View File

@@ -49,6 +49,10 @@ export const meta = {
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,
url: decoration.url,
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
category: decoration.category,
}));
});
}

View File

@@ -32,6 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="url">
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<MkInput v-model="category" :datalist="props.categories || []">
<template #label>{{ i18n.ts.category }}</template>
</MkInput>
<MkTextarea v-model="description">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
@@ -79,6 +82,7 @@ const $i = ensureSignin();
const props = defineProps<{
avatarDecoration?: Misskey.entities.AdminAvatarDecorationsListResponse[number],
categories?: string[],
}>();
const emit = defineEmits<{
@@ -89,6 +93,7 @@ const emit = defineEmits<{
const windowEl = useTemplateRef('windowEl');
const url = ref<string>(props.avatarDecoration ? props.avatarDecoration.url : '');
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 roleIdsThatCanBeUsedThisDecoration = ref(props.avatarDecoration ? props.avatarDecoration.roleIdsThatCanBeUsedThisDecoration : []);
const rolesThatCanBeUsedThisDecoration = ref<Misskey.entities.Role[]>([]);
@@ -118,6 +123,7 @@ async function done() {
url: url.value,
name: name.value,
description: description.value,
category: category.value,
roleIdsThatCanBeUsedThisDecoration: rolesThatCanBeUsedThisDecoration.value.map(x => x.id),
};

View File

@@ -7,18 +7,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 900px;">
<div class="_gaps">
<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/>
<MkFoldableSection v-for="category in Object.keys(groupedDecorations)" :key="category" :expanded="true">
<template #header>{{ category || i18n.ts.other }}</template>
<div :class="$style.decorations">
<div
v-for="avatarDecoration in groupedDecorations[category]"
: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>
</MkFoldableSection>
</div>
</div>
</PageWithHeader>
@@ -32,10 +35,13 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js';
const $i = ensureSignin();
const avatarDecorations = ref<Misskey.entities.AdminAvatarDecorationsListResponse>([]);
const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value));
function load() {
misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
@@ -47,6 +53,7 @@ load();
async function add(ev: PointerEvent) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
categories: Object.keys(groupedDecorations.value),
}, {
done: result => {
if (result.created) {
@@ -60,6 +67,7 @@ async function add(ev: PointerEvent) {
async function edit(avatarDecoration: Misskey.entities.AdminAvatarDecorationsListResponse[number]) {
const { dispose } = await os.popupAsyncWithDialog(import('./avatar-decoration-edit-dialog.vue').then(x => x.default), {
avatarDecoration: avatarDecoration,
categories: Object.keys(groupedDecorations.value),
}, {
done: result => {
if (result.updated) {

View File

@@ -29,15 +29,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton danger @click="detachAllDecorations">{{ i18n.ts.detachAll }}</MkButton>
</div>
<div :class="$style.decorations">
<XDecoration
v-for="avatarDecoration in avatarDecorations"
:key="avatarDecoration.id"
:decoration="avatarDecoration"
@click="openDecoration(avatarDecoration)"
/>
</div>
<MkFoldableSection v-for="category in Object.keys(groupedDecorations)" :key="category" :expanded="true">
<template #header>{{ category || i18n.ts.other }}</template>
<div :class="$style.decorations">
<XDecoration
v-for="avatarDecoration in groupedDecorations[category]"
:key="avatarDecoration.id"
:decoration="avatarDecoration"
@click="openDecoration(avatarDecoration)"
/>
</div>
</MkFoldableSection>
</div>
<div v-else>
<MkLoading/>
@@ -52,17 +54,20 @@ import * as Misskey from 'misskey-js';
import XDecoration from './avatar-decoration.decoration.vue';
import XDialog from './avatar-decoration.dialog.vue';
import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { ensureSignin } from '@/i.js';
import MkInfo from '@/components/MkInfo.vue';
import { definePage } from '@/page.js';
import { groupAvatarDecorations } from '@/utility/group-avatar-decorations.js';
const $i = ensureSignin();
const loading = ref(true);
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
const groupedDecorations = computed(() => groupAvatarDecorations(avatarDecorations.value));
misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
avatarDecorations.value = _avatarDecorations;

View 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;
}

View File

@@ -6964,6 +6964,7 @@ export interface operations {
description: string;
url: string;
roleIdsThatCanBeUsedThisDecoration?: string[];
category?: string | null;
};
};
};
@@ -6985,6 +6986,7 @@ export interface operations {
description: string;
url: string;
roleIdsThatCanBeUsedThisDecoration: string[];
category: string | null;
};
};
};
@@ -7136,6 +7138,7 @@ export interface operations {
description: string;
url: string;
roleIdsThatCanBeUsedThisDecoration: string[];
category?: string | null;
}[];
};
};
@@ -7196,6 +7199,7 @@ export interface operations {
description?: string;
url?: string;
roleIdsThatCanBeUsedThisDecoration?: string[];
category?: string | null;
};
};
};
@@ -23633,6 +23637,7 @@ export interface operations {
description: string;
url: string;
roleIdsThatCanBeUsedThisDecoration: string[];
category?: string | null;
}[];
};
};