diff --git a/apps/signage-manager/USER_STORIES.md b/apps/signage-manager/USER_STORIES.md index f771cd95106..e8ce7068ac9 100644 --- a/apps/signage-manager/USER_STORIES.md +++ b/apps/signage-manager/USER_STORIES.md @@ -311,10 +311,11 @@ These stories cover the current app workflows: **Acceptance Criteria:** -- The groups page is available only when the user can manage signage groups. +- The groups page is available only when the user can manage signage groups. Other users who open its address go to the media library. - Manageable groups appear in a searchable tree. -- Expanding a group loads and shows child groups. -- Selecting a group opens its users and zones panels. +- Expanding a group shows its child groups. +- Selecting a group opens its users and zones panels. On mobile, the back button returns to the group list. +- After a save, the selected group and the expanded groups stay as they were, also when the group list cannot load again. When the group list cannot load at all, the page shows an error. - Users with manage-all-groups permission can create a new group. - Selected groups can be edited or removed. @@ -329,7 +330,7 @@ These stories cover the current app workflows: **Acceptance Criteria:** - The users panel lists assigned users with name, email, and permission labels. -- Users can add a user not already assigned to the group. The user gets the default permissions of the group. +- Users can add a user not already assigned to the group. The user gets the default permissions of the group. The user search shows a loading state, and an error when the search fails. - Users can edit an assigned user's signage permissions. - Users can remove an assigned user from the group. - Empty state appears when no users are assigned. diff --git a/apps/signage-manager/src/app/app.config.ts b/apps/signage-manager/src/app/app.config.ts index 7ddcd1798ac..37169a415ed 100644 --- a/apps/signage-manager/src/app/app.config.ts +++ b/apps/signage-manager/src/app/app.config.ts @@ -19,7 +19,7 @@ import { UnauthorisedComponent, } from '@placeos/components'; import { environment } from '../environments/environment'; -import { signageAccessGuard } from './signage-access.guard'; +import { manageGroupsGuard, signageAccessGuard } from './signage-access.guard'; import { templatesEnabledGuard } from './templates-enabled.guard'; import { templateUnsavedGuard } from './templates/template-unsaved.guard'; @@ -121,6 +121,7 @@ const APP_ROUTES: Routes = [ { path: 'branding', redirectTo: 'manage/branding' }, { path: 'groups', + canActivate: [manageGroupsGuard], loadComponent: () => import('./groups/groups.component').then( (m) => m.GroupsSectionComponent, diff --git a/apps/signage-manager/src/app/groups/signage-group-access-modal.component.ts b/apps/signage-manager/src/app/groups/signage-group-access-modal.component.ts index ba66a0dfd4b..d9a13447b9a 100644 --- a/apps/signage-manager/src/app/groups/signage-group-access-modal.component.ts +++ b/apps/signage-manager/src/app/groups/signage-group-access-modal.component.ts @@ -28,9 +28,9 @@ import { PlaceGroup, PlaceGroupAdMappings } from '@placeos/ts-client'; import { adGroupKey } from '../signage-group-access'; import { dialogClosed } from '../signage-service.util'; import { SignageGroupAdminService } from './signage-group-admin.service'; +import { SignageGroupPermissionLabelsComponent } from './signage-group-permission-labels.component'; import { GROUP_PERMISSION_FLAGS, - groupPermissionLabels, SignageGroupPermissionsModalComponent, } from './signage-group-permissions-modal.component'; @@ -133,26 +133,9 @@ interface AdGroupRow {
- @let labels = - permissionLabels( - row.permissions - ); - @if (labels.length) { - @for ( - label of labels; - track label - ) { - {{ label | translate }} - @if (!$last) { - , - } - } - } @else { - {{ - 'SIGNAGE_MANAGER.DEFAULT_PERMISSIONS' - | translate - }} - } +
} - + `, }) export class SignageGroupTabsComponent { private readonly _group_admin = inject(SignageGroupAdminService); + private readonly _element = inject>(ElementRef); public readonly active_tab = this._group_admin.managed_group_tab; - public readonly tabs = [ - { id: 'users' as const, label: 'SIGNAGE_MANAGER.TAB_USERS' }, - { id: 'zones' as const, label: 'SIGNAGE_MANAGER.TAB_ZONES' }, + public readonly tabs: { id: GroupTab; label: string }[] = [ + { id: 'users', label: 'SIGNAGE_MANAGER.TAB_USERS' }, + { id: 'zones', label: 'SIGNAGE_MANAGER.TAB_ZONES' }, ]; + + /** Arrow keys, Home and End move between the tabs, as in a tab list */ + public onKeydown(event: KeyboardEvent) { + const index = this.tabs.findIndex(({ id }) => id === this.active_tab()); + const last = this.tabs.length - 1; + const targets: Record = { + ArrowLeft: index > 0 ? index - 1 : last, + ArrowRight: index < last ? index + 1 : 0, + Home: 0, + End: last, + }; + const next = targets[event.key]; + if (next === undefined) return; + event.preventDefault(); + const tab = this.tabs[next].id; + this.active_tab.set(tab); + this._element.nativeElement + .querySelector(`#group-${tab}-tab`) + ?.focus(); + } } diff --git a/apps/signage-manager/src/app/groups/signage-group-user-select-modal.component.ts b/apps/signage-manager/src/app/groups/signage-group-user-select-modal.component.ts index eba645c89ac..7505f4b1efc 100644 --- a/apps/signage-manager/src/app/groups/signage-group-user-select-modal.component.ts +++ b/apps/signage-manager/src/app/groups/signage-group-user-select-modal.component.ts @@ -11,6 +11,7 @@ import { MatRippleModule } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { IconComponent, TranslatePipe } from '@placeos/components'; import { SignageGroupAdminService } from './signage-group-admin.service'; @@ -49,7 +50,21 @@ import { SignageGroupAdminService } from './signage-group-admin.service'; " /> - @if (users().length > 0) { + @if (loading()) { +
+ +
+ } @else if (failed()) { + + } @else if (users().length > 0) { @for (user of users(); track user.id || user.email) {
+ @if (failed() && users().length) { + + } @if (users().length) { @for (row of users(); track row.user_id) {
- @let labels = - permissionLabels(row.permissions); - @if (labels.length) { - @for (label of labels; track label) { - {{ label | translate }} - @if (!$last) { - , - } - } - } @else { - {{ - 'SIGNAGE_MANAGER.DEFAULT_PERMISSIONS' - | translate - }} - } +