From 88cb7f5aa946fae974344622e194f0ef23392913 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Fri, 21 Nov 2025 00:51:35 +0200 Subject: [PATCH] feat: add role colors + improve user data structure/enrichment --- lib/Controller/AdminController.php | 33 +++-- lib/Controller/PostController.php | 23 +++- lib/Controller/RoleController.php | 16 +++ lib/Controller/SearchController.php | 27 ++++- lib/Controller/ThreadController.php | 35 +++++- lib/Db/Post.php | 13 +- lib/Db/Role.php | 10 ++ lib/Db/RoleMapper.php | 21 ++++ lib/Db/Thread.php | 14 ++- lib/Db/UserRoleMapper.php | 21 ++++ lib/Migration/Version4Date20251120210339.php | 99 +++++++++++++++ lib/Service/UserService.php | 102 +++++++++++++++- openapi.json | 24 ++++ src/components/PostCard.vue | 7 +- src/components/RoleBadge.vue | 120 +++++++++++++++++++ src/components/SearchPostResult.vue | 2 +- src/components/SearchThreadResult.vue | 2 +- src/components/ThreadCard.vue | 8 +- src/components/UserInfo.vue | 61 +++++++++- src/types/models.ts | 19 ++- src/views/ThreadView.vue | 4 +- src/views/admin/AdminRoleEdit.vue | 115 +++++++++++++++++- src/views/admin/AdminRoleList.vue | 31 +---- src/views/admin/AdminUserList.vue | 66 ++-------- 24 files changed, 720 insertions(+), 153 deletions(-) create mode 100644 lib/Migration/Version4Date20251120210339.php create mode 100644 src/components/RoleBadge.vue diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index c6ca198..76dd25b 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -117,15 +117,19 @@ class AdminController extends OCSController { $statsByUserId[$stats->getUserId()] = $stats; } - // Get all Nextcloud users and enrich with forum data + // Collect all user IDs first + $userIds = []; + $this->userManager->callForAllUsers(function ($user) use (&$userIds) { + $userIds[] = $user->getUID(); + }); + + // Enrich all users at once for performance (includes roles) + $enrichedUserData = $this->userService->enrichMultipleUsers($userIds); + + // Build final user list with forum stats $enrichedUsers = []; - $this->userManager->callForAllUsers(function ($user) use (&$enrichedUsers, $statsByUserId) { - $userId = $user->getUID(); - - // Get user display name - $userInfo = $this->userService->enrichUserData($userId); - - // Get stats if they exist, otherwise use defaults + foreach ($userIds as $userId) { + $userInfo = $enrichedUserData[$userId]; $stats = $statsByUserId[$userId] ?? null; $userData = [ @@ -137,20 +141,11 @@ class AdminController extends OCSController { 'updatedAt' => $stats ? $stats->getUpdatedAt() : 0, 'deletedAt' => $stats ? $stats->getDeletedAt() : null, 'isDeleted' => $userInfo['isDeleted'], - 'roles' => [], + 'roles' => $userInfo['roles'], ]; - // Get user roles - try { - $userRoles = $this->userRoleMapper->findByUserId($userId); - $userData['roles'] = array_map(fn ($ur) => $ur->getRoleId(), $userRoles); - } catch (\Exception $e) { - // User has no roles - $userData['roles'] = []; - } - $enrichedUsers[] = $userData; - }); + } return new DataResponse(['users' => $enrichedUsers]); } catch (\Exception $e) { diff --git a/lib/Controller/PostController.php b/lib/Controller/PostController.php index 2be6005..99a337d 100644 --- a/lib/Controller/PostController.php +++ b/lib/Controller/PostController.php @@ -19,6 +19,7 @@ use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\BBCodeService; use OCA\Forum\Service\NotificationService; use OCA\Forum\Service\PermissionService; +use OCA\Forum\Service\UserService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -43,6 +44,7 @@ class PostController extends OCSController { private PermissionService $permissionService, private ReadMarkerMapper $readMarkerMapper, private NotificationService $notificationService, + private UserService $userService, private IUserSession $userSession, private LoggerInterface $logger, ) { @@ -86,10 +88,16 @@ class PostController extends OCSController { // Get current user ID to mark user's reactions $currentUserId = $this->userSession->getUser()?->getUID(); - // Enrich posts with content and reactions - return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId) { + // Extract unique author IDs + $authorIds = array_unique(array_map(fn ($p) => $p->getAuthorId(), $posts)); + + // Batch fetch author data (includes roles) + $authors = $this->userService->enrichMultipleUsers($authorIds); + + // Enrich posts with content, reactions, and pre-fetched author data + return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $authors) { $postReactions = $reactionsByPostId[$p->getId()] ?? []; - return Post::enrichPostContent($p, $bbcodes, $postReactions, $currentUserId); + return Post::enrichPostContent($p, $bbcodes, $postReactions, $currentUserId, $authors[$p->getAuthorId()]); }, $posts)); } catch (\Exception $e) { $this->logger->error('Error fetching posts by thread: ' . $e->getMessage()); @@ -134,10 +142,13 @@ class PostController extends OCSController { // Get current user ID to mark user's reactions $currentUserId = $this->userSession->getUser()?->getUID(); - // Enrich posts with content and reactions - return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId) { + // For posts by a single author, we can optimize by fetching author data once + $author = $this->userService->enrichUserData($authorId); + + // Enrich posts with content, reactions, and pre-fetched author data + return new DataResponse(array_map(function ($p) use ($bbcodes, $reactionsByPostId, $currentUserId, $author) { $postReactions = $reactionsByPostId[$p->getId()] ?? []; - return Post::enrichPostContent($p, $bbcodes, $postReactions, $currentUserId); + return Post::enrichPostContent($p, $bbcodes, $postReactions, $currentUserId, $author); }, $posts)); } catch (\Exception $e) { $this->logger->error('Error fetching posts by author: ' . $e->getMessage()); diff --git a/lib/Controller/RoleController.php b/lib/Controller/RoleController.php index 222fd0e..9f49419 100644 --- a/lib/Controller/RoleController.php +++ b/lib/Controller/RoleController.php @@ -79,6 +79,8 @@ class RoleController extends OCSController { * * @param string $name Role name * @param string|null $description Role description + * @param string|null $colorLight Light mode color + * @param string|null $colorDark Dark mode color * @param bool $canAccessAdminTools Can access admin tools * @param bool $canEditRoles Can edit roles * @param bool $canEditCategories Can edit categories @@ -92,6 +94,8 @@ class RoleController extends OCSController { public function create( string $name, ?string $description = null, + ?string $colorLight = null, + ?string $colorDark = null, bool $canAccessAdminTools = false, bool $canEditRoles = false, bool $canEditCategories = false, @@ -100,6 +104,8 @@ class RoleController extends OCSController { $role = new \OCA\Forum\Db\Role(); $role->setName($name); $role->setDescription($description); + $role->setColorLight($colorLight); + $role->setColorDark($colorDark); $role->setCanAccessAdminTools($canAccessAdminTools); $role->setCanEditRoles($canEditRoles); $role->setCanEditCategories($canEditCategories); @@ -120,6 +126,8 @@ class RoleController extends OCSController { * @param int $id Role ID * @param string|null $name Role name * @param string|null $description Role description + * @param string|null $colorLight Light mode color + * @param string|null $colorDark Dark mode color * @param bool|null $canAccessAdminTools Can access admin tools * @param bool|null $canEditRoles Can edit roles * @param bool|null $canEditCategories Can edit categories @@ -134,6 +142,8 @@ class RoleController extends OCSController { int $id, ?string $name = null, ?string $description = null, + ?string $colorLight = null, + ?string $colorDark = null, ?bool $canAccessAdminTools = null, ?bool $canEditRoles = null, ?bool $canEditCategories = null, @@ -147,6 +157,12 @@ class RoleController extends OCSController { if ($description !== null) { $role->setDescription($description); } + if ($colorLight !== null) { + $role->setColorLight($colorLight); + } + if ($colorDark !== null) { + $role->setColorDark($colorDark); + } if ($canAccessAdminTools !== null) { $role->setCanAccessAdminTools($canAccessAdminTools); } diff --git a/lib/Controller/SearchController.php b/lib/Controller/SearchController.php index ad9d521..c2e5f9d 100644 --- a/lib/Controller/SearchController.php +++ b/lib/Controller/SearchController.php @@ -11,6 +11,7 @@ use OCA\Forum\Db\Post; use OCA\Forum\Db\Thread; use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Service\SearchService; +use OCA\Forum\Service\UserService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -26,6 +27,7 @@ class SearchController extends OCSController { IRequest $request, private SearchService $searchService, private ThreadMapper $threadMapper, + private UserService $userService, private IUserSession $userSession, private LoggerInterface $logger, ) { @@ -87,14 +89,27 @@ class SearchController extends OCSController { $offset ); - // Enrich threads - $enrichedThreads = array_map(function ($thread) { - return Thread::enrichThread($thread); + // Collect all unique author IDs from both threads and posts + $allAuthorIds = []; + foreach ($results['threads'] as $thread) { + $allAuthorIds[] = $thread->getAuthorId(); + } + foreach ($results['posts'] as $post) { + $allAuthorIds[] = $post->getAuthorId(); + } + $allAuthorIds = array_unique($allAuthorIds); + + // Batch fetch all author data once + $authors = $this->userService->enrichMultipleUsers($allAuthorIds); + + // Enrich threads with pre-fetched author data + $enrichedThreads = array_map(function ($thread) use ($authors) { + return Thread::enrichThread($thread, $authors[$thread->getAuthorId()]); }, $results['threads']); - // Enrich posts (with thread context) - $enrichedPosts = array_map(function ($post) { - $enriched = Post::enrichPostContent($post); + // Enrich posts with pre-fetched author data and thread context + $enrichedPosts = array_map(function ($post) use ($authors) { + $enriched = Post::enrichPostContent($post, [], [], null, $authors[$post->getAuthorId()]); // Add thread info for context try { $thread = $this->threadMapper->find($post->getThreadId()); diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php index bd804f4..aab7f03 100644 --- a/lib/Controller/ThreadController.php +++ b/lib/Controller/ThreadController.php @@ -16,6 +16,7 @@ use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Db\ThreadSubscriptionMapper; use OCA\Forum\Db\UserStatsMapper; use OCA\Forum\Service\UserPreferencesService; +use OCA\Forum\Service\UserService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -36,6 +37,7 @@ class ThreadController extends OCSController { private UserStatsMapper $userStatsMapper, private ThreadSubscriptionMapper $threadSubscriptionMapper, private UserPreferencesService $userPreferencesService, + private UserService $userService, private IUserSession $userSession, private LoggerInterface $logger, ) { @@ -54,7 +56,17 @@ class ThreadController extends OCSController { public function index(): DataResponse { try { $threads = $this->threadMapper->findAll(); - return new DataResponse(array_map(fn ($t) => Thread::enrichThread($t), $threads)); + + // Extract unique author IDs + $authorIds = array_unique(array_map(fn ($t) => $t->getAuthorId(), $threads)); + + // Batch fetch author data (includes roles) + $authors = $this->userService->enrichMultipleUsers($authorIds); + + // Enrich threads with pre-fetched author data + return new DataResponse(array_map(function ($t) use ($authors) { + return Thread::enrichThread($t, $authors[$t->getAuthorId()]); + }, $threads)); } catch (\Exception $e) { $this->logger->error('Error fetching threads: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to fetch threads'], Http::STATUS_INTERNAL_SERVER_ERROR); @@ -77,7 +89,17 @@ class ThreadController extends OCSController { public function byCategory(int $categoryId, int $limit = 50, int $offset = 0): DataResponse { try { $threads = $this->threadMapper->findByCategoryId($categoryId, $limit, $offset); - return new DataResponse(array_map(fn ($t) => Thread::enrichThread($t), $threads)); + + // Extract unique author IDs + $authorIds = array_unique(array_map(fn ($t) => $t->getAuthorId(), $threads)); + + // Batch fetch author data (includes roles) + $authors = $this->userService->enrichMultipleUsers($authorIds); + + // Enrich threads with pre-fetched author data + return new DataResponse(array_map(function ($t) use ($authors) { + return Thread::enrichThread($t, $authors[$t->getAuthorId()]); + }, $threads)); } catch (\Exception $e) { $this->logger->error('Error fetching threads by category: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to fetch threads'], Http::STATUS_INTERNAL_SERVER_ERROR); @@ -99,7 +121,14 @@ class ThreadController extends OCSController { public function byAuthor(string $authorId, int $limit = 50, int $offset = 0): DataResponse { try { $threads = $this->threadMapper->findByAuthorId($authorId, $limit, $offset); - return new DataResponse(array_map(fn ($t) => Thread::enrichThread($t), $threads)); + + // For threads by a single author, we can optimize by fetching author data once + $author = $this->userService->enrichUserData($authorId); + + // Enrich threads with pre-fetched author data + return new DataResponse(array_map(function ($t) use ($author) { + return Thread::enrichThread($t, $author); + }, $threads)); } catch (\Exception $e) { $this->logger->error('Error fetching threads by author: ' . $e->getMessage()); return new DataResponse(['error' => 'Failed to fetch threads'], Http::STATUS_INTERNAL_SERVER_ERROR); diff --git a/lib/Db/Post.php b/lib/Db/Post.php index d49cbf1..b75f365 100644 --- a/lib/Db/Post.php +++ b/lib/Db/Post.php @@ -82,6 +82,7 @@ class Post extends Entity implements JsonSerializable { array $bbcodes = [], array $reactions = [], ?string $currentUserId = null, + ?array $author = null, ): array { if (!is_array($post)) { $post = $post->jsonSerialize(); @@ -95,11 +96,13 @@ class Post extends Entity implements JsonSerializable { } $post['content'] = $service->parse($post['content'], $bbcodes, $post['authorId'], $post['id']); - // Add author display name (obfuscated if user is deleted) - $userService = \OC::$server->get(\OCA\Forum\Service\UserService::class); - $userData = $userService->enrichUserData($post['authorId']); - $post['authorDisplayName'] = $userData['displayName']; - $post['authorIsDeleted'] = $userData['isDeleted']; + // Add author object (includes display name, deleted status, and roles) + if ($author === null) { + $userService = \OC::$server->get(\OCA\Forum\Service\UserService::class); + $post['author'] = $userService->enrichUserData($post['authorId']); + } else { + $post['author'] = $author; + } // Add reactions (grouped by emoji) $post['reactions'] = self::groupReactions($reactions, $currentUserId); diff --git a/lib/Db/Role.php b/lib/Db/Role.php index 4d3db71..b70843e 100644 --- a/lib/Db/Role.php +++ b/lib/Db/Role.php @@ -18,6 +18,10 @@ use OCP\AppFramework\Db\Entity; * @method void setName(string $value) * @method string|null getDescription() * @method void setDescription(?string $value) + * @method string|null getColorLight() + * @method void setColorLight(?string $value) + * @method string|null getColorDark() + * @method void setColorDark(?string $value) * @method bool getCanAccessAdminTools() * @method void setCanAccessAdminTools(bool $value) * @method bool getCanEditRoles() @@ -30,6 +34,8 @@ use OCP\AppFramework\Db\Entity; class Role extends Entity implements JsonSerializable { protected $name; protected $description; + protected $colorLight; + protected $colorDark; protected $canAccessAdminTools; protected $canEditRoles; protected $canEditCategories; @@ -39,6 +45,8 @@ class Role extends Entity implements JsonSerializable { $this->addType('id', 'integer'); $this->addType('name', 'string'); $this->addType('description', 'string'); + $this->addType('colorLight', 'string'); + $this->addType('colorDark', 'string'); $this->addType('canAccessAdminTools', 'boolean'); $this->addType('canEditRoles', 'boolean'); $this->addType('canEditCategories', 'boolean'); @@ -50,6 +58,8 @@ class Role extends Entity implements JsonSerializable { 'id' => $this->getId(), 'name' => $this->getName(), 'description' => $this->getDescription(), + 'colorLight' => $this->getColorLight(), + 'colorDark' => $this->getColorDark(), 'canAccessAdminTools' => $this->getCanAccessAdminTools(), 'canEditRoles' => $this->getCanEditRoles(), 'canEditCategories' => $this->getCanEditCategories(), diff --git a/lib/Db/RoleMapper.php b/lib/Db/RoleMapper.php index d6a3115..67bb5f5 100644 --- a/lib/Db/RoleMapper.php +++ b/lib/Db/RoleMapper.php @@ -74,6 +74,27 @@ class RoleMapper extends QBMapper { return $this->findEntity($qb); } + /** + * Find multiple roles by IDs at once + * + * @param array $ids + * @return array + */ + public function findByIds(array $ids): array { + if (empty($ids)) { + return []; + } + + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)) + ); + return $this->findEntities($qb); + } + /** * @return array */ diff --git a/lib/Db/Thread.php b/lib/Db/Thread.php index 4651e69..c69dcb2 100644 --- a/lib/Db/Thread.php +++ b/lib/Db/Thread.php @@ -91,16 +91,18 @@ class Thread extends Entity implements JsonSerializable { ]; } - public static function enrichThread(mixed $thread): array { + public static function enrichThread(mixed $thread, ?array $author = null): array { if (!is_array($thread)) { $thread = $thread->jsonSerialize(); } - // Add author display name (obfuscated if user is deleted) - $userService = \OC::$server->get(\OCA\Forum\Service\UserService::class); - $userData = $userService->enrichUserData($thread['authorId']); - $thread['authorDisplayName'] = $userData['displayName']; - $thread['authorIsDeleted'] = $userData['isDeleted']; + // Add author object (includes display name, deleted status, and roles) + if ($author === null) { + $userService = \OC::$server->get(\OCA\Forum\Service\UserService::class); + $thread['author'] = $userService->enrichUserData($thread['authorId']); + } else { + $thread['author'] = $author; + } // Add category information (slug and name) for navigation try { diff --git a/lib/Db/UserRoleMapper.php b/lib/Db/UserRoleMapper.php index b49e6ab..7b395da 100644 --- a/lib/Db/UserRoleMapper.php +++ b/lib/Db/UserRoleMapper.php @@ -69,6 +69,27 @@ class UserRoleMapper extends QBMapper { return $this->findEntities($qb); } + /** + * Find user roles for multiple users at once + * + * @param array $userIds + * @return array + */ + public function findByUserIds(array $userIds): array { + if (empty($userIds)) { + return []; + } + + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->in('user_id', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)) + ); + return $this->findEntities($qb); + } + /** * @return array */ diff --git a/lib/Migration/Version4Date20251120210339.php b/lib/Migration/Version4Date20251120210339.php new file mode 100644 index 0000000..646c2da --- /dev/null +++ b/lib/Migration/Version4Date20251120210339.php @@ -0,0 +1,99 @@ + +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\Forum\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version4Date20251120210339 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + // Add color columns to forum_roles table + if ($schema->hasTable('forum_roles')) { + $table = $schema->getTable('forum_roles'); + + if (!$table->hasColumn('color_light')) { + $table->addColumn('color_light', 'string', [ + 'notnull' => false, + 'length' => 7, + 'default' => null, + ]); + } + + if (!$table->hasColumn('color_dark')) { + $table->addColumn('color_dark', 'string', [ + 'notnull' => false, + 'length' => 7, + 'default' => null, + ]); + } + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $this->updateDefaultRoleColors(); + } + + /** + * Update default roles with colors that are legible in both light and dark modes + */ + private function updateDefaultRoleColors(): void { + $db = \OC::$server->get(\OCP\IDBConnection::class); + + // Define colors for default roles + // Light mode uses darker colors, dark mode uses lighter colors for better contrast + $roleColors = [ + 1 => [ + 'light' => '#dc2626', // Red 600 + 'dark' => '#f87171', // Red 400 + ], + 2 => [ + 'light' => '#2563eb', // Blue 600 + 'dark' => '#60a5fa', // Blue 400 + ], + 'User' => [ + 'light' => '#059669', // Emerald 600 + 'dark' => '#34d399', // Emerald 400 + ], + ]; + + foreach ($roleColors as $roleId => $colors) { + $qb = $db->getQueryBuilder(); + $qb->update('forum_roles') + ->set('color_light', $qb->createNamedParameter($colors['light'])) + ->set('color_dark', $qb->createNamedParameter($colors['dark'])) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($roleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))) + ->executeStatement(); + } + } +} diff --git a/lib/Service/UserService.php b/lib/Service/UserService.php index 6b42377..abea0da 100644 --- a/lib/Service/UserService.php +++ b/lib/Service/UserService.php @@ -7,6 +7,8 @@ declare(strict_types=1); namespace OCA\Forum\Service; +use OCA\Forum\Db\RoleMapper; +use OCA\Forum\Db\UserRoleMapper; use OCA\Forum\Db\UserStatsMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\IUserManager; @@ -19,6 +21,8 @@ class UserService { public function __construct( private IUserManager $userManager, private UserStatsMapper $userStatsMapper, + private RoleMapper $roleMapper, + private UserRoleMapper $userRoleMapper, ) { } @@ -58,18 +62,35 @@ class UserService { } /** - * Enrich user data with display name and deleted status + * Enrich user data with display name, deleted status, and roles * - * @return array{userId: string, displayName: string, isDeleted: bool} + * @param string $userId + * @param array|null $roles Optional pre-fetched roles array + * @return array{userId: string, displayName: string, isDeleted: bool, roles: array} */ - public function enrichUserData(string $userId): array { + public function enrichUserData(string $userId, ?array $roles = null): array { $isDeleted = $this->isUserDeleted($userId); $displayName = $this->getUserDisplayName($userId); + // If roles not provided, fetch them + if ($roles === null) { + $userRoles = $this->userRoleMapper->findByUserId($userId); + $roles = []; + foreach ($userRoles as $userRole) { + try { + $role = $this->roleMapper->find($userRole->getRoleId()); + $roles[] = $role->jsonSerialize(); + } catch (\Exception $e) { + // Role not found, skip + } + } + } + return [ 'userId' => $userId, 'displayName' => $displayName, 'isDeleted' => $isDeleted, + 'roles' => $roles, ]; } @@ -77,13 +98,82 @@ class UserService { * Enrich multiple users at once (for performance) * * @param array $userIds - * @return array + * @param array $rolesMap Optional pre-fetched roles map (userId => roles[]) + * @return array */ - public function enrichMultipleUsers(array $userIds): array { + public function enrichMultipleUsers(array $userIds, ?array $rolesMap = null): array { $result = []; + + // If roles not provided, fetch them all at once + if ($rolesMap === null) { + $rolesMap = $this->fetchRolesForUsers($userIds); + } + foreach ($userIds as $userId) { - $result[$userId] = $this->enrichUserData($userId); + $isDeleted = $this->isUserDeleted($userId); + $displayName = $this->getUserDisplayName($userId); + + $result[$userId] = [ + 'userId' => $userId, + 'displayName' => $displayName, + 'isDeleted' => $isDeleted, + 'roles' => $rolesMap[$userId] ?? [], + ]; } return $result; } + + /** + * Fetch roles for multiple users efficiently + * + * @param array $userIds + * @return array Map of userId => roles[] + */ + private function fetchRolesForUsers(array $userIds): array { + if (empty($userIds)) { + return []; + } + + $rolesMap = []; + + // Initialize all user IDs with empty arrays + foreach ($userIds as $userId) { + $rolesMap[$userId] = []; + } + + // Fetch all user roles for these users + $userRoles = $this->userRoleMapper->findByUserIds($userIds); + + // Group by user ID and fetch role details + $roleIds = []; + $userRolesByUser = []; + foreach ($userRoles as $userRole) { + $userId = $userRole->getUserId(); + $roleId = $userRole->getRoleId(); + + if (!isset($userRolesByUser[$userId])) { + $userRolesByUser[$userId] = []; + } + $userRolesByUser[$userId][] = $roleId; + $roleIds[$roleId] = true; + } + + // Fetch all roles at once + $roles = []; + $roleEntities = $this->roleMapper->findByIds(array_keys($roleIds)); + foreach ($roleEntities as $role) { + $roles[$role->getId()] = $role->jsonSerialize(); + } + + // Map roles to users + foreach ($userRolesByUser as $userId => $userRoleIds) { + foreach ($userRoleIds as $roleId) { + if (isset($roles[$roleId])) { + $rolesMap[$userId][] = $roles[$roleId]; + } + } + } + + return $rolesMap; + } } diff --git a/openapi.json b/openapi.json index 3703b0e..4f068c2 100644 --- a/openapi.json +++ b/openapi.json @@ -5756,6 +5756,18 @@ "default": null, "description": "Role description" }, + "colorLight": { + "type": "string", + "nullable": true, + "default": null, + "description": "Light mode color" + }, + "colorDark": { + "type": "string", + "nullable": true, + "default": null, + "description": "Dark mode color" + }, "canAccessAdminTools": { "type": "boolean", "default": false, @@ -5987,6 +5999,18 @@ "default": null, "description": "Role description" }, + "colorLight": { + "type": "string", + "nullable": true, + "default": null, + "description": "Light mode color" + }, + "colorDark": { + "type": "string", + "nullable": true, + "default": null, + "description": "Dark mode color" + }, "canAccessAdminTools": { "type": "boolean", "nullable": true, diff --git a/src/components/PostCard.vue b/src/components/PostCard.vue index aa2221b..ae702cc 100644 --- a/src/components/PostCard.vue +++ b/src/components/PostCard.vue @@ -4,10 +4,11 @@