From 518b3e2f7315af24d7b106bfdfd40a56c4cd6d8c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 15 Jan 2023 19:10:39 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=AE=E5=90=84?=
 =?UTF-8?q?=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AB=E5=84=AA?=
 =?UTF-8?q?=E5=85=88=E5=BA=A6=E3=82=92=E8=A8=AD=E5=AE=9A=E3=81=A7=E3=81=8D?=
 =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                             |   5 +
 packages/backend/src/core/RoleService.ts      |  45 ++--
 .../src/core/entities/RoleEntityService.ts    |   1 +
 packages/backend/src/models/entities/Role.ts  |   1 +
 .../frontend/src/pages/admin/roles.editor.vue | 222 ++++++++++--------
 5 files changed, 160 insertions(+), 114 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a0802dd68c..79856447a0 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -957,6 +957,11 @@ _role:
   chooseRoleToAssign: "アサインするロールを選択"
   canEditMembersByModerator: "モデレーターのメンバー編集を許可"
   descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
+  priority: "優先度"
+  _priority:
+    low: "低"
+    middle: "中"
+    high: "高"
   _options:
     gtlAvailable: "グローバルタイムラインの閲覧"
     ltlAvailable: "ローカルタイムラインの閲覧"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 9fd612c96e..42b477d9ed 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -203,27 +203,36 @@ export class RoleService implements OnApplicationShutdown {
 
 		const roles = await this.getUserRoles(userId);
 
-		function getOptionValues(option: keyof RoleOptions) {
-			if (roles.length === 0) return [baseRoleOptions[option]];
-			return roles.map(role => (role.options[option] && (role.options[option].useDefault !== true)) ? role.options[option].value : baseRoleOptions[option]);
+		function calc<T extends keyof RoleOptions>(name: T, aggregate: (values: RoleOptions[T][]) => RoleOptions[T]) {
+			if (roles.length === 0) return baseRoleOptions[name];
+
+			const options = roles.map(role => role.options[name] ?? { priority: 0, useDefault: true });
+
+			const p2 = options.filter(option => option.priority === 2);
+			if (p2.length > 0) return aggregate(p2.map(option => option.useDefault ? baseRoleOptions[name] : option.value));
+
+			const p1 = options.filter(option => option.priority === 1);
+			if (p1.length > 0) return aggregate(p2.map(option => option.useDefault ? baseRoleOptions[name] : option.value));
+
+			return aggregate(options.map(option => option.useDefault ? baseRoleOptions[name] : option.value));
 		}
 
 		return {
-			gtlAvailable: getOptionValues('gtlAvailable').some(x => x === true),
-			ltlAvailable: getOptionValues('ltlAvailable').some(x => x === true),
-			canPublicNote: getOptionValues('canPublicNote').some(x => x === true),
-			canInvite: getOptionValues('canInvite').some(x => x === true),
-			canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true),
-			driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
-			pinLimit: Math.max(...getOptionValues('pinLimit')),
-			antennaLimit: Math.max(...getOptionValues('antennaLimit')),
-			wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')),
-			webhookLimit: Math.max(...getOptionValues('webhookLimit')),
-			clipLimit: Math.max(...getOptionValues('clipLimit')),
-			noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')),
-			userListLimit: Math.max(...getOptionValues('userListLimit')),
-			userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')),
-			rateLimitFactor: Math.max(...getOptionValues('rateLimitFactor')),
+			gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
+			ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
+			canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
+			canInvite: calc('canInvite', vs => vs.some(v => v === true)),
+			canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
+			driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
+			pinLimit: calc('pinLimit', vs => Math.max(...vs)),
+			antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
+			wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
+			webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
+			clipLimit: calc('clipLimit', vs => Math.max(...vs)),
+			noteEachClipsLimit: calc('noteEachClipsLimit', vs => Math.max(...vs)),
+			userListLimit: calc('userListLimit', vs => Math.max(...vs)),
+			userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
+			rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
 		};
 	}
 
diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts
index 7db7ed6695..6a14775653 100644
--- a/packages/backend/src/core/entities/RoleEntityService.ts
+++ b/packages/backend/src/core/entities/RoleEntityService.ts
@@ -44,6 +44,7 @@ export class RoleEntityService {
 		for (const [k, v] of Object.entries(DEFAULT_ROLE)) {
 			if (roleOptions[k] == null) roleOptions[k] = {
 				useDefault: true,
+				priority: 0,
 				value: v,
 			};
 		}
diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts
index a18df40d0c..d8d203493b 100644
--- a/packages/backend/src/models/entities/Role.ts
+++ b/packages/backend/src/models/entities/Role.ts
@@ -138,6 +138,7 @@ export class Role {
 	})
 	public options: Record<string, {
 		useDefault: boolean;
+		priority: number;
 		value: any;
 	}>;
 }
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index bd166c9d48..0f67cec0b2 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -40,189 +40,235 @@
 		<div class="_gaps_s">
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
-				<template #suffix>{{ options_rateLimitFactor_useDefault ? i18n.ts._role.useBaseValue : `${Math.floor(options_rateLimitFactor_value * 100)}%` }}</template>
+				<template #suffix>{{ options.rateLimitFactor.useDefault ? i18n.ts._role.useBaseValue : `${Math.floor(options.rateLimitFactor.value * 100)}%` }} <i :class="getPriorityIcon(options.rateLimitFactor)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_rateLimitFactor_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.rateLimitFactor.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkRange :model-value="options_rateLimitFactor_value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor_value = (v / 100)">
+					<MkRange :model-value="options.rateLimitFactor.value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options.rateLimitFactor.value = (v / 100)">
+						<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
 						<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
 					</MkRange>
+					<MkRange v-model="options.rateLimitFactor.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
-				<template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+				<template #suffix>{{ options.gtlAvailable.useDefault ? i18n.ts._role.useBaseValue : (options.gtlAvailable.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(options.gtlAvailable)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_gtlAvailable_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.gtlAvailable.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkSwitch v-model="options_gtlAvailable_value" :disabled="options_gtlAvailable_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.gtlAvailable.value" :disabled="options.gtlAvailable.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts.enable }}</template>
 					</MkSwitch>
+					<MkRange v-model="options.gtlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
-				<template #suffix>{{ options_ltlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_ltlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+				<template #suffix>{{ options.ltlAvailable.useDefault ? i18n.ts._role.useBaseValue : (options.ltlAvailable.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(options.ltlAvailable)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_ltlAvailable_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.ltlAvailable.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkSwitch v-model="options_ltlAvailable_value" :disabled="options_ltlAvailable_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.ltlAvailable.value" :disabled="options.ltlAvailable.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts.enable }}</template>
 					</MkSwitch>
+					<MkRange v-model="options.ltlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
-				<template #suffix>{{ options_canPublicNote_useDefault ? i18n.ts._role.useBaseValue : (options_canPublicNote_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+				<template #suffix>{{ options.canPublicNote.useDefault ? i18n.ts._role.useBaseValue : (options.canPublicNote.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(options.canPublicNote)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_canPublicNote_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.canPublicNote.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkSwitch v-model="options_canPublicNote_value" :disabled="options_canPublicNote_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.canPublicNote.value" :disabled="options.canPublicNote.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts.enable }}</template>
 					</MkSwitch>
+					<MkRange v-model="options.canPublicNote.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.canInvite }}</template>
-				<template #suffix>{{ options_canInvite_useDefault ? i18n.ts._role.useBaseValue : (options_canInvite_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+				<template #suffix>{{ options.canInvite.useDefault ? i18n.ts._role.useBaseValue : (options.canInvite.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(options.canInvite)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_canInvite_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.canInvite.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkSwitch v-model="options_canInvite_value" :disabled="options_canInvite_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.canInvite.value" :disabled="options.canInvite.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts.enable }}</template>
 					</MkSwitch>
+					<MkRange v-model="options.canInvite.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
-				<template #suffix>{{ options_canManageCustomEmojis_useDefault ? i18n.ts._role.useBaseValue : (options_canManageCustomEmojis_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+				<template #suffix>{{ options.canManageCustomEmojis.useDefault ? i18n.ts._role.useBaseValue : (options.canManageCustomEmojis.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(options.canManageCustomEmojis)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_canManageCustomEmojis_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.canManageCustomEmojis.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkSwitch v-model="options_canManageCustomEmojis_value" :disabled="options_canManageCustomEmojis_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.canManageCustomEmojis.value" :disabled="options.canManageCustomEmojis.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts.enable }}</template>
 					</MkSwitch>
+					<MkRange v-model="options.canManageCustomEmojis.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
-				<template #suffix>{{ options_driveCapacityMb_useDefault ? i18n.ts._role.useBaseValue : (options_driveCapacityMb_value + 'MB') }}</template>
+				<template #suffix>{{ options.driveCapacityMb.useDefault ? i18n.ts._role.useBaseValue : (options.driveCapacityMb.value + 'MB') }} <i :class="getPriorityIcon(options.driveCapacityMb)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_driveCapacityMb_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.driveCapacityMb.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_driveCapacityMb_value" :disabled="options_driveCapacityMb_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.driveCapacityMb.value" :disabled="options.driveCapacityMb.useDefault" type="number" :readonly="readonly">
 						<template #suffix>MB</template>
 					</MkInput>
+					<MkRange v-model="options.driveCapacityMb.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.pinMax }}</template>
-				<template #suffix>{{ options_pinLimit_useDefault ? i18n.ts._role.useBaseValue : (options_pinLimit_value) }}</template>
+				<template #suffix>{{ options.pinLimit.useDefault ? i18n.ts._role.useBaseValue : (options.pinLimit.value) }} <i :class="getPriorityIcon(options.pinLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_pinLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.pinLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_pinLimit_value" :disabled="options_pinLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.pinLimit.value" :disabled="options.pinLimit.useDefault" type="number" :readonly="readonly">
 					</MkInput>
+					<MkRange v-model="options.pinLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
-				<template #suffix>{{ options_antennaLimit_useDefault ? i18n.ts._role.useBaseValue : (options_antennaLimit_value) }}</template>
+				<template #suffix>{{ options.antennaLimit.useDefault ? i18n.ts._role.useBaseValue : (options.antennaLimit.value) }} <i :class="getPriorityIcon(options.antennaLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_antennaLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.antennaLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_antennaLimit_value" :disabled="options_antennaLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.antennaLimit.value" :disabled="options.antennaLimit.useDefault" type="number" :readonly="readonly">
 					</MkInput>
+					<MkRange v-model="options.antennaLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
-				<template #suffix>{{ options_wordMuteLimit_useDefault ? i18n.ts._role.useBaseValue : (options_wordMuteLimit_value) }}</template>
+				<template #suffix>{{ options.wordMuteLimit.useDefault ? i18n.ts._role.useBaseValue : (options.wordMuteLimit.value) }} <i :class="getPriorityIcon(options.wordMuteLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_wordMuteLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.wordMuteLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_wordMuteLimit_value" :disabled="options_wordMuteLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.wordMuteLimit.value" :disabled="options.wordMuteLimit.useDefault" type="number" :readonly="readonly">
 						<template #suffix>chars</template>
 					</MkInput>
+					<MkRange v-model="options.wordMuteLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
-				<template #suffix>{{ options_webhookLimit_useDefault ? i18n.ts._role.useBaseValue : (options_webhookLimit_value) }}</template>
+				<template #suffix>{{ options.webhookLimit.useDefault ? i18n.ts._role.useBaseValue : (options.webhookLimit.value) }} <i :class="getPriorityIcon(options.webhookLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_webhookLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.webhookLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_webhookLimit_value" :disabled="options_webhookLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.webhookLimit.value" :disabled="options.webhookLimit.useDefault" type="number" :readonly="readonly">
 					</MkInput>
+					<MkRange v-model="options.webhookLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.clipMax }}</template>
-				<template #suffix>{{ options_clipLimit_useDefault ? i18n.ts._role.useBaseValue : (options_clipLimit_value) }}</template>
+				<template #suffix>{{ options.clipLimit.useDefault ? i18n.ts._role.useBaseValue : (options.clipLimit.value) }} <i :class="getPriorityIcon(options.clipLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_clipLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.clipLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_clipLimit_value" :disabled="options_clipLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.clipLimit.value" :disabled="options.clipLimit.useDefault" type="number" :readonly="readonly">
 					</MkInput>
+					<MkRange v-model="options.clipLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
-				<template #suffix>{{ options_noteEachClipsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_noteEachClipsLimit_value) }}</template>
+				<template #suffix>{{ options.noteEachClipsLimit.useDefault ? i18n.ts._role.useBaseValue : (options.noteEachClipsLimit.value) }} <i :class="getPriorityIcon(options.noteEachClipsLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_noteEachClipsLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.noteEachClipsLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_noteEachClipsLimit_value" :disabled="options_noteEachClipsLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.noteEachClipsLimit.value" :disabled="options.noteEachClipsLimit.useDefault" type="number" :readonly="readonly">
 					</MkInput>
+					<MkRange v-model="options.noteEachClipsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.userListMax }}</template>
-				<template #suffix>{{ options_userListLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userListLimit_value) }}</template>
+				<template #suffix>{{ options.userListLimit.useDefault ? i18n.ts._role.useBaseValue : (options.userListLimit.value) }} <i :class="getPriorityIcon(options.userListLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_userListLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.userListLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_userListLimit_value" :disabled="options_userListLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.userListLimit.value" :disabled="options.userListLimit.useDefault" type="number" :readonly="readonly">
 					</MkInput>
+					<MkRange v-model="options.userListLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 
 			<MkFolder>
 				<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
-				<template #suffix>{{ options_userEachUserListsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userEachUserListsLimit_value) }}</template>
+				<template #suffix>{{ options.userEachUserListsLimit.useDefault ? i18n.ts._role.useBaseValue : (options.userEachUserListsLimit.value) }} <i :class="getPriorityIcon(options.userEachUserListsLimit)"></i></template>
 				<div class="_gaps">
-					<MkSwitch v-model="options_userEachUserListsLimit_useDefault" :readonly="readonly">
+					<MkSwitch v-model="options.userEachUserListsLimit.useDefault" :readonly="readonly">
 						<template #label>{{ i18n.ts._role.useBaseValue }}</template>
 					</MkSwitch>
-					<MkInput v-model="options_userEachUserListsLimit_value" :disabled="options_userEachUserListsLimit_useDefault" type="number" :readonly="readonly">
+					<MkInput v-model="options.userEachUserListsLimit.value" :disabled="options.userEachUserListsLimit.useDefault" type="number" :readonly="readonly">
 					</MkInput>
+					<MkRange v-model="options.userEachUserListsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
+						<template #label>{{ i18n.ts._role.priority }}</template>
+					</MkRange>
 				</div>
 			</MkFolder>
 		</div>
@@ -245,7 +291,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, reactive, watch } from 'vue';
 import { v4 as uuid } from 'uuid';
 import RolesEditorFormula from './RolesEditorFormula.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -260,6 +306,24 @@ import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { instance } from '@/instance';
 
+const ROLE_OPTIONS = [
+	'gtlAvailable',
+	'ltlAvailable',
+	'canPublicNote',
+	'canInvite',
+	'canManageCustomEmojis',
+	'driveCapacityMb',
+	'pinLimit',
+	'antennaLimit',
+	'wordMuteLimit',
+	'webhookLimit',
+	'clipLimit',
+	'noteEachClipsLimit',
+	'userListLimit',
+	'userEachUserListsLimit',
+	'rateLimitFactor',
+] as const;
+
 const emit = defineEmits<{
 	(ev: 'created', payload: any): void;
 	(ev: 'updated'): void;
@@ -280,36 +344,16 @@ let target = $ref(role?.target ?? 'manual');
 let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
 let isPublic = $ref(role?.isPublic ?? false);
 let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
-let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true);
-let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? instance.baseRole.gtlAvailable);
-let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true);
-let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? instance.baseRole.ltlAvailable);
-let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true);
-let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? instance.baseRole.canPublicNote);
-let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true);
-let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? instance.baseRole.canInvite);
-let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true);
-let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? instance.baseRole.canManageCustomEmojis);
-let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true);
-let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? instance.baseRole.driveCapacityMb);
-let options_pinLimit_useDefault = $ref(role?.options?.pinLimit?.useDefault ?? true);
-let options_pinLimit_value = $ref(role?.options?.pinLimit?.value ?? instance.baseRole.pinLimit);
-let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true);
-let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? instance.baseRole.antennaLimit);
-let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
-let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? instance.baseRole.wordMuteLimit);
-let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true);
-let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? instance.baseRole.webhookLimit);
-let options_clipLimit_useDefault = $ref(role?.options?.clipLimit?.useDefault ?? true);
-let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? instance.baseRole.clipLimit);
-let options_noteEachClipsLimit_useDefault = $ref(role?.options?.noteEachClipsLimit?.useDefault ?? true);
-let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? instance.baseRole.noteEachClipsLimit);
-let options_userListLimit_useDefault = $ref(role?.options?.userListLimit?.useDefault ?? true);
-let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? instance.baseRole.userListLimit);
-let options_userEachUserListsLimit_useDefault = $ref(role?.options?.userEachUserListsLimit?.useDefault ?? true);
-let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? instance.baseRole.userEachUserListsLimit);
-let options_rateLimitFactor_useDefault = $ref(role?.options?.rateLimitFactor?.useDefault ?? true);
-let options_rateLimitFactor_value = $ref(role?.options?.rateLimitFactor?.value ?? instance.baseRole.rateLimitFactor);
+
+const options = reactive<Record<typeof ROLE_OPTIONS[number], { useDefault: boolean; priority: number; value: any; }>>({});
+for (const ROLE_OPTION of ROLE_OPTIONS) {
+	const _options = role?.options ?? {};
+	options[ROLE_OPTION] = {
+		useDefault: _options[ROLE_OPTION]?.useDefault ?? true,
+		priority: _options[ROLE_OPTION]?.priority ?? 0,
+		value: _options[ROLE_OPTION]?.value ?? instance.baseRole[ROLE_OPTION],
+	};
+}
 
 if (_DEV_) {
 	watch($$(condFormula), () => {
@@ -317,24 +361,10 @@ if (_DEV_) {
 	}, { deep: true });
 }
 
-function getOptions() {
-	return {
-		gtlAvailable: { useDefault: options_gtlAvailable_useDefault, value: options_gtlAvailable_value },
-		ltlAvailable: { useDefault: options_ltlAvailable_useDefault, value: options_ltlAvailable_value },
-		canPublicNote: { useDefault: options_canPublicNote_useDefault, value: options_canPublicNote_value },
-		canInvite: { useDefault: options_canInvite_useDefault, value: options_canInvite_value },
-		canManageCustomEmojis: { useDefault: options_canManageCustomEmojis_useDefault, value: options_canManageCustomEmojis_value },
-		driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
-		pinLimit: { useDefault: options_pinLimit_useDefault, value: options_pinLimit_value },
-		antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
-		wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value },
-		webhookLimit: { useDefault: options_webhookLimit_useDefault, value: options_webhookLimit_value },
-		clipLimit: { useDefault: options_clipLimit_useDefault, value: options_clipLimit_value },
-		noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value },
-		userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value },
-		userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value },
-		rateLimitFactor: { useDefault: options_rateLimitFactor_useDefault, value: options_rateLimitFactor_value },
-	};
+function getPriorityIcon(option) {
+	if (option.priority === 2) return 'ti ti-arrows-up';
+	if (option.priority === 1) return 'ti ti-arrow-narrow-up';
+	return 'ti ti-point';
 }
 
 async function save() {
@@ -351,7 +381,7 @@ async function save() {
 			isModerator: rolePermission === 'moderator',
 			isPublic,
 			canEditMembersByModerator,
-			options: getOptions(),
+			options,
 		});
 		emit('updated');
 	} else {
@@ -365,7 +395,7 @@ async function save() {
 			isModerator: rolePermission === 'moderator',
 			isPublic,
 			canEditMembersByModerator,
-			options: getOptions(),
+			options,
 		});
 		emit('created', created);
 	}