From 7c2d8d73ce3dc407a57925562b48357e54c315e6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 12 Mar 2023 16:38:08 +0900
Subject: [PATCH] =?UTF-8?q?enhance:=20=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=AE?=
 =?UTF-8?q?=E4=B8=A6=E3=81=B3=E9=A0=86=E3=82=92=E8=A8=AD=E5=AE=9A=E5=8F=AF?=
 =?UTF-8?q?=E8=83=BD=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                          |  1 +
 locales/ja-JP.yml                                     |  3 +++
 .../migration/1678602320354-role-display-order.js     | 11 +++++++++++
 .../backend/src/core/entities/RoleEntityService.ts    |  1 +
 .../backend/src/core/entities/UserEntityService.ts    |  6 ++++--
 packages/backend/src/models/entities/Role.ts          |  6 ++++++
 .../src/server/api/endpoints/admin/roles/create.ts    |  3 +++
 .../src/server/api/endpoints/admin/roles/update.ts    |  3 +++
 packages/frontend/src/pages/admin/roles.edit.vue      |  1 +
 packages/frontend/src/pages/admin/roles.editor.vue    |  6 ++++++
 packages/frontend/src/pages/explore.roles.vue         |  8 +++-----
 11 files changed, 42 insertions(+), 7 deletions(-)
 create mode 100644 packages/backend/migration/1678602320354-role-display-order.js

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b6c79571d..48e8c975cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ You should also include the user name that made the change.
 ### Improvements
 - ユーザーごとにRenoteをミュートできるように
 - ノートごとに絵文字リアクションを受け取るか設定できるように
+- ロールの並び順を設定可能に
 - enhance(client): DM作成時にメンションも含むように
 - enhance(client): フォロー申請のボタンのデザインを改善
 - enhance(backend): OpenAPIエンドポイントを復旧
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 1ecd35ff04..b7ff05e458 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -969,6 +969,7 @@ cannotBeChangedLater: "後から変更できません。"
 reactionAcceptance: "リアクションの受け入れ"
 likeOnly: "いいねのみ"
 likeOnlyForRemote: "リモートからはいいねのみ"
+rolesAssignedToMe: "自分に割り当てられたロール"
 
 _achievements:
   earnedAt: "獲得日時"
@@ -1230,6 +1231,8 @@ _role:
   iconUrl: "アイコン画像のURL"
   asBadge: "バッジとして表示"
   descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
+  displayOrder: "表示順"
+  descriptionOfDisplayOrder: "数値が大きいほどUI上で先頭に表示されます。"
   canEditMembersByModerator: "モデレーターのメンバー編集を許可"
   descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
   priority: "優先度"
diff --git a/packages/backend/migration/1678602320354-role-display-order.js b/packages/backend/migration/1678602320354-role-display-order.js
new file mode 100644
index 0000000000..de8f6f1033
--- /dev/null
+++ b/packages/backend/migration/1678602320354-role-display-order.js
@@ -0,0 +1,11 @@
+export class roleDisplayOrder1678602320354 {
+    name = 'roleDisplayOrder1678602320354'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "role" ADD "displayOrder" integer NOT NULL DEFAULT '0'`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "displayOrder"`);
+    }
+}
diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts
index 2f1d51fa1a..e111a10b77 100644
--- a/packages/backend/src/core/entities/RoleEntityService.ts
+++ b/packages/backend/src/core/entities/RoleEntityService.ts
@@ -61,6 +61,7 @@ export class RoleEntityService {
 			isModerator: role.isModerator,
 			asBadge: role.asBadge,
 			canEditMembersByModerator: role.canEditMembersByModerator,
+			displayOrder: role.displayOrder,
 			policies: policies,
 			usersCount: assignedCount,
 		});
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 53353e916d..068ffad09d 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -390,9 +390,10 @@ export class UserEntityService implements OnModuleInit {
 			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 => ({
+			badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.sort((a, b) => b.displayOrder - a.displayOrder).map(r => ({
 				name: r.name,
 				iconUrl: r.iconUrl,
+				displayOrder: r.displayOrder,
 			}))) : undefined,
 
 			...(opts.detail ? {
@@ -429,7 +430,7 @@ export class UserEntityService implements OnModuleInit {
 						userId: user.id,
 					}).then(result => result >= 1)
 					: false,
-				roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).map(role => ({
+				roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
 					id: role.id,
 					name: role.name,
 					color: role.color,
@@ -437,6 +438,7 @@ export class UserEntityService implements OnModuleInit {
 					description: role.description,
 					isModerator: role.isModerator,
 					isAdministrator: role.isAdministrator,
+					displayOrder: role.displayOrder,
 				}))),
 			} : {}),
 
diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts
index 399e9ead05..85ff266740 100644
--- a/packages/backend/src/models/entities/Role.ts
+++ b/packages/backend/src/models/entities/Role.ts
@@ -144,6 +144,12 @@ export class Role {
 	})
 	public canEditMembersByModerator: boolean;
 
+	// UIに表示する際の並び順用(大きいほど先頭)
+	@Column('integer', {
+		default: 0,
+	})
+	public displayOrder: number;
+
 	@Column('jsonb', {
 		default: { },
 	})
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
index df60c6be94..1359894634 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
@@ -27,6 +27,7 @@ export const paramDef = {
 		isAdministrator: { type: 'boolean' },
 		asBadge: { type: 'boolean' },
 		canEditMembersByModerator: { type: 'boolean' },
+		displayOrder: { type: 'number' },
 		policies: {
 			type: 'object',
 		},
@@ -43,6 +44,7 @@ export const paramDef = {
 		'isAdministrator',
 		'asBadge',
 		'canEditMembersByModerator',
+		'displayOrder',
 		'policies',
 	],
 } as const;
@@ -76,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				isModerator: ps.isModerator,
 				asBadge: ps.asBadge,
 				canEditMembersByModerator: ps.canEditMembersByModerator,
+				displayOrder: ps.displayOrder,
 				policies: ps.policies,
 			}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
 	
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index b939ccdbf9..37b68c4c41 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -35,6 +35,7 @@ export const paramDef = {
 		isAdministrator: { type: 'boolean' },
 		asBadge: { type: 'boolean' },
 		canEditMembersByModerator: { type: 'boolean' },
+		displayOrder: { type: 'number' },
 		policies: {
 			type: 'object',
 		},
@@ -52,6 +53,7 @@ export const paramDef = {
 		'isAdministrator',
 		'asBadge',
 		'canEditMembersByModerator',
+		'displayOrder',
 		'policies',
 	],
 } as const;
@@ -85,6 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				isAdministrator: ps.isAdministrator,
 				asBadge: ps.asBadge,
 				canEditMembersByModerator: ps.canEditMembersByModerator,
+				displayOrder: ps.displayOrder,
 				policies: ps.policies,
 			});
 			const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index ac6cca84c1..e6896237f8 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -55,6 +55,7 @@ if (props.id) {
 		isPublic: false,
 		asBadge: false,
 		canEditMembersByModerator: false,
+		displayOrder: 0,
 		policies: {},
 	};
 }
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 2fb605f8c0..408bbc6460 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -17,6 +17,11 @@
 		<template #label>{{ i18n.ts._role.iconUrl }}</template>
 	</MkInput>
 
+	<MkInput v-model="role.displayOrder" type="number">
+		<template #label>{{ i18n.ts._role.displayOrder }}</template>
+		<template #caption>{{ i18n.ts._role.descriptionOfDisplayOrder }}</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>
@@ -444,6 +449,7 @@ const save = throttle(100, () => {
 		description: role.description,
 		color: role.color === '' ? null : role.color,
 		iconUrl: role.iconUrl === '' ? null : role.iconUrl,
+		displayOrder: role.displayOrder,
 		target: role.target,
 		condFormula: role.condFormula,
 		isAdministrator: role.isAdministrator,
diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue
index 51177d079c..6ac469f7ba 100644
--- a/packages/frontend/src/pages/explore.roles.vue
+++ b/packages/frontend/src/pages/explore.roles.vue
@@ -1,5 +1,5 @@
 <template>
-<MkSpacer :content-max="1200">
+<MkSpacer :content-max="700">
 	<div class="_gaps_s">
 		<MkRolePreview v-for="role in roles" :key="role.id" :role="role" :for-moderation="false"/>
 	</div>
@@ -13,10 +13,8 @@ import * as os from '@/os';
 
 let roles = $ref();
 
-os.api('roles/list', {
-	limit: 30,
-}).then(res => {
-	roles = res.filter(x => x.target === 'manual');
+os.api('roles/list').then(res => {
+	roles = res.filter(x => x.target === 'manual').sort((a, b) => b.displayOrder - a.displayOrder);
 });
 </script>