parent
868c8fffb3
commit
6a3039f7b7
12 changed files with 107 additions and 12 deletions
|
@ -11,6 +11,7 @@ You should also include the user name that made the change.
|
|||
## 13.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
- ロールにアイコンを設定してユーザー名の横に表示できるように
|
||||
|
||||
### Bugfixes
|
||||
-
|
||||
|
|
|
@ -1184,7 +1184,7 @@ _role:
|
|||
description: "ロールの説明"
|
||||
permission: "ロールの権限"
|
||||
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
|
||||
assignTarget: "アサインターゲット"
|
||||
assignTarget: "アサイン"
|
||||
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
||||
manual: "マニュアル"
|
||||
conditional: "コンディショナル"
|
||||
|
@ -1197,6 +1197,9 @@ _role:
|
|||
baseRole: "ベースロール"
|
||||
useBaseValue: "ベースロールの値を使用"
|
||||
chooseRoleToAssign: "アサインするロールを選択"
|
||||
iconUrl: "アイコン画像のURL"
|
||||
asBadge: "バッジとして表示"
|
||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
||||
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
||||
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
|
||||
priority: "優先度"
|
||||
|
|
13
packages/backend/migration/1675557528704-role-icon-badge.js
Normal file
13
packages/backend/migration/1675557528704-role-icon-badge.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export class roleIconBadge1675557528704 {
|
||||
name = 'roleIconBadge1675557528704'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD "iconUrl" character varying(512)`);
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD "asBadge" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "asBadge"`);
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "iconUrl"`);
|
||||
}
|
||||
}
|
|
@ -202,6 +202,19 @@ export class RoleService implements OnApplicationShutdown {
|
|||
return [...assignedRoles, ...matchedCondRoles];
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定ユーザーのバッジロール一覧取得
|
||||
*/
|
||||
@bindThis
|
||||
public async getUserBadgeRoles(userId: User['id']) {
|
||||
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
||||
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
||||
// コンディショナルロールも含めるのは負荷高そうだから一旦無し
|
||||
return assignedBadgeRoles;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
|
|
@ -56,11 +56,13 @@ export class RoleEntityService {
|
|||
name: role.name,
|
||||
description: role.description,
|
||||
color: role.color,
|
||||
iconUrl: role.iconUrl,
|
||||
target: role.target,
|
||||
condFormula: role.condFormula,
|
||||
isPublic: role.isPublic,
|
||||
isAdministrator: role.isAdministrator,
|
||||
isModerator: role.isModerator,
|
||||
asBadge: role.asBadge,
|
||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||
policies: policies,
|
||||
usersCount: assigns.length,
|
||||
|
|
|
@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit {
|
|||
} : undefined) : undefined,
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
// パフォーマンス上の理由でローカルユーザーのみ
|
||||
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
}))) : undefined,
|
||||
|
||||
...(opts.detail ? {
|
||||
url: profile!.url,
|
||||
|
@ -454,6 +459,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
iconUrl: role.iconUrl,
|
||||
description: role.description,
|
||||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
|
|
|
@ -102,6 +102,11 @@ export class Role {
|
|||
})
|
||||
public color: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public iconUrl: string | null;
|
||||
|
||||
@Column('enum', {
|
||||
enum: ['manual', 'conditional'],
|
||||
default: 'manual',
|
||||
|
@ -118,6 +123,12 @@ export class Role {
|
|||
})
|
||||
public isPublic: boolean;
|
||||
|
||||
// trueの場合ユーザー名の横にバッジとして表示
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public asBadge: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
@ -19,11 +19,13 @@ export const paramDef = {
|
|||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
asBadge: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
policies: {
|
||||
type: 'object',
|
||||
|
@ -33,11 +35,13 @@ export const paramDef = {
|
|||
'name',
|
||||
'description',
|
||||
'color',
|
||||
'iconUrl',
|
||||
'target',
|
||||
'condFormula',
|
||||
'isPublic',
|
||||
'isModerator',
|
||||
'isAdministrator',
|
||||
'asBadge',
|
||||
'canEditMembersByModerator',
|
||||
'policies',
|
||||
],
|
||||
|
@ -64,11 +68,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
name: ps.name,
|
||||
description: ps.description,
|
||||
color: ps.color,
|
||||
iconUrl: ps.iconUrl,
|
||||
target: ps.target,
|
||||
condFormula: ps.condFormula,
|
||||
isPublic: ps.isPublic,
|
||||
isAdministrator: ps.isAdministrator,
|
||||
isModerator: ps.isModerator,
|
||||
asBadge: ps.asBadge,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
policies: ps.policies,
|
||||
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
|
|
@ -27,11 +27,13 @@ export const paramDef = {
|
|||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
asBadge: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
policies: {
|
||||
type: 'object',
|
||||
|
@ -42,11 +44,13 @@ export const paramDef = {
|
|||
'name',
|
||||
'description',
|
||||
'color',
|
||||
'iconUrl',
|
||||
'target',
|
||||
'condFormula',
|
||||
'isPublic',
|
||||
'isModerator',
|
||||
'isAdministrator',
|
||||
'asBadge',
|
||||
'canEditMembersByModerator',
|
||||
'policies',
|
||||
],
|
||||
|
@ -73,11 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
name: ps.name,
|
||||
description: ps.description,
|
||||
color: ps.color,
|
||||
iconUrl: ps.iconUrl,
|
||||
target: ps.target,
|
||||
condFormula: ps.condFormula,
|
||||
isPublic: ps.isPublic,
|
||||
isModerator: ps.isModerator,
|
||||
isAdministrator: ps.isAdministrator,
|
||||
asBadge: ps.asBadge,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
policies: ps.policies,
|
||||
});
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
|
||||
</div>
|
||||
<div :class="$style.info">
|
||||
<MkA :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
|
@ -77,4 +80,17 @@ defineProps<{
|
|||
margin-left: auto;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.badgeRoles {
|
||||
margin: 0 .5em 0 0;
|
||||
}
|
||||
|
||||
.badgeRole {
|
||||
height: 1.3em;
|
||||
vertical-align: -20%;
|
||||
|
||||
& + .badgeRole {
|
||||
margin-left: .125em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
<template #caption>#RRGGBB</template>
|
||||
</MkInput>
|
||||
|
||||
<MkInput v-model="iconUrl">
|
||||
<template #label>{{ i18n.ts._role.iconUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkSelect v-model="rolePermission" :readonly="readonly">
|
||||
<template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template>
|
||||
<template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template>
|
||||
|
@ -35,6 +39,21 @@
|
|||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="isPublic" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.isPublic }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="asBadge" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.asBadge }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<FormSlot>
|
||||
<template #label><i class="ti ti-license"></i> {{ i18n.ts._role.policies }}</template>
|
||||
<div class="_gaps_s">
|
||||
|
@ -358,16 +377,6 @@
|
|||
</div>
|
||||
</FormSlot>
|
||||
|
||||
<MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="isPublic" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.isPublic }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<div v-if="!readonly" class="_buttons">
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ role ? i18n.ts.save : i18n.ts.create }}</MkButton>
|
||||
</div>
|
||||
|
@ -426,9 +435,11 @@ let name = $ref(role?.name ?? 'New Role');
|
|||
let description = $ref(role?.description ?? '');
|
||||
let rolePermission = $ref(role?.isAdministrator ? 'administrator' : role?.isModerator ? 'moderator' : 'normal');
|
||||
let color = $ref(role?.color ?? null);
|
||||
let iconUrl = $ref(role?.iconUrl ?? null);
|
||||
let target = $ref(role?.target ?? 'manual');
|
||||
let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
|
||||
let isPublic = $ref(role?.isPublic ?? false);
|
||||
let asBadge = $ref(role?.asBadge ?? false);
|
||||
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
|
||||
|
||||
const policies = reactive<Record<typeof ROLE_POLICIES[number], { useDefault: boolean; priority: number; value: any; }>>({});
|
||||
|
@ -466,11 +477,13 @@ async function save() {
|
|||
name,
|
||||
description,
|
||||
color: color === '' ? null : color,
|
||||
iconUrl: iconUrl === '' ? null : iconUrl,
|
||||
target,
|
||||
condFormula,
|
||||
isAdministrator: rolePermission === 'administrator',
|
||||
isModerator: rolePermission === 'moderator',
|
||||
isPublic,
|
||||
asBadge,
|
||||
canEditMembersByModerator,
|
||||
policies,
|
||||
});
|
||||
|
@ -480,11 +493,13 @@ async function save() {
|
|||
name,
|
||||
description,
|
||||
color: color === '' ? null : color,
|
||||
iconUrl: iconUrl === '' ? null : iconUrl,
|
||||
target,
|
||||
condFormula,
|
||||
isAdministrator: rolePermission === 'administrator',
|
||||
isModerator: rolePermission === 'moderator',
|
||||
isPublic,
|
||||
asBadge,
|
||||
canEditMembersByModerator,
|
||||
policies,
|
||||
});
|
||||
|
|
|
@ -39,7 +39,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="user.roles.length > 0" class="roles">
|
||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">{{ role.name }}</span>
|
||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
|
||||
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
|
||||
{{ role.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<MkOmit>
|
||||
|
|
Loading…
Reference in a new issue