mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-28 14:30:39 +01:00
enhance: exploreで公開ロール一覧とそのメンバーを閲覧できるように
This commit is contained in:
parent
69869307bf
commit
870f7608be
20 changed files with 405 additions and 46 deletions
|
@ -25,14 +25,7 @@ export class RoleEntityService {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Role['id'] | Role,
|
src: Role['id'] | Role,
|
||||||
me?: { id: User['id'] } | null | undefined,
|
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 role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
const assigns = await this.roleAssignmentsRepository.findBy({
|
const assigns = await this.roleAssignmentsRepository.findBy({
|
||||||
|
@ -65,9 +58,6 @@ export class RoleEntityService {
|
||||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||||
policies: policies,
|
policies: policies,
|
||||||
usersCount: assigns.length,
|
usersCount: assigns.length,
|
||||||
...(opts.detail ? {
|
|
||||||
users: this.userEntityService.packMany(assigns.map(x => x.userId), me),
|
|
||||||
} : {}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,11 +65,8 @@ export class RoleEntityService {
|
||||||
public packMany(
|
public packMany(
|
||||||
roles: any[],
|
roles: any[],
|
||||||
me: { id: User['id'] },
|
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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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_assign from './endpoints/admin/roles/assign.js';
|
||||||
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.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_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___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
import * as ep___antennas_delete from './endpoints/antennas/delete.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___ping from './endpoints/ping.js';
|
||||||
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
||||||
import * as ep___promo_read from './endpoints/promo/read.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___requestResetPassword from './endpoints/request-reset-password.js';
|
||||||
import * as ep___resetDb from './endpoints/reset-db.js';
|
import * as ep___resetDb from './endpoints/reset-db.js';
|
||||||
import * as ep___resetPassword from './endpoints/reset-password.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_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_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_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 $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
|
||||||
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.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 };
|
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 $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
|
||||||
const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.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 $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 $requestResetPassword: Provider = { provide: 'ep:request-reset-password', useClass: ep___requestResetPassword.default };
|
||||||
const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
|
const $resetDb: Provider = { provide: 'ep:reset-db', useClass: ep___resetDb.default };
|
||||||
const $resetPassword: Provider = { provide: 'ep:reset-password', useClass: ep___resetPassword.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_assign,
|
||||||
$admin_roles_unassign,
|
$admin_roles_unassign,
|
||||||
$admin_roles_updateDefaultPolicies,
|
$admin_roles_updateDefaultPolicies,
|
||||||
|
$admin_roles_users,
|
||||||
$announcements,
|
$announcements,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
$antennas_delete,
|
$antennas_delete,
|
||||||
|
@ -915,6 +924,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$ping,
|
$ping,
|
||||||
$pinnedUsers,
|
$pinnedUsers,
|
||||||
$promo_read,
|
$promo_read,
|
||||||
|
$roles_list,
|
||||||
|
$roles_show,
|
||||||
|
$roles_users,
|
||||||
$requestResetPassword,
|
$requestResetPassword,
|
||||||
$resetDb,
|
$resetDb,
|
||||||
$resetPassword,
|
$resetPassword,
|
||||||
|
@ -1019,6 +1031,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_roles_assign,
|
$admin_roles_assign,
|
||||||
$admin_roles_unassign,
|
$admin_roles_unassign,
|
||||||
$admin_roles_updateDefaultPolicies,
|
$admin_roles_updateDefaultPolicies,
|
||||||
|
$admin_roles_users,
|
||||||
$announcements,
|
$announcements,
|
||||||
$antennas_create,
|
$antennas_create,
|
||||||
$antennas_delete,
|
$antennas_delete,
|
||||||
|
@ -1230,6 +1243,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$ping,
|
$ping,
|
||||||
$pinnedUsers,
|
$pinnedUsers,
|
||||||
$promo_read,
|
$promo_read,
|
||||||
|
$roles_list,
|
||||||
|
$roles_show,
|
||||||
|
$roles_users,
|
||||||
$requestResetPassword,
|
$requestResetPassword,
|
||||||
$resetDb,
|
$resetDb,
|
||||||
$resetPassword,
|
$resetPassword,
|
||||||
|
|
|
@ -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_assign from './endpoints/admin/roles/assign.js';
|
||||||
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.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_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___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
import * as ep___antennas_delete from './endpoints/antennas/delete.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___ping from './endpoints/ping.js';
|
||||||
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
|
||||||
import * as ep___promo_read from './endpoints/promo/read.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___requestResetPassword from './endpoints/request-reset-password.js';
|
||||||
import * as ep___resetDb from './endpoints/reset-db.js';
|
import * as ep___resetDb from './endpoints/reset-db.js';
|
||||||
import * as ep___resetPassword from './endpoints/reset-password.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/assign', ep___admin_roles_assign],
|
||||||
['admin/roles/unassign', ep___admin_roles_unassign],
|
['admin/roles/unassign', ep___admin_roles_unassign],
|
||||||
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
|
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
|
||||||
|
['admin/roles/users', ep___admin_roles_users],
|
||||||
['announcements', ep___announcements],
|
['announcements', ep___announcements],
|
||||||
['antennas/create', ep___antennas_create],
|
['antennas/create', ep___antennas_create],
|
||||||
['antennas/delete', ep___antennas_delete],
|
['antennas/delete', ep___antennas_delete],
|
||||||
|
@ -592,6 +597,9 @@ const eps = [
|
||||||
['ping', ep___ping],
|
['ping', ep___ping],
|
||||||
['pinned-users', ep___pinnedUsers],
|
['pinned-users', ep___pinnedUsers],
|
||||||
['promo/read', ep___promo_read],
|
['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],
|
['request-reset-password', ep___requestResetPassword],
|
||||||
['reset-db', ep___resetDb],
|
['reset-db', ep___resetDb],
|
||||||
['reset-password', ep___resetPassword],
|
['reset-password', ep___resetPassword],
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
const roles = await this.rolesRepository.find({
|
const roles = await this.rolesRepository.find({
|
||||||
order: { lastUsedAt: 'DESC' },
|
order: { lastUsedAt: 'DESC' },
|
||||||
});
|
});
|
||||||
return await this.roleEntityService.packMany(roles, me, { detail: false });
|
return await this.roleEntityService.packMany(roles, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,12 +39,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
private roleEntityService: RoleEntityService,
|
private roleEntityService: RoleEntityService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
throw new ApiError(meta.errors.noSuchRole);
|
throw new ApiError(meta.errors.noSuchRole);
|
||||||
}
|
}
|
||||||
return await this.roleEntityService.pack(role);
|
return await this.roleEntityService.pack(role, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }),
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
moderationNote: profile.moderationNote,
|
moderationNote: profile.moderationNote,
|
||||||
signins,
|
signins,
|
||||||
policies: await this.roleService.getUserPolicies(user.id),
|
policies: await this.roleService.getUserPolicies(user.id),
|
||||||
roles: await this.roleEntityService.packMany(roles, me, { detail: false }),
|
roles: await this.roleEntityService.packMany(roles, me),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
37
packages/backend/src/server/api/endpoints/roles/list.ts
Normal file
37
packages/backend/src/server/api/endpoints/roles/list.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
52
packages/backend/src/server/api/endpoints/roles/show.ts
Normal file
52
packages/backend/src/server/api/endpoints/roles/show.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
71
packages/backend/src/server/api/endpoints/roles/users.ts
Normal file
71
packages/backend/src/server/api/endpoints/roles/users.ts
Normal file
|
@ -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 }),
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,15 @@
|
||||||
<template>
|
<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">
|
<div :class="$style.title">
|
||||||
<span :class="$style.icon">
|
<span :class="$style.icon">
|
||||||
<i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
|
<template v-if="role.iconUrl">
|
||||||
<i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
|
<img :class="$style.badge" :src="role.iconUrl"/>
|
||||||
<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
|
</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>
|
||||||
<span :class="$style.name">{{ role.name }}</span>
|
<span :class="$style.name">{{ role.name }}</span>
|
||||||
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</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<{
|
const props = defineProps<{
|
||||||
role: any;
|
role: any;
|
||||||
|
forModeration: boolean;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -38,6 +44,11 @@ const props = defineProps<{
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
height: 1.3em;
|
||||||
|
vertical-align: -20%;
|
||||||
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,9 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ items: users }">
|
<template #default="{ items }">
|
||||||
<div class="efvhhmdq">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
|
@ -20,10 +20,13 @@ import MkUserInfo from '@/components/MkUserInfo.vue';
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
pagination: Paging;
|
pagination: Paging;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
}>();
|
extractor?: (item: any) => any;
|
||||||
|
}>(), {
|
||||||
|
extractor: (item) => item,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -16,16 +16,29 @@
|
||||||
<MkFolder v-if="role.target === 'manual'" default-open>
|
<MkFolder v-if="role.target === 'manual'" default-open>
|
||||||
<template #icon><i class="ti ti-users"></i></template>
|
<template #icon><i class="ti ti-users"></i></template>
|
||||||
<template #label>{{ i18n.ts.users }}</template>
|
<template #label>{{ i18n.ts.users }}</template>
|
||||||
<template #suffix>{{ role.users.length }}</template>
|
<template #suffix>{{ role.usersCount }}</template>
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
|
<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">
|
<MkPagination :pagination="usersPagination">
|
||||||
<MkA :class="$style.user" :to="`/user-info/${user.id}`">
|
<template #empty>
|
||||||
<MkUserCardMini :user="user"/>
|
<div class="_fullinfo">
|
||||||
</MkA>
|
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||||
<button class="_button" :class="$style.unassign" @click="unassign(user, $event)"><i class="ti ti-x"></i></button>
|
<div>{{ i18n.ts.noUsers }}</div>
|
||||||
</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>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<MkInfo v-else>{{ i18n.ts._role.isConditionalRole }}</MkInfo>
|
<MkInfo v-else>{{ i18n.ts._role.isConditionalRole }}</MkInfo>
|
||||||
|
@ -47,6 +60,7 @@ import { useRouter } from '@/router';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -54,6 +68,14 @@ const props = defineProps<{
|
||||||
id?: string;
|
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', {
|
const role = reactive(await os.api('admin/roles/show', {
|
||||||
roleId: props.id,
|
roleId: props.id,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<div class="_gaps_s">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
|
22
packages/frontend/src/pages/explore.roles.vue
Normal file
22
packages/frontend/src/pages/explore.roles.vue
Normal file
|
@ -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>
|
||||||
|
|
|
@ -8,19 +8,19 @@
|
||||||
<template v-if="tag == null">
|
<template v-if="tag == null">
|
||||||
<MkFoldableSection class="_margin" persist-key="explore-pinned-users">
|
<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>
|
<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>
|
||||||
<MkFoldableSection class="_margin" persist-key="explore-popular-users">
|
<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>
|
<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>
|
||||||
<MkFoldableSection class="_margin" persist-key="explore-recently-updated-users">
|
<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>
|
<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>
|
||||||
<MkFoldableSection class="_margin" persist-key="explore-recently-registered-users">
|
<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>
|
<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>
|
</MkFoldableSection>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,21 +36,21 @@
|
||||||
|
|
||||||
<MkFoldableSection v-if="tag != null" :key="`${tag}`" class="_margin">
|
<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>
|
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
|
||||||
<XUserList :pagination="tagUsers"/>
|
<MkUserList :pagination="tagUsers"/>
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<template v-if="tag == null">
|
<template v-if="tag == null">
|
||||||
<MkFoldableSection class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="ti ti-chart-line ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularUsers }}</template>
|
<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>
|
||||||
<MkFoldableSection class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="ti ti-message ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyUpdatedUsers }}</template>
|
<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>
|
||||||
<MkFoldableSection class="_margin">
|
<MkFoldableSection class="_margin">
|
||||||
<template #header><i class="ti ti-rocket ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.recentlyDiscoveredUsers }}</template>
|
<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>
|
</MkFoldableSection>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
import XUserList from '@/components/MkUserList.vue';
|
import MkUserList from '@/components/MkUserList.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
<div v-else-if="tab === 'users'">
|
<div v-else-if="tab === 'users'">
|
||||||
<XUsers/>
|
<XUsers/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="tab === 'roles'">
|
||||||
|
<XRoles/>
|
||||||
|
</div>
|
||||||
<div v-else-if="tab === 'search'">
|
<div v-else-if="tab === 'search'">
|
||||||
<MkSpacer :content-max="1200">
|
<MkSpacer :content-max="1200">
|
||||||
<div>
|
<div>
|
||||||
|
@ -22,7 +25,7 @@
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<XUserList v-if="searchQuery" ref="searchEl" class="_margin" :pagination="searchPagination"/>
|
<MkUserList v-if="searchQuery" ref="searchEl" class="_margin" :pagination="searchPagination"/>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,12 +36,13 @@
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
import XFeatured from './explore.featured.vue';
|
import XFeatured from './explore.featured.vue';
|
||||||
import XUsers from './explore.users.vue';
|
import XUsers from './explore.users.vue';
|
||||||
|
import XRoles from './explore.roles.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkRadios from '@/components/MkRadios.vue';
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import XUserList from '@/components/MkUserList.vue';
|
import MkUserList from '@/components/MkUserList.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
tag?: string;
|
tag?: string;
|
||||||
|
@ -75,8 +79,13 @@ const headerTabs = $computed(() => [{
|
||||||
key: 'users',
|
key: 'users',
|
||||||
icon: 'ti ti-users',
|
icon: 'ti ti-users',
|
||||||
title: i18n.ts.users,
|
title: i18n.ts.users,
|
||||||
|
}, {
|
||||||
|
key: 'roles',
|
||||||
|
icon: 'ti ti-badges',
|
||||||
|
title: i18n.ts.roles,
|
||||||
}, {
|
}, {
|
||||||
key: 'search',
|
key: 'search',
|
||||||
|
icon: 'ti ti-search',
|
||||||
title: i18n.ts.search,
|
title: i18n.ts.search,
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
47
packages/frontend/src/pages/role.vue
Normal file
47
packages/frontend/src/pages/role.vue
Normal file
|
@ -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>
|
||||||
|
|
|
@ -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>
|
<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">
|
<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-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>
|
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -197,6 +197,9 @@ export const routes = [{
|
||||||
path: '/theme-editor',
|
path: '/theme-editor',
|
||||||
component: page(() => import('./pages/theme-editor.vue')),
|
component: page(() => import('./pages/theme-editor.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/roles/:role',
|
||||||
|
component: page(() => import('./pages/role.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/explore/tags/:tag',
|
path: '/explore/tags/:tag',
|
||||||
component: page(() => import('./pages/explore.vue')),
|
component: page(() => import('./pages/explore.vue')),
|
||||||
|
|
Loading…
Reference in a new issue