From 870f7608beca17d4360cab33c76d0bba95729ef0 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Wed, 22 Feb 2023 14:43:18 +0900 Subject: [PATCH] =?UTF-8?q?enhance:=20explore=E3=81=A7=E5=85=AC=E9=96=8B?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=AB=E4=B8=80=E8=A6=A7=E3=81=A8=E3=81=9D?= =?UTF-8?q?=E3=81=AE=E3=83=A1=E3=83=B3=E3=83=90=E3=83=BC=E3=82=92=E9=96=B2?= =?UTF-8?q?=E8=A6=A7=E3=81=A7=E3=81=8D=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 --- .../src/core/entities/RoleEntityService.ts | 15 +--- .../backend/src/server/api/EndpointsModule.ts | 16 +++++ packages/backend/src/server/api/endpoints.ts | 8 +++ .../server/api/endpoints/admin/roles/list.ts | 2 +- .../server/api/endpoints/admin/roles/show.ts | 4 +- .../server/api/endpoints/admin/roles/users.ts | 71 +++++++++++++++++++ .../server/api/endpoints/admin/show-user.ts | 2 +- .../src/server/api/endpoints/roles/list.ts | 37 ++++++++++ .../src/server/api/endpoints/roles/show.ts | 52 ++++++++++++++ .../src/server/api/endpoints/roles/users.ts | 71 +++++++++++++++++++ .../frontend/src/components/MkRolePreview.vue | 19 +++-- .../frontend/src/components/MkUserList.vue | 11 +-- .../frontend/src/pages/admin/roles.role.vue | 36 ++++++++-- packages/frontend/src/pages/admin/roles.vue | 2 +- packages/frontend/src/pages/explore.roles.vue | 22 ++++++ packages/frontend/src/pages/explore.users.vue | 18 ++--- packages/frontend/src/pages/explore.vue | 13 +++- packages/frontend/src/pages/role.vue | 47 ++++++++++++ packages/frontend/src/pages/user-info.vue | 2 +- packages/frontend/src/router.ts | 3 + 20 files changed, 405 insertions(+), 46 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/admin/roles/users.ts create mode 100644 packages/backend/src/server/api/endpoints/roles/list.ts create mode 100644 packages/backend/src/server/api/endpoints/roles/show.ts create mode 100644 packages/backend/src/server/api/endpoints/roles/users.ts create mode 100644 packages/frontend/src/pages/explore.roles.vue create mode 100644 packages/frontend/src/pages/role.vue diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index f2ba64237..80ef5ac1f 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -25,14 +25,7 @@ export class RoleEntityService { public async pack( src: Role['id'] | Role, me?: { id: User['id'] } | null | undefined, - options?: { - detail?: boolean; - }, ) { - const opts = Object.assign({ - detail: true, - }, options); - const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src }); const assigns = await this.roleAssignmentsRepository.findBy({ @@ -65,9 +58,6 @@ export class RoleEntityService { canEditMembersByModerator: role.canEditMembersByModerator, policies: policies, usersCount: assigns.length, - ...(opts.detail ? { - users: this.userEntityService.packMany(assigns.map(x => x.userId), me), - } : {}), }); } @@ -75,11 +65,8 @@ export class RoleEntityService { public packMany( roles: any[], me: { id: User['id'] }, - options?: { - detail?: boolean; - }, ) { - return Promise.all(roles.map(x => this.pack(x, me, options))); + return Promise.all(roles.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index d27f926ef..d3e2219bd 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -66,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; +import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -277,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; import * as ep___ping from './endpoints/ping.js'; import * as ep___pinnedUsers from './endpoints/pinned-users.js'; import * as ep___promo_read from './endpoints/promo/read.js'; +import * as ep___roles_list from './endpoints/roles/list.js'; +import * as ep___roles_show from './endpoints/roles/show.js'; +import * as ep___roles_users from './endpoints/roles/users.js'; import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; import * as ep___resetDb from './endpoints/reset-db.js'; import * as ep___resetPassword from './endpoints/reset-password.js'; @@ -383,6 +387,7 @@ const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useCla const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default }; const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default }; +const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default }; const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default }; @@ -594,6 +599,9 @@ const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___ const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default }; const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default }; const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default }; +const $roles_list: Provider = { provide: 'ep:roles/list', useClass: ep___roles_list.default }; +const $roles_show: Provider = { provide: 'ep:roles/show', useClass: ep___roles_show.default }; +const $roles_users: Provider = { provide: 'ep:roles/users', useClass: ep___roles_users.default }; const $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default }; const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default }; const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.default }; @@ -704,6 +712,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_roles_assign, $admin_roles_unassign, $admin_roles_updateDefaultPolicies, + $admin_roles_users, $announcements, $antennas_create, $antennas_delete, @@ -915,6 +924,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $ping, $pinnedUsers, $promo_read, + $roles_list, + $roles_show, + $roles_users, $requestResetPassword, $resetDb, $resetPassword, @@ -1019,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_roles_assign, $admin_roles_unassign, $admin_roles_updateDefaultPolicies, + $admin_roles_users, $announcements, $antennas_create, $antennas_delete, @@ -1230,6 +1243,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $ping, $pinnedUsers, $promo_read, + $roles_list, + $roles_show, + $roles_users, $requestResetPassword, $resetDb, $resetPassword, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f7f2b1f37..4d5ed9fb6 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -66,6 +66,7 @@ import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js'; +import * as ep___admin_roles_users from './endpoints/admin/roles/users.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -277,6 +278,9 @@ import * as ep___flash_myLikes from './endpoints/flash/my-likes.js'; import * as ep___ping from './endpoints/ping.js'; import * as ep___pinnedUsers from './endpoints/pinned-users.js'; import * as ep___promo_read from './endpoints/promo/read.js'; +import * as ep___roles_list from './endpoints/roles/list.js'; +import * as ep___roles_show from './endpoints/roles/show.js'; +import * as ep___roles_users from './endpoints/roles/users.js'; import * as ep___requestResetPassword from './endpoints/request-reset-password.js'; import * as ep___resetDb from './endpoints/reset-db.js'; import * as ep___resetPassword from './endpoints/reset-password.js'; @@ -381,6 +385,7 @@ const eps = [ ['admin/roles/assign', ep___admin_roles_assign], ['admin/roles/unassign', ep___admin_roles_unassign], ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies], + ['admin/roles/users', ep___admin_roles_users], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], @@ -592,6 +597,9 @@ const eps = [ ['ping', ep___ping], ['pinned-users', ep___pinnedUsers], ['promo/read', ep___promo_read], + ['roles/list', ep___roles_list], + ['roles/show', ep___roles_show], + ['roles/users', ep___roles_users], ['request-reset-password', ep___requestResetPassword], ['reset-db', ep___resetDb], ['reset-password', ep___resetPassword], diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts index ac56de56b..edaf638ea 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts @@ -32,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { const roles = await this.rolesRepository.find({ order: { lastUsedAt: 'DESC' }, }); - return await this.roleEntityService.packMany(roles, me, { detail: false }); + return await this.roleEntityService.packMany(roles, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts index c83f96191..01028a086 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/show.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts @@ -39,12 +39,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { private roleEntityService: RoleEntityService, ) { - super(meta, paramDef, async (ps) => { + super(meta, paramDef, async (ps, me) => { const role = await this.rolesRepository.findOneBy({ id: ps.roleId }); if (role == null) { throw new ApiError(meta.errors.noSuchRole); } - return await this.roleEntityService.pack(role); + return await this.roleEntityService.pack(role, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts new file mode 100644 index 000000000..bb016a842 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts @@ -0,0 +1,71 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin', 'role', 'users'], + + requireCredential: false, + requireAdmin: true, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '224eff5e-2488-4b18-b3e7-f50d94421648', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, + required: ['roleId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private queryService: QueryService, + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ + id: ps.roleId, + }); + + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) + .andWhere('assign.roleId = :roleId', { roleId: role.id }) + .innerJoinAndSelect('assign.user', 'user'); + + const assigns = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(assigns.map(async assign => ({ + id: assign.id, + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), + }))); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 823af6d8b..b25a9deb6 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { moderationNote: profile.moderationNote, signins, policies: await this.roleService.getUserPolicies(user.id), - roles: await this.roleEntityService.packMany(roles, me, { detail: false }), + roles: await this.roleEntityService.packMany(roles, me), }; }); } diff --git a/packages/backend/src/server/api/endpoints/roles/list.ts b/packages/backend/src/server/api/endpoints/roles/list.ts new file mode 100644 index 000000000..d61c6b8dc --- /dev/null +++ b/packages/backend/src/server/api/endpoints/roles/list.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { RolesRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; + +export const meta = { + tags: ['role'], + + requireCredential: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [ + ], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const roles = await this.rolesRepository.findBy({ + isPublic: true, + }); + return await this.roleEntityService.packMany(roles, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/roles/show.ts b/packages/backend/src/server/api/endpoints/roles/show.ts new file mode 100644 index 000000000..cc755dcc7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/roles/show.ts @@ -0,0 +1,52 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { RolesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { RoleEntityService } from '@/core/entities/RoleEntityService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['role', 'users'], + + requireCredential: false, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: 'de5502bf-009a-4639-86c1-fec349e46dcb', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + }, + required: ['roleId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + private roleEntityService: RoleEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ + id: ps.roleId, + isPublic: true, + }); + + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + return await this.roleEntityService.pack(role, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts new file mode 100644 index 000000000..6e221b6c6 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/roles/users.ts @@ -0,0 +1,71 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueryService } from '@/core/QueryService.js'; +import { DI } from '@/di-symbols.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['role', 'users'], + + requireCredential: false, + + errors: { + noSuchRole: { + message: 'No such role.', + code: 'NO_SUCH_ROLE', + id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + roleId: { type: 'string', format: 'misskey:id' }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + }, + required: ['roleId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { + constructor( + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private queryService: QueryService, + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const role = await this.rolesRepository.findOneBy({ + id: ps.roleId, + isPublic: true, + }); + + if (role == null) { + throw new ApiError(meta.errors.noSuchRole); + } + + const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId) + .andWhere('assign.roleId = :roleId', { roleId: role.id }) + .innerJoinAndSelect('assign.user', 'user'); + + const assigns = await query + .take(ps.limit) + .getMany(); + + return await Promise.all(assigns.map(async assign => ({ + id: assign.id, + user: await this.userEntityService.pack(assign.user!, me, { detail: true }), + }))); + }); + } +} diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index 8c1d7af19..2f5866f34 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -1,10 +1,15 @@ <template> -<MkA v-adaptive-bg :to="`/admin/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }"> +<MkA v-adaptive-bg :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }"> <div :class="$style.title"> <span :class="$style.icon"> - <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i> - <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i> - <i v-else class="ti ti-user" style="opacity: 0.7;"></i> + <template v-if="role.iconUrl"> + <img :class="$style.badge" :src="role.iconUrl"/> + </template> + <template v-else> + <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i> + <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i> + <i v-else class="ti ti-user" style="opacity: 0.7;"></i> + </template> </span> <span :class="$style.name">{{ role.name }}</span> <span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span> @@ -20,6 +25,7 @@ import { i18n } from '@/i18n'; const props = defineProps<{ role: any; + forModeration: boolean; }>(); </script> @@ -38,6 +44,11 @@ const props = defineProps<{ margin-right: 8px; } +.badge { + height: 1.3em; + vertical-align: -20%; +} + .name { font-weight: bold; } diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue index dd683fcc2..51eb426e9 100644 --- a/packages/frontend/src/components/MkUserList.vue +++ b/packages/frontend/src/components/MkUserList.vue @@ -7,9 +7,9 @@ </div> </template> - <template #default="{ items: users }"> + <template #default="{ items }"> <div class="efvhhmdq"> - <MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/> + <MkUserInfo v-for="item in items" :key="item.id" class="user" :user="extractor(item)"/> </div> </template> </MkPagination> @@ -20,10 +20,13 @@ import MkUserInfo from '@/components/MkUserInfo.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ pagination: Paging; noGap?: boolean; -}>(); + extractor?: (item: any) => any; +}>(), { + extractor: (item) => item, +}); </script> <style lang="scss" scoped> diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index 0365165b5..1b9f0e7c5 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -16,16 +16,29 @@ <MkFolder v-if="role.target === 'manual'" default-open> <template #icon><i class="ti ti-users"></i></template> <template #label>{{ i18n.ts.users }}</template> - <template #suffix>{{ role.users.length }}</template> + <template #suffix>{{ role.usersCount }}</template> <div class="_gaps"> <MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> - <div v-for="user in role.users" :key="user.id" :class="$style.userItem"> - <MkA :class="$style.user" :to="`/user-info/${user.id}`"> - <MkUserCardMini :user="user"/> - </MkA> - <button class="_button" :class="$style.unassign" @click="unassign(user, $event)"><i class="ti ti-x"></i></button> - </div> + <MkPagination :pagination="usersPagination"> + <template #empty> + <div class="_fullinfo"> + <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> + <div>{{ i18n.ts.noUsers }}</div> + </div> + </template> + + <template #default="{ items }"> + <div class="_gaps_s"> + <div v-for="item in items" :key="item.user.id" :class="$style.userItem"> + <MkA :class="$style.user" :to="`/user-info/${item.user.id}`"> + <MkUserCardMini :user="item.user"/> + </MkA> + <button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button> + </div> + </div> + </template> + </MkPagination> </div> </MkFolder> <MkInfo v-else>{{ i18n.ts._role.isConditionalRole }}</MkInfo> @@ -47,6 +60,7 @@ import { useRouter } from '@/router'; import MkButton from '@/components/MkButton.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkInfo from '@/components/MkInfo.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; const router = useRouter(); @@ -54,6 +68,14 @@ const props = defineProps<{ id?: string; }>(); +const usersPagination = { + endpoint: 'admin/roles/users' as const, + limit: 20, + params: computed(() => ({ + roleId: props.id, + })), +}; + const role = reactive(await os.api('admin/roles/show', { roleId: props.id, })); diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 19a0f6617..d89f0d2a7 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -133,7 +133,7 @@ </div> </MkFolder> <div class="_gaps_s"> - <MkRolePreview v-for="role in roles" :key="role.id" :role="role"/> + <MkRolePreview v-for="role in roles" :key="role.id" :role="role" :for-moderation="true"/> </div> </div> </MkSpacer> diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue new file mode 100644 index 000000000..8be11008c --- /dev/null +++ b/packages/frontend/src/pages/explore.roles.vue @@ -0,0 +1,22 @@ +<template> +<MkSpacer :content-max="1200"> + <div class="_gaps_s"> + <MkRolePreview v-for="role in roles" :key="role.id" :role="role" :for-moderation="false"/> + </div> +</MkSpacer> +</template> + +<script lang="ts" setup> +import { } from 'vue'; +import MkRolePreview from '@/components/MkRolePreview.vue'; +import * as os from '@/os'; + +let roles = $ref(); + +os.api('roles/list', { + limit: 30, +}).then(res => { + roles = res; +}); +</script> + diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index 4d6ac7d71..05a36c950 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -8,19 +8,19 @@ <template v-if="tag == null"> <MkFoldableSection class="_margin" persist-key="explore-pinned-users"> <template #header><i class="ti ti-bookmark ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedUsers }}</template> - <XUserList :pagination="pinnedUsers"/> + <MkUserList :pagination="pinnedUsers"/> </MkFoldableSection> <MkFoldableSection class="_margin" persist-key="explore-popular-users"> <template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> - <XUserList :pagination="popularUsers"/> + <MkUserList :pagination="popularUsers"/> </MkFoldableSection> <MkFoldableSection class="_margin" persist-key="explore-recently-updated-users"> <template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template> - <XUserList :pagination="recentlyUpdatedUsers"/> + <MkUserList :pagination="recentlyUpdatedUsers"/> </MkFoldableSection> <MkFoldableSection class="_margin" persist-key="explore-recently-registered-users"> <template #header><i class="ti ti-plus ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyRegisteredUsers }}</template> - <XUserList :pagination="recentlyRegisteredUsers"/> + <MkUserList :pagination="recentlyRegisteredUsers"/> </MkFoldableSection> </template> </div> @@ -36,21 +36,21 @@ <MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin"> <template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template> - <XUserList :pagination="tagUsers"/> + <MkUserList :pagination="tagUsers"/> </MkFoldableSection> <template v-if="tag == null"> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template> - <XUserList :pagination="popularUsersF"/> + <MkUserList :pagination="popularUsersF"/> </MkFoldableSection> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template> - <XUserList :pagination="recentlyUpdatedUsersF"/> + <MkUserList :pagination="recentlyUpdatedUsersF"/> </MkFoldableSection> <MkFoldableSection class="_margin"> <template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template> - <XUserList :pagination="recentlyRegisteredUsersF"/> + <MkUserList :pagination="recentlyRegisteredUsersF"/> </MkFoldableSection> </template> </div> @@ -59,7 +59,7 @@ <script lang="ts" setup> import { watch } from 'vue'; -import XUserList from '@/components/MkUserList.vue'; +import MkUserList from '@/components/MkUserList.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkTab from '@/components/MkTab.vue'; import * as os from '@/os'; diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue index dd1685f27..0ed0a7ebc 100644 --- a/packages/frontend/src/pages/explore.vue +++ b/packages/frontend/src/pages/explore.vue @@ -8,6 +8,9 @@ <div v-else-if="tab === 'users'"> <XUsers/> </div> + <div v-else-if="tab === 'roles'"> + <XRoles/> + </div> <div v-else-if="tab === 'search'"> <MkSpacer :content-max="1200"> <div> @@ -22,7 +25,7 @@ </MkRadios> </div> - <XUserList v-if="searchQuery" ref="searchEl" class="_margin" :pagination="searchPagination"/> + <MkUserList v-if="searchQuery" ref="searchEl" class="_margin" :pagination="searchPagination"/> </MkSpacer> </div> </div> @@ -33,12 +36,13 @@ import { computed, watch } from 'vue'; import XFeatured from './explore.featured.vue'; import XUsers from './explore.users.vue'; +import XRoles from './explore.roles.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkInput from '@/components/MkInput.vue'; import MkRadios from '@/components/MkRadios.vue'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; -import XUserList from '@/components/MkUserList.vue'; +import MkUserList from '@/components/MkUserList.vue'; const props = withDefaults(defineProps<{ tag?: string; @@ -75,8 +79,13 @@ const headerTabs = $computed(() => [{ key: 'users', icon: 'ti ti-users', title: i18n.ts.users, +}, { + key: 'roles', + icon: 'ti ti-badges', + title: i18n.ts.roles, }, { key: 'search', + icon: 'ti ti-search', title: i18n.ts.search, }]); diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue new file mode 100644 index 000000000..2e9d3d616 --- /dev/null +++ b/packages/frontend/src/pages/role.vue @@ -0,0 +1,47 @@ +<template> +<MkStickyContainer> + <template #header><MkPageHeader/></template> + + <MkSpacer :content-max="1200"> + <div class="_gaps_s"> + <div v-if="role">{{ role.description }}</div> + <MkUserList :pagination="users" :extractor="(item) => item.user"/> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { computed, watch } from 'vue'; +import * as os from '@/os'; +import MkUserList from '@/components/MkUserList.vue'; +import { definePageMetadata } from '@/scripts/page-metadata'; + +const props = defineProps<{ + role: string; +}>(); + +let role = $ref(); + +watch(() => props.role, () => { + os.api('roles/show', { + roleId: props.role, + }).then(res => { + role = res; + }); +}, { immediate: true }); + +const users = $computed(() => ({ + endpoint: 'roles/users' as const, + limit: 30, + params: { + roleId: props.role, + }, +})); + +definePageMetadata(computed(() => ({ + title: role?.name, + icon: 'ti ti-badge', +}))); +</script> + diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue index b8ed5787c..13a06286f 100644 --- a/packages/frontend/src/pages/user-info.vue +++ b/packages/frontend/src/pages/user-info.vue @@ -112,7 +112,7 @@ <MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem"> - <MkRolePreview :class="$style.role" :role="role"/> + <MkRolePreview :class="$style.role" :role="role" :for-moderation="true"/> <button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button> <button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button> </div> diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index ff380df63..3b7ee1486 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -197,6 +197,9 @@ export const routes = [{ path: '/theme-editor', component: page(() => import('./pages/theme-editor.vue')), loginRequired: true, +}, { + path: '/roles/:role', + component: page(() => import('./pages/role.vue')), }, { path: '/explore/tags/:tag', component: page(() => import('./pages/explore.vue')),