frontend: add room detail page with loading state and actions
This commit is contained in:
parent
0e37d8cc09
commit
35e727cbd0
@ -158,6 +158,7 @@
|
||||
[checked]="allSelected()"
|
||||
[indeterminate]="someSelected()"
|
||||
(change)="toggleAllSelection()"
|
||||
(click)="$event.stopPropagation()"
|
||||
[disabled]="loading()"
|
||||
id="select-all-checkbox"
|
||||
>
|
||||
@ -167,6 +168,7 @@
|
||||
@if (canSelectRoom(room)) {
|
||||
<mat-checkbox
|
||||
[checked]="isRoomSelected(room)"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="toggleRoomSelection(room)"
|
||||
[disabled]="loading()"
|
||||
id="select-room-{{ room.roomId }}"
|
||||
@ -308,7 +310,7 @@
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="Open Room"
|
||||
(click)="openRoom(room)"
|
||||
(click)="$event.stopPropagation(); openRoom(room)"
|
||||
[disabled]="loading()"
|
||||
class="primary-action"
|
||||
id="open-room-btn-{{ room.roomId }}"
|
||||
@ -322,7 +324,7 @@
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="Room Settings"
|
||||
(click)="editRoom(room)"
|
||||
(click)="$event.stopPropagation(); editRoom(room)"
|
||||
[disabled]="loading()"
|
||||
id="edit-room-btn-{{ room.roomId }}"
|
||||
>
|
||||
@ -344,7 +346,7 @@
|
||||
<mat-menu #actionsMenu="matMenu" id="actions-menu-{{ room.roomId }}">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="viewRecordings(room)"
|
||||
(click)="$event.stopPropagation(); viewRecordings(room)"
|
||||
id="view-recordings-btn-{{ room.roomId }}"
|
||||
>
|
||||
<mat-icon class="ov-recording-icon">video_library</mat-icon>
|
||||
@ -354,7 +356,7 @@
|
||||
@if (canOpenRoom(room)) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="copyModeratorLink(room)"
|
||||
(click)="$event.stopPropagation(); copyModeratorLink(room)"
|
||||
id="copy-moderator-link-btn-{{ room.roomId }}"
|
||||
>
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
@ -362,7 +364,7 @@
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="copySpeakerLink(room)"
|
||||
(click)="$event.stopPropagation(); copySpeakerLink(room)"
|
||||
id="copy-speaker-link-btn-{{ room.roomId }}"
|
||||
>
|
||||
<mat-icon>content_copy</mat-icon>
|
||||
@ -374,7 +376,7 @@
|
||||
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="toggleRoomStatus(room)"
|
||||
(click)="$event.stopPropagation(); toggleRoomStatus(room)"
|
||||
[ngClass]="getRoomToggleIconClass(room)"
|
||||
id="toggle-room-status-btn-{{ room.roomId }}"
|
||||
>
|
||||
@ -383,7 +385,7 @@
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="deleteRoom(room)"
|
||||
(click)="$event.stopPropagation(); deleteRoom(room)"
|
||||
class="delete-action"
|
||||
id="delete-room-btn-{{ room.roomId }}"
|
||||
>
|
||||
@ -401,6 +403,7 @@
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[class.selected-row]="isRoomSelected(row)"
|
||||
id="table-row-{{ row.roomId }}"
|
||||
(click)="onRoomClick(row)"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -114,6 +114,7 @@ export class RoomsListsComponent implements OnInit {
|
||||
@Output() filterChange = new EventEmitter<RoomTableFilter>();
|
||||
@Output() loadMore = new EventEmitter<RoomTableFilter>();
|
||||
@Output() refresh = new EventEmitter<RoomTableFilter>();
|
||||
@Output() roomClicked = new EventEmitter<string>();
|
||||
|
||||
// Filter controls
|
||||
nameFilterControl = new FormControl('');
|
||||
@ -151,7 +152,6 @@ export class RoomsListsComponent implements OnInit {
|
||||
const currentSelection = untracked(() => this.selectedRooms());
|
||||
const filteredSelection = new Set([...currentSelection].filter((id) => validRoomIds.has(id)));
|
||||
|
||||
|
||||
// Only update if the selection has actually changed
|
||||
if (!setsAreEqual(filteredSelection, currentSelection)) {
|
||||
this.selectedRooms.set(filteredSelection);
|
||||
@ -261,6 +261,10 @@ export class RoomsListsComponent implements OnInit {
|
||||
this.roomAction.emit({ rooms: [room], action: 'open' });
|
||||
}
|
||||
|
||||
onRoomClick(room: MeetRoom) {
|
||||
this.roomClicked.emit(room.roomId);
|
||||
}
|
||||
|
||||
editRoom(room: MeetRoom) {
|
||||
this.roomAction.emit({ rooms: [room], action: 'edit' });
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export * from './room-basic-creation/room-basic-creation.component';
|
||||
export * from './room-detail/room-detail.component';
|
||||
export * from './room-wizard/room-wizard.component';
|
||||
export * from './rooms/rooms.component';
|
||||
|
||||
|
||||
@ -0,0 +1,127 @@
|
||||
<!-- Loading State -->
|
||||
@if (isLoading()) {
|
||||
<div class="ov-page-loading">
|
||||
<div class="loading-content">
|
||||
<div class="loading-header">
|
||||
<div class="loading-title">
|
||||
<mat-icon class="ov-room-icon loading-icon">video_chat</mat-icon>
|
||||
<h1>Loading Room Details</h1>
|
||||
</div>
|
||||
<p class="loading-subtitle">Please wait while we fetch the room information...</p>
|
||||
</div>
|
||||
|
||||
<div class="loading-spinner-container">
|
||||
<mat-spinner diameter="48"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isLoading() && room()) {
|
||||
<div class="ov-page-container ov-mb-xxl">
|
||||
<!-- Room Detail Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-top">
|
||||
<!-- Breadcrumb -->
|
||||
<ov-breadcrumb [items]="breadcrumbItems()"></ov-breadcrumb>
|
||||
</div>
|
||||
|
||||
<div class="header-content">
|
||||
<div class="title-section">
|
||||
<div class="title">
|
||||
<mat-icon class="ov-room-icon">video_chat</mat-icon>
|
||||
<h1>{{ room()?.roomName }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- Status Badge -->
|
||||
<div class="status-badge" [class.active]="isActiveRoom()" [class.closed]="isClosedRoom()">
|
||||
<span class="status-dot" [class.active]="isActiveRoom()"></span>
|
||||
<span class="status-text">{{ room()?.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="actions">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="action-btn primary-btn"
|
||||
(click)="joinRoom()"
|
||||
matTooltip="Join the room"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
<span>Join Room</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="action-btn"
|
||||
(click)="copyAccessLink()"
|
||||
matTooltip="Copy access link to clipboard"
|
||||
>
|
||||
<mat-icon>link</mat-icon>
|
||||
<span>Copy Link</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
class="icon-action-btn"
|
||||
(click)="editRoom()"
|
||||
matTooltip="Edit room configuration"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-icon-button
|
||||
class="icon-action-btn delete-btn"
|
||||
(click)="deleteRoom()"
|
||||
matTooltip="Delete room"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Room Information Section -->
|
||||
<div class="page-content">
|
||||
<mat-card class="info-card">
|
||||
<mat-card-content>
|
||||
<div class="info-grid">
|
||||
<!-- Room ID -->
|
||||
<div class="info-item">
|
||||
<p class="info-label">Room ID</p>
|
||||
<p class="info-value monospace">{{ room()?.roomId }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Owner -->
|
||||
<div class="info-item">
|
||||
<p class="info-label">Owner</p>
|
||||
<div class="owner-info">
|
||||
<div class="owner-avatar">
|
||||
<span>{{ getOwnerInitials() }}</span>
|
||||
</div>
|
||||
<p class="info-value">{{ room()?.owner }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creation Date -->
|
||||
<div class="info-item">
|
||||
<p class="info-label">Creation Date</p>
|
||||
<p class="info-value">{{ getFormattedDate(room()!.creationDate) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Auto-deletion -->
|
||||
<div class="info-item">
|
||||
<p class="info-label">Auto-deletion</p>
|
||||
<p class="info-value" [class.disabled]="!room()?.autoDeletionDate">
|
||||
{{ getAutoDeletionStatus() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@ -0,0 +1,199 @@
|
||||
@use '../../../../../../../../src/assets/styles/design-tokens' as *;
|
||||
|
||||
.page-header {
|
||||
@include ov-page-header;
|
||||
|
||||
.header-top {
|
||||
margin-bottom: var(--ov-meet-spacing-sm);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ov-meet-spacing-md);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ov-meet-spacing-xs);
|
||||
padding: var(--ov-meet-spacing-xs) var(--ov-meet-spacing-sm);
|
||||
border-radius: var(--ov-meet-radius-md);
|
||||
font-size: var(--ov-meet-font-size-xs);
|
||||
font-weight: var(--ov-meet-font-weight-semibold);
|
||||
text-transform: uppercase;
|
||||
width: fit-content;
|
||||
|
||||
&.active {
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
color: var(--ov-meet-color-success);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
&.closed {
|
||||
background-color: rgba(158, 158, 158, 0.1);
|
||||
color: var(--ov-meet-text-secondary);
|
||||
border: 1px solid rgba(158, 158, 158, 0.3);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--ov-meet-radius-circle);
|
||||
background-color: var(--ov-meet-text-secondary);
|
||||
|
||||
&.active {
|
||||
background-color: var(--ov-meet-color-success);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
align-items: center;
|
||||
|
||||
.action-btn {
|
||||
@include ov-button-base;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ov-meet-spacing-xs);
|
||||
|
||||
mat-icon {
|
||||
@include ov-icon(sm);
|
||||
}
|
||||
|
||||
span {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-action-btn {
|
||||
@include ov-theme-transition;
|
||||
border: 1px solid var(--ov-meet-border-color);
|
||||
border-radius: var(--ov-meet-radius-sm);
|
||||
|
||||
mat-icon {
|
||||
@include ov-icon(sm);
|
||||
}
|
||||
|
||||
&.delete-btn {
|
||||
color: var(--ov-meet-color-error);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
margin-top: var(--ov-meet-spacing-lg);
|
||||
|
||||
.info-card {
|
||||
@include ov-card;
|
||||
|
||||
mat-card-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--ov-meet-spacing-lg);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ov-meet-spacing-xs);
|
||||
|
||||
.info-label {
|
||||
font-size: var(--ov-meet-font-size-xs);
|
||||
font-weight: var(--ov-meet-font-weight-semibold);
|
||||
color: var(--ov-meet-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: var(--ov-meet-font-size-sm);
|
||||
color: var(--ov-meet-text-primary);
|
||||
margin: 0;
|
||||
font-weight: var(--ov-meet-font-weight-medium);
|
||||
|
||||
&.monospace {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: var(--ov-meet-font-size-xs);
|
||||
color: var(--ov-meet-text-secondary);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--ov-meet-text-hint);
|
||||
}
|
||||
}
|
||||
|
||||
.owner-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
|
||||
.owner-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--ov-meet-radius-circle);
|
||||
background-color: var(--ov-meet-color-primary);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--ov-meet-font-size-xs);
|
||||
font-weight: var(--ov-meet-font-weight-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,175 @@
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { MeetRoom, MeetRoomStatus } from '@openvidu-meet/typings';
|
||||
import { ILogger, LoggerService } from 'openvidu-components-angular';
|
||||
import { BreadcrumbComponent, BreadcrumbItem } from '../../../../shared/components/breadcrumb/breadcrumb.component';
|
||||
import { NavigationService } from '../../../../shared/services/navigation.service';
|
||||
import { NotificationService } from '../../../../shared/services/notification.service';
|
||||
import { DeleteRoomDialogComponent } from '../../components/delete-room-dialog/delete-room-dialog.component';
|
||||
import { RoomService } from '../../services/room.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ov-room-detail',
|
||||
imports: [
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatTooltipModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatChipsModule,
|
||||
RouterModule,
|
||||
BreadcrumbComponent
|
||||
],
|
||||
templateUrl: './room-detail.component.html',
|
||||
styleUrl: './room-detail.component.scss'
|
||||
})
|
||||
export class RoomDetailComponent implements OnInit {
|
||||
room = signal<MeetRoom | null>(null);
|
||||
isLoading = signal(true);
|
||||
breadcrumbItems = signal<BreadcrumbItem[]>([]);
|
||||
protected log: ILogger;
|
||||
MeetRoomStatus = MeetRoomStatus;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private roomService: RoomService,
|
||||
private notificationService: NotificationService,
|
||||
protected navigationService: NavigationService,
|
||||
private clipboard: Clipboard,
|
||||
private dialog: MatDialog,
|
||||
protected loggerService: LoggerService
|
||||
) {
|
||||
this.log = this.loggerService.get('OpenVidu Meet - RoomDetailComponent');
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const roomId = this.route.snapshot.paramMap.get('roomId');
|
||||
if (!roomId) {
|
||||
this.notificationService.showSnackbar('Room ID is required');
|
||||
await this.navigationService.navigateTo('rooms');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadRoomDetails(roomId);
|
||||
}
|
||||
|
||||
private async loadRoomDetails(roomId: string) {
|
||||
try {
|
||||
this.isLoading.set(true);
|
||||
const room = await this.roomService.getRoom(roomId);
|
||||
debugger;
|
||||
this.room.set(room);
|
||||
|
||||
// Update breadcrumb items
|
||||
this.breadcrumbItems.set([
|
||||
{
|
||||
label: 'Rooms',
|
||||
action: () => this.navigationService.navigateTo('rooms')
|
||||
},
|
||||
{
|
||||
label: room.roomName
|
||||
}
|
||||
]);
|
||||
} catch (error) {
|
||||
this.log.e('Error loading room details:', error);
|
||||
this.notificationService.showSnackbar('Failed to load room details');
|
||||
await this.navigationService.navigateTo('rooms');
|
||||
} finally {
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
async joinRoom() {
|
||||
const room = this.room();
|
||||
if (!room) return;
|
||||
|
||||
window.open(room.accessUrl, '_blank');
|
||||
}
|
||||
|
||||
copyAccessLink() {
|
||||
const room = this.room();
|
||||
if (!room) return;
|
||||
|
||||
this.clipboard.copy(room.accessUrl);
|
||||
this.notificationService.showSnackbar('Access link copied to clipboard');
|
||||
}
|
||||
|
||||
async editRoom() {
|
||||
const room = this.room();
|
||||
if (!room) return;
|
||||
|
||||
await this.navigationService.navigateTo(`rooms/${room.roomId}/edit`);
|
||||
}
|
||||
|
||||
async deleteRoom() {
|
||||
const room = this.room();
|
||||
if (!room) return;
|
||||
|
||||
const dialogRef = this.dialog.open(DeleteRoomDialogComponent, {
|
||||
data: {
|
||||
rooms: [room],
|
||||
hasMeetings: room.status === MeetRoomStatus.ACTIVE_MEETING,
|
||||
hasRecordings: false // You may need to check this separately
|
||||
},
|
||||
width: '500px',
|
||||
disableClose: true
|
||||
});
|
||||
|
||||
const result = await dialogRef.afterClosed().toPromise();
|
||||
|
||||
if (result?.confirmed) {
|
||||
try {
|
||||
await this.roomService.deleteRoom(room.roomId, result.deletionPolicy);
|
||||
this.notificationService.showSnackbar('Room deleted successfully');
|
||||
await this.navigationService.navigateTo('rooms');
|
||||
} catch (error) {
|
||||
this.log.e('Error deleting room:', error);
|
||||
this.notificationService.showSnackbar('Failed to delete room');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFormattedDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
getAutoDeletionStatus(): string {
|
||||
const room = this.room();
|
||||
if (!room) return 'N/A';
|
||||
|
||||
if (room.autoDeletionDate) {
|
||||
return this.getFormattedDate(room.autoDeletionDate);
|
||||
}
|
||||
|
||||
return 'Disabled';
|
||||
}
|
||||
|
||||
isActiveRoom(): boolean {
|
||||
return this.room()?.status === MeetRoomStatus.ACTIVE_MEETING;
|
||||
}
|
||||
|
||||
isClosedRoom(): boolean {
|
||||
return this.room()?.status === MeetRoomStatus.CLOSED;
|
||||
}
|
||||
|
||||
getOwnerInitials(): string {
|
||||
const room = this.room();
|
||||
if (!room || !room.owner) return '';
|
||||
return room.owner.substring(0, 2).toUpperCase();
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,7 @@
|
||||
[showLoadMore]="hasMoreRooms"
|
||||
[initialFilters]="initialFilters()"
|
||||
(roomAction)="onRoomAction($event)"
|
||||
(roomClicked)="onRoomClick($event)"
|
||||
(loadMore)="loadMoreRooms($event)"
|
||||
(refresh)="refreshRooms($event)"
|
||||
(filterChange)="refreshRooms($event)"
|
||||
|
||||
@ -239,6 +239,15 @@ export class RoomsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async onRoomClick(roomId: string) {
|
||||
try {
|
||||
await this.navigationService.navigateTo(`rooms/${roomId}/detail`);
|
||||
} catch (error) {
|
||||
this.notificationService.showSnackbar('Error navigating to room detail');
|
||||
this.log.e('Error navigating to room detail:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async reopenRoom(room: MeetRoom) {
|
||||
try {
|
||||
const { room: updatedRoom } = await this.roomService.updateRoomStatus(room.roomId, MeetRoomStatus.OPEN);
|
||||
|
||||
@ -36,5 +36,11 @@ export const roomsConsoleRoutes: DomainRouteConfig[] = [
|
||||
import('../pages/room-wizard/room-wizard.component').then((m) => m.RoomWizardComponent),
|
||||
canActivate: [checkEditableRoomGuard]
|
||||
}
|
||||
},
|
||||
{
|
||||
route: {
|
||||
path: 'rooms/:roomId/detail',
|
||||
loadComponent: () => import('../pages/room-detail/room-detail.component').then((m) => m.RoomDetailComponent)
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
<div class="ov-breadcrumb">
|
||||
@for (item of items(); track $index) {
|
||||
@if (!isLastItem($index)) {
|
||||
<a class="breadcrumb-link" (click)="onItemClick(item)">{{ item.label }}</a>
|
||||
<mat-icon class="breadcrumb-separator">chevron_right</mat-icon>
|
||||
} @else {
|
||||
<span class="breadcrumb-current">{{ item.label }}</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@ -0,0 +1,30 @@
|
||||
@use '../../../../../../../src/assets/styles/design-tokens' as *;
|
||||
|
||||
.ov-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ov-meet-spacing-xs);
|
||||
color: var(--ov-meet-text-secondary);
|
||||
font-size: var(--ov-meet-font-size-sm);
|
||||
|
||||
.breadcrumb-link {
|
||||
color: var(--ov-meet-color-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color var(--ov-meet-transition-fast);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
@include ov-icon(xs);
|
||||
color: var(--ov-meet-text-hint);
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
color: var(--ov-meet-text-primary);
|
||||
font-weight: var(--ov-meet-font-weight-medium);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string;
|
||||
route?: string;
|
||||
action?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable breadcrumb navigation component.
|
||||
*
|
||||
* Features:
|
||||
* - Display hierarchical navigation path
|
||||
* - Support for route-based or action-based navigation
|
||||
* - Responsive design
|
||||
* - Consistent styling with design tokens
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <ov-breadcrumb [items]="breadcrumbItems"></ov-breadcrumb>
|
||||
* ```
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* breadcrumbItems: BreadcrumbItem[] = [
|
||||
* { label: 'Home', route: '/home' },
|
||||
* { label: 'Users', action: () => this.navigateToUsers() },
|
||||
* { label: 'John Doe' }
|
||||
* ];
|
||||
* ```
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ov-breadcrumb',
|
||||
imports: [MatIconModule],
|
||||
templateUrl: './breadcrumb.component.html',
|
||||
styleUrl: './breadcrumb.component.scss'
|
||||
})
|
||||
export class BreadcrumbComponent {
|
||||
/**
|
||||
* Array of breadcrumb items to display.
|
||||
* The last item is automatically treated as the current page (non-clickable).
|
||||
*/
|
||||
items = input.required<BreadcrumbItem[]>();
|
||||
|
||||
/**
|
||||
* Checks if an item is the last item in the breadcrumb
|
||||
*/
|
||||
isLastItem(index: number): boolean {
|
||||
return index === this.items().length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click on a breadcrumb item
|
||||
*/
|
||||
async onItemClick(item: BreadcrumbItem): Promise<void> {
|
||||
if (item.action) {
|
||||
await item.action();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './breadcrumb/breadcrumb.component';
|
||||
export * from './dialogs/confirm-dialog/confirm-dialog.component';
|
||||
export * from './pro-feature-badge/pro-feature-badge.component';
|
||||
export * from './selectable-card/selectable-card.component';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user