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:
@@ -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: '{}',
|
||||
})
|
||||
public roleIdsThatCanBeUsedThisDecoration: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public category: string | null;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
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;
|
||||
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;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user