mirror of
https://github.com/booklore-app/booklore.git
synced 2025-12-23 22:28:11 -05:00
Introduce more granular permission controls and update the user management UI (#1965)
Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,10 @@ import {BookdropFileReviewComponent} from './features/bookdrop/component/bookdro
|
||||
import {ManageLibraryGuard} from './core/security/guards/manage-library.guard';
|
||||
import {LoginGuard} from './shared/components/setup/login.guard';
|
||||
import {UserStatsComponent} from './features/stats/component/user-stats/user-stats.component';
|
||||
import {BookdropGuard} from './core/security/guards/bookdrop.guard';
|
||||
import {LibraryStatsGuard} from './core/security/guards/library-stats.guard';
|
||||
import {UserStatsGuard} from './core/security/guards/user-stats.guard';
|
||||
import {EditMetadataGuard} from './core/security/guards/edit-metdata.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -49,10 +53,10 @@ export const routes: Routes = [
|
||||
{path: 'series/:seriesName', component: SeriesPageComponent, canActivate: [AuthGuard]},
|
||||
{path: 'magic-shelf/:magicShelfId/books', component: BookBrowserComponent, canActivate: [AuthGuard]},
|
||||
{path: 'book/:bookId', component: BookMetadataCenterComponent, canActivate: [AuthGuard]},
|
||||
{path: 'bookdrop', component: BookdropFileReviewComponent, canActivate: [ManageLibraryGuard]},
|
||||
{path: 'metadata-manager', component: MetadataManagerComponent, canActivate: [ManageLibraryGuard]},
|
||||
{path: 'library-stats', component: StatsComponent, canActivate: [AuthGuard]},
|
||||
{path: 'reading-stats', component: UserStatsComponent, canActivate: [AuthGuard]},
|
||||
{path: 'bookdrop', component: BookdropFileReviewComponent, canActivate: [BookdropGuard]},
|
||||
{path: 'metadata-manager', component: MetadataManagerComponent, canActivate: [EditMetadataGuard]},
|
||||
{path: 'library-stats', component: StatsComponent, canActivate: [LibraryStatsGuard]},
|
||||
{path: 'reading-stats', component: UserStatsComponent, canActivate: [UserStatsGuard]},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
20
booklore-ui/src/app/core/security/guards/bookdrop.guard.ts
Normal file
20
booklore-ui/src/app/core/security/guards/bookdrop.guard.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const BookdropGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canAccessBookdrop)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const EditMetadataGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canEditMetadata)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const LibraryStatsGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canAccessLibraryStats)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ export const ManageLibraryGuard: CanActivateFn = () => {
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canManipulateLibrary)) {
|
||||
if (user && (user.permissions.admin || user.permissions.canManageLibrary)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
|
||||
20
booklore-ui/src/app/core/security/guards/user-stats.guard.ts
Normal file
20
booklore-ui/src/app/core/security/guards/user-stats.guard.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {CanActivateFn, Router} from '@angular/router';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
export const UserStatsGuard: CanActivateFn = () => {
|
||||
const userService = inject(UserService);
|
||||
const router = inject(Router);
|
||||
|
||||
return userService.userState$.pipe(
|
||||
map(state => {
|
||||
const user = state.user;
|
||||
if (user && (user.permissions.admin || user.permissions.canAccessUserStats)) {
|
||||
return true;
|
||||
}
|
||||
router.navigate(['/dashboard']);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (entityType !== EntityType.ALL_BOOKS && entityType !== EntityType.UNSHELVED &&
|
||||
(userState.user!.permissions.admin || userState.user!.permissions.canManipulateLibrary)) {
|
||||
(userState.user!.permissions.admin || userState.user!.permissions.canManageLibrary)) {
|
||||
<div class="ml-2 flex-shrink-0">
|
||||
<p-button
|
||||
icon="pi pi-ellipsis-v"
|
||||
|
||||
@@ -39,7 +39,7 @@ export class BookdropFileService implements OnDestroy {
|
||||
)
|
||||
.subscribe(state => {
|
||||
const user = state.user!;
|
||||
if (user.permissions.admin || user.permissions.canManipulateLibrary) {
|
||||
if (user.permissions.admin || user.permissions.canAccessBookdrop) {
|
||||
this.authService.token$
|
||||
.pipe(filter(t => !!t), take(1))
|
||||
.subscribe(() => this.refresh());
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="dashboard-no-library">
|
||||
@if ((userService.userState$ | async)?.user?.permissions; as permissions) {
|
||||
<div>
|
||||
@if (permissions.admin || permissions.canManipulateLibrary) {
|
||||
@if (permissions.admin || permissions.canManageLibrary) {
|
||||
<div>
|
||||
<h1 class="no-library-header">
|
||||
Welcome to BookLore!<br>
|
||||
|
||||
@@ -9,16 +9,20 @@
|
||||
<p-tab [value]="SettingsTab.ViewPreferences">
|
||||
<i class="pi pi-desktop"></i> View
|
||||
</p-tab>
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tab [value]="SettingsTab.MetadataSettings">
|
||||
<i class="pi pi-sliders-h"></i> Metadata 1
|
||||
</p-tab>
|
||||
<p-tab [value]="SettingsTab.LibraryMetadataSettings">
|
||||
<i class="pi pi-database"></i> Metadata 2
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageGlobalPreferences) {
|
||||
<p-tab [value]="SettingsTab.ApplicationSettings">
|
||||
<i class="pi pi-cog"></i> Application
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tab [value]="SettingsTab.UserManagement">
|
||||
<i class="pi pi-users"></i> Users
|
||||
</p-tab>
|
||||
@@ -26,14 +30,17 @@
|
||||
<p-tab [value]="SettingsTab.EmailSettingsV2">
|
||||
<i class="pi pi-envelope"></i> Email
|
||||
</p-tab>
|
||||
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tab [value]="SettingsTab.NamingPattern">
|
||||
<i class="pi pi-sitemap"></i> Patterns
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tab [value]="SettingsTab.AuthenticationSettings">
|
||||
<i class="pi pi-lock"></i> Authentication
|
||||
</p-tab>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canAccessTaskManager) {
|
||||
<p-tab [value]="SettingsTab.Tasks">
|
||||
<i class="pi pi-list-check"></i> Tasks
|
||||
</p-tab>
|
||||
@@ -52,16 +59,20 @@
|
||||
<p-tabpanel [value]="SettingsTab.ViewPreferences">
|
||||
<app-view-preferences-parent></app-view-preferences-parent>
|
||||
</p-tabpanel>
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tabpanel [value]="SettingsTab.MetadataSettings">
|
||||
<app-metadata-settings-component></app-metadata-settings-component>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel [value]="SettingsTab.LibraryMetadataSettings">
|
||||
<app-library-metadata-settings-component></app-library-metadata-settings-component>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageGlobalPreferences) {
|
||||
<p-tabpanel [value]="SettingsTab.ApplicationSettings">
|
||||
<app-global-preferences></app-global-preferences>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tabpanel [value]="SettingsTab.UserManagement">
|
||||
<app-user-management></app-user-management>
|
||||
</p-tabpanel>
|
||||
@@ -69,13 +80,17 @@
|
||||
<p-tabpanel [value]="SettingsTab.EmailSettingsV2">
|
||||
<app-email-v2></app-email-v2>
|
||||
</p-tabpanel>
|
||||
@if (userState.user.permissions.admin) {
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canManageMetadataConfig) {
|
||||
<p-tabpanel [value]="SettingsTab.NamingPattern">
|
||||
<app-file-naming-pattern></app-file-naming-pattern>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin) {
|
||||
<p-tabpanel [value]="SettingsTab.AuthenticationSettings">
|
||||
<app-authentication-settings></app-authentication-settings>
|
||||
</p-tabpanel>
|
||||
}
|
||||
@if (userState.user.permissions.admin || userState.user.permissions.canAccessTaskManager) {
|
||||
<p-tabpanel [value]="SettingsTab.Tasks">
|
||||
<app-task-management></app-task-management>
|
||||
</p-tabpanel>
|
||||
|
||||
@@ -29,65 +29,51 @@
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
<p-table [value]="users" [scrollable]="true" scrollHeight="flex">
|
||||
<p-table
|
||||
[value]="users"
|
||||
[scrollable]="true"
|
||||
scrollHeight="flex"
|
||||
dataKey="id">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Username</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="header-content">
|
||||
<th style="width: 40px"></th>
|
||||
<th style="width: 100px; text-align: center">
|
||||
<div class="header-content" style="justify-content: center">
|
||||
<i class="pi pi-tag"></i>
|
||||
<span>Type</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="full-name-column">
|
||||
<th style="min-width: 180px">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-id-card"></i>
|
||||
<span>Full Name</span>
|
||||
<i class="pi pi-user"></i>
|
||||
<span>User</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="email-column">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>Email</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="libraries-column">
|
||||
<th style="min-width: 200px">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-book"></i>
|
||||
<span>Assigned Libraries</span>
|
||||
<span>Libraries</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="permission-header">Admin</th>
|
||||
<th class="permission-header">Upload</th>
|
||||
<th class="permission-header">Download</th>
|
||||
<th class="permission-header">Manage Metadata</th>
|
||||
<th class="permission-header">Manage Library</th>
|
||||
<th class="permission-header">Email Books</th>
|
||||
<th class="permission-header">Delete Books</th>
|
||||
<th class="permission-header">Access OPDS</th>
|
||||
<th class="permission-header">KOReader Sync</th>
|
||||
<th class="permission-header">Kobo Sync</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-cog"></i>
|
||||
<span>Edit</span>
|
||||
</div>
|
||||
<th style="width: 120px; text-align: center" pTooltip="Admin permissions" tooltipPosition="top">
|
||||
<i class="pi pi-shield"></i>
|
||||
</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-key"></i>
|
||||
<span>Password</span>
|
||||
</div>
|
||||
<th style="width: 120px; text-align: center" pTooltip="Book Management permissions" tooltipPosition="top">
|
||||
<i class="pi pi-book"></i>
|
||||
</th>
|
||||
<th class="actions-header">
|
||||
<div class="header-content">
|
||||
<i class="pi pi-trash"></i>
|
||||
<span>Delete</span>
|
||||
<th style="width: 120px; text-align: center" pTooltip="Device Sync permissions" tooltipPosition="top">
|
||||
<i class="pi pi-mobile"></i>
|
||||
</th>
|
||||
<th style="width: 120px; text-align: center" pTooltip="System Access permissions" tooltipPosition="top">
|
||||
<i class="pi pi-eye"></i>
|
||||
</th>
|
||||
<th style="width: 120px; text-align: center" pTooltip="System Configuration permissions" tooltipPosition="top">
|
||||
<i class="pi pi-cog"></i>
|
||||
</th>
|
||||
<th style="width: 180px; text-align: center">
|
||||
<div class="header-content" style="justify-content: center">
|
||||
<i class="pi pi-ellipsis-h"></i>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -95,36 +81,31 @@
|
||||
|
||||
<ng-template pTemplate="body" let-user>
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
pButton
|
||||
class="p-button-text p-button-rounded p-button-plain"
|
||||
(click)="toggleRowExpansion(user)">
|
||||
<i [class]="isRowExpanded(user) ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></i>
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="user-type-badge" [attr.data-type]="user.provisioningMethod || 'LOCAL'">
|
||||
{{ (user.provisioningMethod || 'LOCAL') | lowercase | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<div class="user-avatar">
|
||||
{{ user.username.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<span class="username">{{ user.username }}</span>
|
||||
<div class="user-details">
|
||||
<span class="username">{{ user.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="user-type-badge">
|
||||
{{ (user.provisioningMethod || 'LOCAL') | lowercase | titlecase }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="full-name-column">
|
||||
@if (user.isEditing) {
|
||||
<input type="text" [(ngModel)]="user.name" class="p-inputtext w-full" size="small"/>
|
||||
}
|
||||
@if (!user.isEditing) {
|
||||
<span>{{ user.name }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="email-column">
|
||||
@if (user.isEditing) {
|
||||
<input type="email" [(ngModel)]="user.email" class="p-inputtext w-full" size="small"/>
|
||||
}
|
||||
@if (!user.isEditing) {
|
||||
<span>{{ user.email }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="libraries-column">
|
||||
@if (user.isEditing) {
|
||||
<p-multiSelect
|
||||
[options]="allLibraries"
|
||||
@@ -133,102 +114,244 @@
|
||||
[(ngModel)]="editingLibraryIds"
|
||||
placeholder="Select Libraries"
|
||||
appendTo="body"
|
||||
[style]="{'width': '100%'}"
|
||||
size="small">
|
||||
</p-multiSelect>
|
||||
}
|
||||
@if (!user.isEditing) {
|
||||
<span class="library-names">{{ user.libraryNames }}</span>
|
||||
} @else {
|
||||
<span class="library-names">{{ user.libraryNames || 'None' }}</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.admin" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-indicator admin-indicator" [class.active]="user.permissions.admin">
|
||||
@if (user.permissions.admin) {
|
||||
<i class="pi pi-shield" pTooltip="Administrator"></i>
|
||||
} @else {
|
||||
<i class="pi pi-minus" pTooltip="Not Administrator"></i>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canUpload" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getBookManagementPermissionsCount(user)" [attr.data-total]="6">
|
||||
<span class="permission-count">
|
||||
{{ getBookManagementPermissionsCount(user) }}/6
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDownload" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getDeviceSyncPermissionsCount(user)" [attr.data-total]="3">
|
||||
<span class="permission-count">
|
||||
{{ getDeviceSyncPermissionsCount(user) }}/3
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEditMetadata" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getSystemAccessPermissionsCount(user)" [attr.data-total]="3">
|
||||
<span class="permission-count">
|
||||
{{ getSystemAccessPermissionsCount(user) }}/3
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManipulateLibrary" [disabled]="!user.isEditing"></p-checkbox>
|
||||
<div class="permission-summary" [attr.data-count]="getSystemConfigPermissionsCount(user)" [attr.data-total]="4">
|
||||
<span class="permission-count">
|
||||
{{ getSystemConfigPermissionsCount(user) }}/4
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEmailBook" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDeleteBook" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessOpds" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKoReader" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKobo" [disabled]="!user.isEditing"></p-checkbox>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
@if (!user.isEditing) {
|
||||
<p-button
|
||||
icon="pi pi-pencil"
|
||||
severity="info"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="toggleEdit(user)"
|
||||
pTooltip="Edit user">
|
||||
</p-button>
|
||||
}
|
||||
@if (user.isEditing) {
|
||||
<div class="flex gap-1">
|
||||
<td>
|
||||
<div class="actions-group">
|
||||
@if (!user.isEditing) {
|
||||
<p-button
|
||||
icon="pi pi-pencil"
|
||||
severity="info"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="toggleEdit(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Edit user">
|
||||
</p-button>
|
||||
}
|
||||
@if (user.isEditing) {
|
||||
<p-button
|
||||
icon="pi pi-check"
|
||||
severity="success"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="saveUser(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Save changes">
|
||||
</p-button>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="toggleEdit(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Cancel">
|
||||
</p-button>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<p-button
|
||||
icon="pi pi-key"
|
||||
severity="warn"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="openChangePasswordDialog(user)"
|
||||
pTooltip="Change password">
|
||||
</p-button>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<p-button
|
||||
[disabled]="user.id === currentUser?.id"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="deleteUser(user)"
|
||||
pTooltip="Delete user">
|
||||
</p-button>
|
||||
}
|
||||
@if (!user.isEditing) {
|
||||
<p-button
|
||||
icon="pi pi-key"
|
||||
severity="warn"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="openChangePasswordDialog(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Change password">
|
||||
</p-button>
|
||||
<p-button
|
||||
[disabled]="user.id === currentUser?.id"
|
||||
icon="pi pi-trash"
|
||||
severity="danger"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
(onClick)="deleteUser(user)"
|
||||
tooltipPosition="top"
|
||||
pTooltip="Delete user">
|
||||
</p-button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@if (isRowExpanded(user)) {
|
||||
<tr>
|
||||
<td colspan="10">
|
||||
<div class="expanded-content">
|
||||
<div class="expanded-section">
|
||||
<h4 class="expanded-title">
|
||||
<i class="pi pi-id-card"></i>
|
||||
User Information
|
||||
</h4>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>Full Name</label>
|
||||
@if (user.isEditing) {
|
||||
<input type="text" [(ngModel)]="user.name" class="p-inputtext p-component p-inputtext-sm"/>
|
||||
} @else {
|
||||
<span>{{ user.name || 'N/A' }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>Email</label>
|
||||
@if (user.isEditing) {
|
||||
<input type="email" [(ngModel)]="user.email" class="p-inputtext p-component p-inputtext-sm"/>
|
||||
} @else {
|
||||
<span>{{ user.email || 'N/A' }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="expanded-section">
|
||||
<h4 class="expanded-title">
|
||||
<i class="pi pi-lock"></i>
|
||||
Permissions
|
||||
</h4>
|
||||
<div class="permissions-grid">
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-book"></i> Book Management</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canUpload" [disabled]="isPermissionDisabled(user)" inputId="upload-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'upload-'+user.id">Upload Books</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDownload" [disabled]="isPermissionDisabled(user)" inputId="download-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'download-'+user.id">Download Books</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canDeleteBook" [disabled]="isPermissionDisabled(user)" inputId="delete-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'delete-'+user.id">Delete Books</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEditMetadata" [disabled]="isPermissionDisabled(user)" inputId="metadata-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'metadata-'+user.id">Edit Metadata</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageLibrary" [disabled]="isPermissionDisabled(user)" inputId="library-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'library-'+user.id">Manage Library</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canEmailBook" [disabled]="isPermissionDisabled(user)" inputId="email-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'email-'+user.id">Email Books</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-mobile"></i> Device Sync</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKoReader" [disabled]="isPermissionDisabled(user)" inputId="koreader-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'koreader-'+user.id">KOReader Sync</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canSyncKobo" [disabled]="isPermissionDisabled(user)" inputId="kobo-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'kobo-'+user.id">Kobo Sync</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessOpds" [disabled]="isPermissionDisabled(user)" inputId="opds-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'opds-'+user.id">OPDS Feed Access</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-eye"></i> System Access</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessBookdrop" [disabled]="isPermissionDisabled(user)" inputId="bookdrop-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'bookdrop-'+user.id">Access Bookdrop</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessLibraryStats" [disabled]="isPermissionDisabled(user)" inputId="stats-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'stats-'+user.id">View Library Statistics</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessUserStats" [disabled]="isPermissionDisabled(user)" inputId="userstats-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'userstats-'+user.id">View User Reading Statistics</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-cog"></i> System Configuration</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageMetadataConfig" [disabled]="isPermissionDisabled(user)" inputId="metadataconfig-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'metadataconfig-'+user.id">Manage Metadata Configuration</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageGlobalPreferences" [disabled]="isPermissionDisabled(user)" inputId="preferences-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'preferences-'+user.id">Manage Application Preferences</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canAccessTaskManager" [disabled]="isPermissionDisabled(user)" inputId="taskmanager-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'taskmanager-'+user.id">Access Task Manager</label>
|
||||
</div>
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.canManageIcons" [disabled]="isPermissionDisabled(user)" inputId="icons-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'icons-'+user.id">Manage Icons</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="permission-group">
|
||||
<h5><i class="pi pi-shield"></i> Administration</h5>
|
||||
<div class="permission-items">
|
||||
<div class="permission-item">
|
||||
<p-checkbox [binary]="true" [(ngModel)]="user.permissions.admin" (onChange)="onAdminCheckboxChange(user)" [disabled]="!user.isEditing" inputId="admin-{{user.id}}"></p-checkbox>
|
||||
<label [for]="'admin-'+user.id">Full Administrator Access</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
|
||||
@@ -137,12 +137,12 @@
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--p-primary-color);
|
||||
color: white;
|
||||
@@ -151,93 +151,292 @@
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
}
|
||||
|
||||
.user-type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--p-primary-color);
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--p-text-color);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border: 1px solid;
|
||||
border-radius: 12px;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
|
||||
&[data-type="LOCAL"] {
|
||||
color: #3b82f6;
|
||||
background: color-mix(in srgb, #3b82f6 15%, transparent);
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
&[data-type="OIDC"] {
|
||||
color: #8b5cf6;
|
||||
background: color-mix(in srgb, #8b5cf6 15%, transparent);
|
||||
border-color: #8b5cf6;
|
||||
}
|
||||
|
||||
&:not([data-type="LOCAL"]):not([data-type="OIDC"]) {
|
||||
color: #6b7280;
|
||||
background: color-mix(in srgb, #6b7280 15%, transparent);
|
||||
border-color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.library-names {
|
||||
.permission-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--card-background);
|
||||
color: var(--text-color-secondary);
|
||||
transition: all 0.2s;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&.active {
|
||||
background: color-mix(in srgb, var(--primary-color) 20%, var(--card-background));
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
|
||||
.pi {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.admin-indicator {
|
||||
&.active {
|
||||
background: color-mix(in srgb, #10b981 20%, var(--card-background));
|
||||
color: #10b981;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
background: color-mix(in srgb, #6b7280 15%, var(--card-background));
|
||||
color: #6b7280;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.375rem 0.875rem;
|
||||
border-radius: 16px;
|
||||
background: var(--card-background);
|
||||
border: 1px solid var(--border-color);
|
||||
transition: all 0.2s;
|
||||
|
||||
&[data-count="0"] {
|
||||
background: color-mix(in srgb, #6b7280 20%, var(--card-background));
|
||||
border-color: #6b7280;
|
||||
|
||||
.permission-count {
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="1"],
|
||||
&[data-total="6"][data-count="2"],
|
||||
&[data-total="3"][data-count="1"],
|
||||
&[data-total="2"][data-count="1"],
|
||||
&[data-total="4"][data-count="1"] {
|
||||
background: color-mix(in srgb, #ef4444 20%, var(--card-background));
|
||||
border-color: #ef4444;
|
||||
|
||||
.permission-count {
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="3"],
|
||||
&[data-total="6"][data-count="4"],
|
||||
&[data-total="3"][data-count="2"],
|
||||
&[data-total="4"][data-count="2"] {
|
||||
background: color-mix(in srgb, #f59e0b 20%, var(--card-background));
|
||||
border-color: #f59e0b;
|
||||
|
||||
.permission-count {
|
||||
color: #f59e0b;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="5"],
|
||||
&[data-total="4"][data-count="3"] {
|
||||
background: color-mix(in srgb, #3b82f6 20%, var(--card-background));
|
||||
border-color: #3b82f6;
|
||||
|
||||
.permission-count {
|
||||
color: #3b82f6;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-total="6"][data-count="6"],
|
||||
&[data-total="3"][data-count="3"],
|
||||
&[data-total="2"][data-count="2"],
|
||||
&[data-total="4"][data-count="4"] {
|
||||
background: color-mix(in srgb, #10b981 20%, var(--card-background));
|
||||
border-color: #10b981;
|
||||
|
||||
.permission-count {
|
||||
color: #10b981;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.permission-count {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.actions-cell {
|
||||
text-align: center;
|
||||
.actions-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.user-dialog {
|
||||
|
||||
.p-dialog-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-form {
|
||||
.expanded-content {
|
||||
padding: 1.5rem;
|
||||
background: var(--overlay-background);
|
||||
border-radius: 8px;
|
||||
margin: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
.expanded-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.expanded-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.pi {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--p-text-color);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--p-red-50);
|
||||
border: 1px solid var(--p-red-200);
|
||||
border-radius: 4px;
|
||||
color: var(--p-red-700);
|
||||
font-size: 0.875rem;
|
||||
.permissions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.pi {
|
||||
color: var(--p-red-500);
|
||||
.permission-group {
|
||||
h5 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin: 0 0 0.75rem 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
|
||||
.pi {
|
||||
color: var(--primary-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
.permission-items {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.full-name-column {
|
||||
min-width: 150px;
|
||||
width: 150px;
|
||||
.permission-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
p-checkbox {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.email-column {
|
||||
min-width: 150px;
|
||||
width: 200px;
|
||||
.library-names {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.libraries-column {
|
||||
min-width: 200px;
|
||||
width: 250px;
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Button} from 'primeng/button';
|
||||
import {Button, ButtonDirective} from 'primeng/button';
|
||||
import {DynamicDialogRef} from 'primeng/dynamicdialog';
|
||||
import {TableModule} from 'primeng/table';
|
||||
import {LowerCasePipe, TitleCasePipe} from '@angular/common';
|
||||
@@ -29,7 +29,8 @@ import {DialogLauncherService} from '../../../shared/services/dialog-launcher.se
|
||||
Password,
|
||||
LowerCasePipe,
|
||||
TitleCasePipe,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
ButtonDirective
|
||||
],
|
||||
templateUrl: './user-management.component.html',
|
||||
styleUrls: ['./user-management.component.scss'],
|
||||
@@ -46,6 +47,7 @@ export class UserManagementComponent implements OnInit, OnDestroy {
|
||||
currentUser: User | null = null;
|
||||
editingLibraryIds: number[] = [];
|
||||
allLibraries: Library[] = [];
|
||||
expandedRows: { [key: string]: boolean } = {};
|
||||
|
||||
isPasswordDialogVisible = false;
|
||||
selectedUser: User | null = null;
|
||||
@@ -215,4 +217,82 @@ export class UserManagementComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getBookManagementPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canUpload) count++;
|
||||
if (permissions.canDownload) count++;
|
||||
if (permissions.canDeleteBook) count++;
|
||||
if (permissions.canEditMetadata) count++;
|
||||
if (permissions.canManageLibrary) count++;
|
||||
if (permissions.canEmailBook) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
getDeviceSyncPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canSyncKoReader) count++;
|
||||
if (permissions.canSyncKobo) count++;
|
||||
if (permissions.canAccessOpds) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
getSystemAccessPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canAccessBookdrop) count++;
|
||||
if (permissions.canAccessLibraryStats) count++;
|
||||
if (permissions.canAccessUserStats) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
getSystemConfigPermissionsCount(user: User): number {
|
||||
const permissions = user.permissions;
|
||||
let count = 0;
|
||||
if (permissions.canAccessTaskManager) count++;
|
||||
if (permissions.canManageGlobalPreferences) count++;
|
||||
if (permissions.canManageMetadataConfig) count++;
|
||||
if (permissions.canManageIcons) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
toggleRowExpansion(user: User) {
|
||||
if (this.expandedRows[user.id]) {
|
||||
delete this.expandedRows[user.id];
|
||||
} else {
|
||||
this.expandedRows[user.id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
isRowExpanded(user: User): boolean {
|
||||
return this.expandedRows[user.id];
|
||||
}
|
||||
|
||||
onAdminCheckboxChange(user: any) {
|
||||
if (user.permissions.admin) {
|
||||
user.permissions.canUpload = true;
|
||||
user.permissions.canDownload = true;
|
||||
user.permissions.canDeleteBook = true;
|
||||
user.permissions.canEditMetadata = true;
|
||||
user.permissions.canManageLibrary = true;
|
||||
user.permissions.canEmailBook = true;
|
||||
user.permissions.canSyncKoReader = true;
|
||||
user.permissions.canSyncKobo = true;
|
||||
user.permissions.canAccessOpds = true;
|
||||
user.permissions.canAccessBookdrop = true;
|
||||
user.permissions.canAccessLibraryStats = true;
|
||||
user.permissions.canAccessUserStats = true;
|
||||
user.permissions.canManageMetadataConfig = true;
|
||||
user.permissions.canManageGlobalPreferences = true;
|
||||
user.permissions.canAccessTaskManager = true;
|
||||
user.permissions.canManageEmailConfig = true;
|
||||
user.permissions.canManageIcons = true;
|
||||
}
|
||||
}
|
||||
|
||||
isPermissionDisabled(user: any): boolean {
|
||||
return !user.isEditing || user.permissions.admin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,10 +158,19 @@ export interface User {
|
||||
canEmailBook: boolean;
|
||||
canDeleteBook: boolean;
|
||||
canEditMetadata: boolean;
|
||||
canManipulateLibrary: boolean;
|
||||
canManageLibrary: boolean;
|
||||
canManageMetadataConfig: boolean;
|
||||
canSyncKoReader: boolean;
|
||||
canSyncKobo: boolean;
|
||||
canAccessOpds: boolean;
|
||||
canAccessBookdrop: boolean;
|
||||
canAccessLibraryStats: boolean;
|
||||
canAccessUserStats: boolean;
|
||||
canAccessTaskManager: boolean;
|
||||
canManageEmailConfig: boolean;
|
||||
canManageGlobalPreferences: boolean;
|
||||
canManageIcons: boolean;
|
||||
demoUser: boolean;
|
||||
};
|
||||
userSettings: UserSettings;
|
||||
provisioningMethod?: 'LOCAL' | 'OIDC' | 'REMOTE';
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<i class="pi pi-chart-line"></i>
|
||||
<h2>{{ userName ? userName + "'s Reading Statistics" : "Your Reading Statistics" }}</h2>
|
||||
</div>
|
||||
<p class="subtitle">Track your reading habits and progress</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -51,14 +51,6 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
@@ -100,12 +92,6 @@
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.875rem;
|
||||
padding-left: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
@@ -133,12 +119,6 @@
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.8rem;
|
||||
padding-left: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<p-tablist>
|
||||
<p-tab value="0">Prime Icons</p-tab>
|
||||
<p-tab value="1">SVG Icons</p-tab>
|
||||
<p-tab value="2">Add SVG Icon(s)</p-tab>
|
||||
@if (canManageIcons) {
|
||||
<p-tab value="2">Add SVG Icon(s)</p-tab>
|
||||
}
|
||||
</p-tablist>
|
||||
<p-tabpanels>
|
||||
<p-tabpanel value="0">
|
||||
@@ -58,8 +60,8 @@
|
||||
outlined>
|
||||
</p-button>
|
||||
<span class="pagination-info">
|
||||
Page {{ currentSvgPage + 1 }} of {{ totalSvgPages }}
|
||||
</span>
|
||||
Page {{ currentSvgPage + 1 }} of {{ totalSvgPages }}
|
||||
</span>
|
||||
<p-button
|
||||
(onClick)="loadSvgIcons(currentSvgPage + 1)"
|
||||
[disabled]="currentSvgPage >= totalSvgPages - 1"
|
||||
@@ -71,113 +73,116 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style="display: flex; align-items: center; position: fixed; right: 32px; bottom: 32px; z-index: 101;">
|
||||
<div
|
||||
class="svg-trash-area"
|
||||
[class.trash-hover]="isTrashHover"
|
||||
(dragover)="onTrashDragOver($event)"
|
||||
(dragleave)="onTrashDragLeave($event)"
|
||||
(drop)="onTrashDrop($event)"
|
||||
>
|
||||
<i class="pi pi-trash"></i>
|
||||
<span>Drag here to delete icon</span>
|
||||
@if (canManageIcons) {
|
||||
<div style="display: flex; align-items: center; position: fixed; right: 32px; bottom: 32px; z-index: 101;">
|
||||
<div
|
||||
class="svg-trash-area"
|
||||
[class.trash-hover]="isTrashHover"
|
||||
(dragover)="onTrashDragOver($event)"
|
||||
(dragleave)="onTrashDragLeave($event)"
|
||||
(drop)="onTrashDrop($event)">
|
||||
<i class="pi pi-trash"></i>
|
||||
<span>Drag here to delete icon</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
<p-tabpanel value="2">
|
||||
<div class="svg-paste-container">
|
||||
<div class="svg-input-section">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="svgName"
|
||||
placeholder="Enter icon name for saving"
|
||||
class="svg-name-input"
|
||||
/>
|
||||
@if (canManageIcons) {
|
||||
<p-tabpanel value="2">
|
||||
<div class="svg-paste-container">
|
||||
<div class="svg-input-section">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="svgName"
|
||||
placeholder="Enter icon name for saving"
|
||||
class="svg-name-input"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
[(ngModel)]="svgContent"
|
||||
(ngModelChange)="onSvgContentChange()"
|
||||
placeholder="Paste your SVG code here..."
|
||||
class="svg-textarea"
|
||||
rows="8"></textarea>
|
||||
<textarea
|
||||
[(ngModel)]="svgContent"
|
||||
(ngModelChange)="onSvgContentChange()"
|
||||
placeholder="Paste your SVG code here..."
|
||||
class="svg-textarea"
|
||||
rows="8"></textarea>
|
||||
|
||||
@if (svgContent && svgPreview) {
|
||||
<div class="svg-preview-section">
|
||||
<h4 class="preview-title">Preview</h4>
|
||||
<div class="svg-preview" [innerHTML]="svgPreview"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorMessage) {
|
||||
<div class="error-message">{{ errorMessage }}</div>
|
||||
}
|
||||
|
||||
<div class="button-container">
|
||||
<p-button
|
||||
(onClick)="addSvgEntry()"
|
||||
[disabled]="!svgContent || !svgName"
|
||||
label="Add to Queue"
|
||||
severity="primary"
|
||||
outlined
|
||||
icon="pi pi-plus">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (svgEntries.length > 0) {
|
||||
<div class="svg-entries-section">
|
||||
<div class="entries-header">
|
||||
<h4 class="entries-title">
|
||||
Queued Icons ({{ svgEntries.length }})
|
||||
</h4>
|
||||
<p-button
|
||||
(onClick)="clearAllEntries()"
|
||||
label="Clear All"
|
||||
severity="danger"
|
||||
[text]="true"
|
||||
icon="pi pi-times">
|
||||
</p-button>
|
||||
</div>
|
||||
|
||||
<div class="entries-grid">
|
||||
@for (entry of svgEntries; track entry.name; let i = $index) {
|
||||
<div class="entry-card" [class.has-error]="entry.error">
|
||||
<div class="entry-header">
|
||||
<span class="entry-name">{{ entry.name }}</span>
|
||||
<button
|
||||
class="entry-remove"
|
||||
(click)="removeSvgEntry(i)"
|
||||
title="Remove">
|
||||
<i class="pi pi-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="entry-preview" [innerHTML]="entry.preview"></div>
|
||||
@if (entry.error) {
|
||||
<div class="entry-error">{{ entry.error }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (batchErrorMessage) {
|
||||
<div class="error-message">{{ batchErrorMessage }}</div>
|
||||
@if (svgContent && svgPreview) {
|
||||
<div class="svg-preview-section">
|
||||
<h4 class="preview-title">Preview</h4>
|
||||
<div class="svg-preview" [innerHTML]="svgPreview"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="batch-actions">
|
||||
@if (errorMessage) {
|
||||
<div class="error-message">{{ errorMessage }}</div>
|
||||
}
|
||||
|
||||
<div class="button-container">
|
||||
<p-button
|
||||
(onClick)="saveAllSvgs()"
|
||||
[loading]="isSavingBatch"
|
||||
[disabled]="svgEntries.length === 0"
|
||||
label="Save All Icons"
|
||||
severity="success"
|
||||
icon="pi pi-save">
|
||||
(onClick)="addSvgEntry()"
|
||||
[disabled]="!svgContent || !svgName"
|
||||
label="Add to Queue"
|
||||
severity="primary"
|
||||
outlined
|
||||
icon="pi pi-plus">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
|
||||
@if (svgEntries.length > 0) {
|
||||
<div class="svg-entries-section">
|
||||
<div class="entries-header">
|
||||
<h4 class="entries-title">
|
||||
Queued Icons ({{ svgEntries.length }})
|
||||
</h4>
|
||||
<p-button
|
||||
(onClick)="clearAllEntries()"
|
||||
label="Clear All"
|
||||
severity="danger"
|
||||
[text]="true"
|
||||
icon="pi pi-times">
|
||||
</p-button>
|
||||
</div>
|
||||
|
||||
<div class="entries-grid">
|
||||
@for (entry of svgEntries; track entry.name; let i = $index) {
|
||||
<div class="entry-card" [class.has-error]="entry.error">
|
||||
<div class="entry-header">
|
||||
<span class="entry-name">{{ entry.name }}</span>
|
||||
<button
|
||||
class="entry-remove"
|
||||
(click)="removeSvgEntry(i)"
|
||||
title="Remove">
|
||||
<i class="pi pi-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="entry-preview" [innerHTML]="entry.preview"></div>
|
||||
@if (entry.error) {
|
||||
<div class="entry-error">{{ entry.error }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (batchErrorMessage) {
|
||||
<div class="error-message">{{ batchErrorMessage }}</div>
|
||||
}
|
||||
|
||||
<div class="batch-actions">
|
||||
<p-button
|
||||
(onClick)="saveAllSvgs()"
|
||||
[loading]="isSavingBatch"
|
||||
[disabled]="svgEntries.length === 0"
|
||||
label="Save All Icons"
|
||||
severity="success"
|
||||
icon="pi pi-save">
|
||||
</p-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</p-tabpanel>
|
||||
}
|
||||
</p-tabpanels>
|
||||
</p-tabs>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {MessageService} from 'primeng/api';
|
||||
import {IconCategoriesHelper} from '../../helpers/icon-categories.helper';
|
||||
import {Button} from 'primeng/button';
|
||||
import {TabsModule} from 'primeng/tabs';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
|
||||
interface SvgEntry {
|
||||
name: string;
|
||||
@@ -65,6 +66,7 @@ export class IconPickerComponent implements OnInit {
|
||||
sanitizer = inject(DomSanitizer);
|
||||
urlHelper = inject(UrlHelperService);
|
||||
messageService = inject(MessageService);
|
||||
userService = inject(UserService);
|
||||
|
||||
searchText: string = '';
|
||||
selectedIcon: string | null = null;
|
||||
@@ -399,4 +401,9 @@ export class IconPickerComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get canManageIcons(): boolean {
|
||||
const user = this.userService.getCurrentUser();
|
||||
return user?.permissions.canManageIcons || user?.permissions.admin || false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export class AppMenuitemComponent implements OnInit, OnDestroy {
|
||||
) {
|
||||
this.userService.userState$.subscribe(userState => {
|
||||
if (userState?.user) {
|
||||
this.canManipulateLibrary = userState.user.permissions.canManipulateLibrary;
|
||||
this.canManipulateLibrary = userState.user.permissions.canManageLibrary;
|
||||
this.admin = userState.user.permissions.admin;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,46 +30,50 @@
|
||||
<ul class="topbar-items hidden md:flex items-center gap-3 ml-auto pl-4">
|
||||
<div class="flex gap-4">
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
<li>
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
@if (userState.user?.permissions?.canAccessBookdrop || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<a class="topbar-item" (click)="navigateToBookdrop()" pTooltip="Bookdrop" tooltipPosition="bottom">
|
||||
<i class="pi pi-inbox text-surface-100"></i>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
</li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<a class="topbar-item" (click)="openLibraryCreatorDialog()" pTooltip="Create New Library" tooltipPosition="bottom">
|
||||
<i class="pi pi-plus-circle text-surface-100"></i>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
@if (userState.user?.permissions?.canUpload || userState.user?.permissions?.admin) {
|
||||
</li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canUpload || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<a class="topbar-item" (click)="openFileUploadDialog()" pTooltip="Upload Book" tooltipPosition="bottom">
|
||||
<i class="pi pi-upload text-surface-100"></i>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if (hasStatsAccess) {
|
||||
<li>
|
||||
<button
|
||||
class="topbar-item"
|
||||
(click)="shouldShowStatsMenu ? statsMenu.toggle($event) : handleStatsButtonClick($event)"
|
||||
[pTooltip]="statsTooltip"
|
||||
tooltipPosition="bottom">
|
||||
<i class="pi pi-chart-bar text-surface-100"></i>
|
||||
</button>
|
||||
@if (shouldShowStatsMenu) {
|
||||
<p-menu #statsMenu [model]="statsMenuItems" [popup]="true" appendTo="body" />
|
||||
}
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<button
|
||||
class="topbar-item"
|
||||
(click)="statsMenu.toggle($event)"
|
||||
pTooltip="Stats"
|
||||
tooltipPosition="bottom">
|
||||
<i class="pi pi-chart-bar text-surface-100"></i>
|
||||
</button>
|
||||
<p-menu #statsMenu [model]="statsMenuItems" [popup]="true" appendTo="body" />
|
||||
</li>
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
<li>
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<a class="topbar-item" (click)="navigateToMetadataManager()" pTooltip="Metadata Manager" tooltipPosition="bottom">
|
||||
<i class="pi pi-sparkles text-surface-100"></i>
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<li>
|
||||
<a class="topbar-item" (click)="navigateToSettings()" pTooltip="Settings" tooltipPosition="bottom">
|
||||
@@ -140,11 +144,18 @@
|
||||
<i class="pi pi-info-circle text-surface-100"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button class="topbar-item" (click)="openUserProfileDialog()" pTooltip="Profile" tooltipPosition="bottom">
|
||||
<i class="pi pi-user text-surface-100"></i>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<!-- Show Profile only to demo users -->
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (!userState.user?.permissions?.demoUser) {
|
||||
<li>
|
||||
<button class="topbar-item" (click)="openUserProfileDialog()" pTooltip="Profile" tooltipPosition="bottom">
|
||||
<i class="pi pi-user text-surface-100"></i>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
<li>
|
||||
<button class="topbar-item" (click)="logout()" pTooltip="Logout" tooltipPosition="left">
|
||||
<i class="pi pi-sign-out text-surface-100"></i>
|
||||
@@ -162,16 +173,7 @@
|
||||
<p-popover #mobileMenu>
|
||||
<ul class="flex flex-col gap-1 w-48">
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (userState.user?.permissions?.canManipulateLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openLibraryCreatorDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-plus-circle text-surface-100"></i>
|
||||
Create Library
|
||||
</button>
|
||||
</li>
|
||||
@if (userState.user?.permissions?.canAccessBookdrop || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
@@ -182,6 +184,17 @@
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openLibraryCreatorDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-plus-circle text-surface-100"></i>
|
||||
Create Library
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
@if (userState.user?.permissions?.canUpload || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
@@ -194,16 +207,33 @@
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="statsMenu.toggle($event)"
|
||||
>
|
||||
<i class="pi pi-chart-bar text-surface-100"></i>
|
||||
Charts
|
||||
</button>
|
||||
<p-menu #statsMenuMobile [model]="statsMenuItems" [popup]="true" />
|
||||
</li>
|
||||
@if (hasStatsAccess) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="shouldShowStatsMenu ? statsMenuMobile.toggle($event) : handleStatsButtonClick($event)"
|
||||
>
|
||||
<i class="pi pi-chart-bar text-surface-100"></i>
|
||||
Charts
|
||||
</button>
|
||||
@if (shouldShowStatsMenu) {
|
||||
<p-menu #statsMenuMobile [model]="statsMenuItems" [popup]="true" />
|
||||
}
|
||||
</li>
|
||||
}
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (userState.user?.permissions?.canManageLibrary || userState.user?.permissions?.admin) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="navigateToMetadataManager(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-sparkles text-surface-100"></i>
|
||||
Metadata Manager
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
@@ -232,15 +262,21 @@
|
||||
Support BookLore
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openUserProfileDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-user text-surface-100"></i>
|
||||
Profile
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@if (userService.userState$ | async; as userState) {
|
||||
@if (!userState.user?.permissions?.demoUser) {
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
(click)="openUserProfileDialog(); mobileMenu.hide()"
|
||||
>
|
||||
<i class="pi pi-user text-surface-100"></i>
|
||||
Profile
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center gap-2 w-full text-left p-2 hover:bg-surface-200 dark:hover:bg-surface-700 rounded"
|
||||
|
||||
@@ -57,6 +57,8 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
@ViewChild('menubutton') menuButton!: ElementRef;
|
||||
@ViewChild('topbarmenubutton') topbarMenuButton!: ElementRef;
|
||||
@ViewChild('topbarmenu') menu!: ElementRef;
|
||||
@ViewChild('statsMenu') statsMenu: any;
|
||||
@ViewChild('statsMenuMobile') statsMenuMobile: any;
|
||||
|
||||
isMenuVisible = true;
|
||||
progressHighlight = false;
|
||||
@@ -83,7 +85,6 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
private bookdropFileService: BookdropFileService,
|
||||
private dialogLauncher: DialogLauncherService
|
||||
) {
|
||||
this.initializeStatsMenu();
|
||||
this.subscribeToMetadataProgress();
|
||||
this.subscribeToNotifications();
|
||||
|
||||
@@ -104,6 +105,12 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
this.updateCompletedTaskCount();
|
||||
this.updateTaskVisibilityWithBookdrop();
|
||||
});
|
||||
|
||||
this.userService.userState$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.initializeStatsMenu();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -158,6 +165,16 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
this.authService.logout();
|
||||
}
|
||||
|
||||
handleStatsButtonClick(event: Event) {
|
||||
if (this.statsMenuItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.statsMenuItems.length === 1) {
|
||||
this.statsMenuItems[0].command?.({originalEvent: event, item: this.statsMenuItems[0]});
|
||||
}
|
||||
}
|
||||
|
||||
private subscribeToMetadataProgress() {
|
||||
this.metadataProgressService.progressUpdates$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -200,18 +217,44 @@ export class AppTopBarComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
private initializeStatsMenu() {
|
||||
this.statsMenuItems = [
|
||||
{
|
||||
const userState = this.userService.userStateSubject.value;
|
||||
const user = userState.user;
|
||||
|
||||
this.statsMenuItems = [];
|
||||
|
||||
if (user?.permissions?.canAccessLibraryStats || user?.permissions?.admin) {
|
||||
this.statsMenuItems.push({
|
||||
label: 'Library Stats',
|
||||
icon: 'pi pi-chart-line',
|
||||
command: () => this.navigateToStats()
|
||||
},
|
||||
{
|
||||
});
|
||||
}
|
||||
|
||||
if (user?.permissions?.canAccessUserStats || user?.permissions?.admin) {
|
||||
this.statsMenuItems.push({
|
||||
label: 'Reading Stats',
|
||||
icon: 'pi pi-users',
|
||||
command: () => this.navigateToUserStats()
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get hasStatsAccess(): boolean {
|
||||
return this.statsMenuItems.length > 0;
|
||||
}
|
||||
|
||||
get shouldShowStatsMenu(): boolean {
|
||||
return this.statsMenuItems.length > 1;
|
||||
}
|
||||
|
||||
get statsTooltip(): string {
|
||||
if (this.statsMenuItems.length === 0) {
|
||||
return 'Stats';
|
||||
}
|
||||
if (this.statsMenuItems.length === 1) {
|
||||
return this.statsMenuItems[0].label || 'Stats';
|
||||
}
|
||||
return 'Stats';
|
||||
}
|
||||
|
||||
get iconClass(): string {
|
||||
|
||||
@@ -184,7 +184,7 @@ export class DialogLauncherService {
|
||||
openIconPickerDialog(): DynamicDialogRef | null {
|
||||
return this.openDialog(IconPickerComponent, {
|
||||
header: 'Choose an Icon',
|
||||
styleClass: 'dialog-maximal',
|
||||
styleClass: 'dialog-medium',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user