frontend: enhance rooms lists component with improved loading states, search functionality, and empty filter messaging
This commit is contained in:
parent
1ac05d6157
commit
cea5421012
@ -1,19 +1,46 @@
|
|||||||
<!-- Loading Spinner -->
|
@if (!loading && rooms.length === 0 && !showEmptyFilterMessage) {
|
||||||
@if (loading) {
|
<!-- Empty State -->
|
||||||
<div class="loading-container">
|
<div class="no-rooms-state">
|
||||||
<mat-spinner diameter="40"></mat-spinner>
|
<div class="empty-content">
|
||||||
<span>Loading rooms...</span>
|
<h3>No rooms created yet</h3>
|
||||||
|
<p>No rooms found. Create your first room to start hosting meetings and manage your video conferences.</p>
|
||||||
|
|
||||||
|
<div class="getting-started-actions">
|
||||||
|
<button mat-button (click)="createRoom()" class="create-room-btn primary-button">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
Create Your First Room
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else if (rooms.length > 0) {
|
} @else {
|
||||||
<!-- Rooms Toolbar -->
|
<!-- Rooms Toolbar -->
|
||||||
<mat-toolbar class="rooms-toolbar" id="rooms-toolbar">
|
<mat-toolbar class="rooms-toolbar" id="rooms-toolbar">
|
||||||
<!-- Left Section: Search -->
|
<!-- Left Section: Search -->
|
||||||
<div class="toolbar-left" id="toolbar-left">
|
<div class="toolbar-left" id="toolbar-left">
|
||||||
<mat-form-field class="search-field" appearance="outline" id="search-field">
|
@if (showSearchBox) {
|
||||||
<mat-label>Search rooms</mat-label>
|
<mat-form-field class="search-field" appearance="outline" id="search-field">
|
||||||
<input matInput [formControl]="nameFilterControl" placeholder="Search by room name" id="search-input" />
|
<mat-label>Search rooms</mat-label>
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
<input
|
||||||
</mat-form-field>
|
matInput
|
||||||
|
[formControl]="nameFilterControl"
|
||||||
|
placeholder="Search by room name"
|
||||||
|
id="search-input"
|
||||||
|
(keydown.enter)="triggerSearch()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
class="search-btn"
|
||||||
|
(click)="triggerSearch()"
|
||||||
|
[disabled]="loading || !nameFilterControl.value"
|
||||||
|
matTooltip="Search"
|
||||||
|
id="search-btn"
|
||||||
|
>
|
||||||
|
<mat-icon>search</mat-icon>
|
||||||
|
</button>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Center Section: Batch Actions (visible when items selected) -->
|
<!-- Center Section: Batch Actions (visible when items selected) -->
|
||||||
@ -36,10 +63,23 @@
|
|||||||
|
|
||||||
<!-- Right Section: Actions -->
|
<!-- Right Section: Actions -->
|
||||||
<div class="toolbar-right" id="toolbar-right">
|
<div class="toolbar-right" id="toolbar-right">
|
||||||
|
@if (hasActiveFilters()) {
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
class="clear-btn"
|
||||||
|
(click)="clearFilters()"
|
||||||
|
[disabled]="loading"
|
||||||
|
matTooltip="Clear all filters"
|
||||||
|
id="clear-filters-btn"
|
||||||
|
>
|
||||||
|
<mat-icon>filter_alt_off</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
class="refresh-btn"
|
class="refresh-btn"
|
||||||
(click)="refresh.emit()"
|
(click)="refreshRooms()"
|
||||||
[disabled]="loading"
|
[disabled]="loading"
|
||||||
matTooltip="Refresh rooms"
|
matTooltip="Refresh rooms"
|
||||||
id="refresh-btn"
|
id="refresh-btn"
|
||||||
@ -76,266 +116,282 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
|
|
||||||
<div class="table-container" id="table-container">
|
<!-- Loading Spinner -->
|
||||||
<table mat-table [dataSource]="rooms" class="rooms-table" id="rooms-table">
|
@if (loading) {
|
||||||
<!-- Selection Column -->
|
<div class="loading-container">
|
||||||
@if (showSelection) {
|
<mat-spinner diameter="40"></mat-spinner>
|
||||||
<ng-container matColumnDef="select">
|
<span>Loading rooms...</span>
|
||||||
<th mat-header-cell *matHeaderCellDef id="select-header">
|
</div>
|
||||||
<mat-checkbox
|
} @else if (rooms.length === 0 && showEmptyFilterMessage) {
|
||||||
[checked]="allSelected()"
|
<!-- No rooms match the current filters -->
|
||||||
[indeterminate]="someSelected()"
|
<div class="no-recordings-state">
|
||||||
(change)="toggleAllSelection()"
|
<div class="empty-content">
|
||||||
[disabled]="loading"
|
<h3>No rooms match your search criteria and/or filters</h3>
|
||||||
id="select-all-checkbox"
|
<p>Try adjusting or clearing your filters to see more rooms.</p>
|
||||||
>
|
<div class="getting-started-actions">
|
||||||
</mat-checkbox>
|
<button mat-button (click)="clearFilters()" class="clear-filters-btn primary-button">
|
||||||
</th>
|
<mat-icon>filter_alt_off</mat-icon>
|
||||||
<td mat-cell *matCellDef="let room" id="select-cell-{{ room.roomId }}">
|
Clear Filters
|
||||||
@if (canSelectRoom(room)) {
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<!-- Rooms Table -->
|
||||||
|
<div class="table-container" id="table-container">
|
||||||
|
<table mat-table [dataSource]="rooms" class="rooms-table" id="rooms-table">
|
||||||
|
<!-- Selection Column -->
|
||||||
|
@if (showSelection) {
|
||||||
|
<ng-container matColumnDef="select">
|
||||||
|
<th mat-header-cell *matHeaderCellDef id="select-header">
|
||||||
<mat-checkbox
|
<mat-checkbox
|
||||||
[checked]="isRoomSelected(room)"
|
[checked]="allSelected()"
|
||||||
(change)="toggleRoomSelection(room)"
|
[indeterminate]="someSelected()"
|
||||||
|
(change)="toggleAllSelection()"
|
||||||
[disabled]="loading"
|
[disabled]="loading"
|
||||||
id="select-room-{{ room.roomId }}"
|
id="select-all-checkbox"
|
||||||
>
|
>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
</th>
|
||||||
|
<td mat-cell *matCellDef="let room" id="select-cell-{{ room.roomId }}">
|
||||||
|
@if (canSelectRoom(room)) {
|
||||||
|
<mat-checkbox
|
||||||
|
[checked]="isRoomSelected(room)"
|
||||||
|
(change)="toggleRoomSelection(room)"
|
||||||
|
[disabled]="loading"
|
||||||
|
id="select-room-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
</mat-checkbox>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Room Name Column -->
|
||||||
|
<ng-container matColumnDef="roomName">
|
||||||
|
<th mat-header-cell *matHeaderCellDef class="room-header" id="room-name-header">Room Name</th>
|
||||||
|
<td mat-cell *matCellDef="let room" class="room-cell" id="room-name-cell-{{ room.roomId }}">
|
||||||
|
<div class="room-info" id="room-info-{{ room.roomId }}">
|
||||||
|
<span class="room-name" id="room-name-{{ room.roomId }}">{{ room.roomName }}</span>
|
||||||
|
<span class="room-id" id="room-id-{{ room.roomId }}">{{ room.roomId }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Status Column -->
|
||||||
|
<ng-container matColumnDef="status">
|
||||||
|
<th mat-header-cell *matHeaderCellDef id="status-header">Status</th>
|
||||||
|
<td mat-cell *matCellDef="let room" id="status-cell-{{ room.roomId }}">
|
||||||
|
<div
|
||||||
|
class="status-badge"
|
||||||
|
[style.color]="getStatusColor(room)"
|
||||||
|
[matTooltip]="getStatusTooltip(room)"
|
||||||
|
id="status-badge-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
class="status-icon"
|
||||||
|
[style.color]="getStatusColor(room)"
|
||||||
|
id="status-icon-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
{{ getStatusIcon(room) }}
|
||||||
|
</mat-icon>
|
||||||
|
<span class="status-label" id="status-label-{{ room.roomId }}">{{
|
||||||
|
getRoomStatus(room)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Creation Date Column -->
|
||||||
|
<ng-container matColumnDef="creationDate">
|
||||||
|
<th mat-header-cell *matHeaderCellDef id="creation-date-header">Created</th>
|
||||||
|
<td mat-cell *matCellDef="let room" id="creation-date-cell-{{ room.roomId }}">
|
||||||
|
@if (room.creationDate) {
|
||||||
|
<div class="date-info" id="date-info-{{ room.roomId }}">
|
||||||
|
<span class="date" id="creation-date-{{ room.roomId }}">{{
|
||||||
|
room.creationDate | date: 'mediumDate'
|
||||||
|
}}</span>
|
||||||
|
<span class="time" id="creation-time-{{ room.roomId }}">{{
|
||||||
|
room.creationDate | date: 'shortTime'
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<span class="no-data" id="no-creation-date-{{ room.roomId }}">-</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Room Name Column -->
|
<!-- Auto Deletion Column -->
|
||||||
<ng-container matColumnDef="roomName">
|
<ng-container matColumnDef="autoDeletion">
|
||||||
<th mat-header-cell *matHeaderCellDef class="room-header" id="room-name-header">Room Name</th>
|
<th mat-header-cell *matHeaderCellDef id="auto-deletion-header">Auto Deletion</th>
|
||||||
<td mat-cell *matCellDef="let room" class="room-cell" id="room-name-cell-{{ room.roomId }}">
|
<td mat-cell *matCellDef="let room" id="auto-deletion-cell-{{ room.roomId }}">
|
||||||
<div class="room-info" id="room-info-{{ room.roomId }}">
|
<div class="auto-deletion-content" id="auto-deletion-content-{{ room.roomId }}">
|
||||||
<span class="room-name" id="room-name-{{ room.roomId }}">{{ room.roomName }}</span>
|
<div class="auto-deletion-info" id="auto-deletion-info-{{ room.roomId }}">
|
||||||
<span class="room-id" id="room-id-{{ room.roomId }}">{{ room.roomId }}</span>
|
@if (hasAutoDeletion(room)) {
|
||||||
</div>
|
<div
|
||||||
</td>
|
class="deletion-badge"
|
||||||
</ng-container>
|
[ngClass]="getAutoDeletionClass(room)"
|
||||||
|
[matTooltip]="getAutoDeletionTooltip(room)"
|
||||||
<!-- Status Column -->
|
id="deletion-badge-{{ room.roomId }}"
|
||||||
<ng-container matColumnDef="status">
|
|
||||||
<th mat-header-cell *matHeaderCellDef id="status-header">Status</th>
|
|
||||||
<td mat-cell *matCellDef="let room" id="status-cell-{{ room.roomId }}">
|
|
||||||
<div
|
|
||||||
class="status-badge"
|
|
||||||
[style.color]="getStatusColor(room)"
|
|
||||||
[matTooltip]="getStatusTooltip(room)"
|
|
||||||
id="status-badge-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon
|
|
||||||
class="status-icon"
|
|
||||||
[style.color]="getStatusColor(room)"
|
|
||||||
id="status-icon-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
{{ getStatusIcon(room) }}
|
|
||||||
</mat-icon>
|
|
||||||
<span class="status-label" id="status-label-{{ room.roomId }}">{{ getRoomStatus(room) }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Creation Date Column -->
|
|
||||||
<ng-container matColumnDef="creationDate">
|
|
||||||
<th mat-header-cell *matHeaderCellDef id="creation-date-header">Created</th>
|
|
||||||
<td mat-cell *matCellDef="let room" id="creation-date-cell-{{ room.roomId }}">
|
|
||||||
@if (room.creationDate) {
|
|
||||||
<div class="date-info" id="date-info-{{ room.roomId }}">
|
|
||||||
<span class="date" id="creation-date-{{ room.roomId }}">{{
|
|
||||||
room.creationDate | date: 'mediumDate'
|
|
||||||
}}</span>
|
|
||||||
<span class="time" id="creation-time-{{ room.roomId }}">{{
|
|
||||||
room.creationDate | date: 'shortTime'
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<span class="no-data" id="no-creation-date-{{ room.roomId }}">-</span>
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Auto Deletion Column -->
|
|
||||||
<ng-container matColumnDef="autoDeletion">
|
|
||||||
<th mat-header-cell *matHeaderCellDef id="auto-deletion-header">Auto Deletion</th>
|
|
||||||
<td mat-cell *matCellDef="let room" id="auto-deletion-cell-{{ room.roomId }}">
|
|
||||||
<div class="auto-deletion-content" id="auto-deletion-content-{{ room.roomId }}">
|
|
||||||
<div class="auto-deletion-info" id="auto-deletion-info-{{ room.roomId }}">
|
|
||||||
@if (hasAutoDeletion(room)) {
|
|
||||||
<div
|
|
||||||
class="deletion-badge"
|
|
||||||
[ngClass]="getAutoDeletionClass(room)"
|
|
||||||
[matTooltip]="getAutoDeletionTooltip(room)"
|
|
||||||
id="deletion-badge-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon
|
|
||||||
class="deletion-icon material-symbols-outlined"
|
|
||||||
id="deletion-icon-{{ room.roomId }}"
|
|
||||||
>{{ getAutoDeletionIcon(room) }}</mat-icon
|
|
||||||
>
|
>
|
||||||
<span class="deletion-text" id="deletion-text-{{ room.roomId }}">{{
|
<mat-icon
|
||||||
getAutoDeletionStatus(room)
|
class="deletion-icon material-symbols-outlined"
|
||||||
}}</span>
|
id="deletion-icon-{{ room.roomId }}"
|
||||||
</div>
|
>{{ getAutoDeletionIcon(room) }}</mat-icon
|
||||||
|
>
|
||||||
|
<span class="deletion-text" id="deletion-text-{{ room.roomId }}">{{
|
||||||
|
getAutoDeletionStatus(room)
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (!room.markedForDeletion) {
|
@if (!room.markedForDeletion) {
|
||||||
<div class="deletion-date-time" id="deletion-date-time-{{ room.roomId }}">
|
<div class="deletion-date-time" id="deletion-date-time-{{ room.roomId }}">
|
||||||
<div class="deletion-date" id="deletion-date-container-{{ room.roomId }}">
|
<div class="deletion-date" id="deletion-date-container-{{ room.roomId }}">
|
||||||
<span class="date" id="deletion-date-{{ room.roomId }}">{{
|
<span class="date" id="deletion-date-{{ room.roomId }}">{{
|
||||||
room.autoDeletionDate | date: 'mediumDate'
|
room.autoDeletionDate | date: 'mediumDate'
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="deletion-time" id="deletion-time-container-{{ room.roomId }}">
|
<div class="deletion-time" id="deletion-time-container-{{ room.roomId }}">
|
||||||
<span class="time" id="deletion-time-{{ room.roomId }}">{{
|
<span class="time" id="deletion-time-{{ room.roomId }}">{{
|
||||||
room.autoDeletionDate | date: 'shortTime'
|
room.autoDeletionDate | date: 'shortTime'
|
||||||
}}</span>
|
}}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<div
|
||||||
|
class="deletion-badge"
|
||||||
|
[ngClass]="getAutoDeletionClass(room)"
|
||||||
|
[matTooltip]="getAutoDeletionTooltip(room)"
|
||||||
|
id="deletion-badge-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
class="deletion-icon material-symbols-outlined"
|
||||||
|
id="deletion-icon-{{ room.roomId }}"
|
||||||
|
>{{ getAutoDeletionIcon(room) }}</mat-icon
|
||||||
|
>
|
||||||
|
<span class="deletion-text" id="deletion-text-{{ room.roomId }}">{{
|
||||||
|
getAutoDeletionStatus(room)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} @else {
|
</div>
|
||||||
<div
|
|
||||||
class="deletion-badge"
|
|
||||||
[ngClass]="getAutoDeletionClass(room)"
|
|
||||||
[matTooltip]="getAutoDeletionTooltip(room)"
|
|
||||||
id="deletion-badge-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon
|
|
||||||
class="deletion-icon material-symbols-outlined"
|
|
||||||
id="deletion-icon-{{ room.roomId }}"
|
|
||||||
>{{ getAutoDeletionIcon(room) }}</mat-icon
|
|
||||||
>
|
|
||||||
<span class="deletion-text" id="deletion-text-{{ room.roomId }}">{{
|
|
||||||
getAutoDeletionStatus(room)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</ng-container>
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Actions Column -->
|
<!-- Actions Column -->
|
||||||
<ng-container matColumnDef="actions">
|
<ng-container matColumnDef="actions">
|
||||||
<th mat-header-cell *matHeaderCellDef class="actions-header" id="actions-header">Actions</th>
|
<th mat-header-cell *matHeaderCellDef class="actions-header" id="actions-header">Actions</th>
|
||||||
<td mat-cell *matCellDef="let room" class="actions-cell" id="actions-cell-{{ room.roomId }}">
|
<td mat-cell *matCellDef="let room" class="actions-cell" id="actions-cell-{{ room.roomId }}">
|
||||||
<div class="action-buttons" id="action-buttons-{{ room.roomId }}">
|
<div class="action-buttons" id="action-buttons-{{ room.roomId }}">
|
||||||
<!-- Open Room Button -->
|
<!-- Open Room Button -->
|
||||||
@if (canOpenRoom(room)) {
|
@if (canOpenRoom(room)) {
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
matTooltip="Open Room"
|
|
||||||
(click)="openRoom(room)"
|
|
||||||
[disabled]="loading"
|
|
||||||
class="primary-action"
|
|
||||||
id="open-room-btn-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon>open_in_new</mat-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Settings Button -->
|
|
||||||
@if (canEditRoom(room)) {
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
matTooltip="Room Settings"
|
|
||||||
(click)="editRoom(room)"
|
|
||||||
[disabled]="loading"
|
|
||||||
id="edit-room-btn-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon class="ov-settings-icon">settings</mat-icon>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- More Actions Menu -->
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
[matMenuTriggerFor]="actionsMenu"
|
|
||||||
matTooltip="More Actions"
|
|
||||||
[disabled]="loading"
|
|
||||||
id="more-actions-btn-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon>more_vert</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<mat-menu #actionsMenu="matMenu" id="actions-menu-{{ room.roomId }}">
|
|
||||||
<button
|
|
||||||
mat-menu-item
|
|
||||||
(click)="viewRecordings(room)"
|
|
||||||
id="view-recordings-btn-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon class="ov-recording-icon">video_library</mat-icon>
|
|
||||||
<span>View Recordings</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (!room.markedForDeletion) {
|
|
||||||
<button
|
<button
|
||||||
mat-menu-item
|
mat-icon-button
|
||||||
(click)="copyModeratorLink(room)"
|
matTooltip="Open Room"
|
||||||
id="copy-moderator-link-btn-{{ room.roomId }}"
|
(click)="openRoom(room)"
|
||||||
|
[disabled]="loading"
|
||||||
|
class="primary-action"
|
||||||
|
id="open-room-btn-{{ room.roomId }}"
|
||||||
>
|
>
|
||||||
<mat-icon>content_copy</mat-icon>
|
<mat-icon>open_in_new</mat-icon>
|
||||||
<span>Copy Moderator Link</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
mat-menu-item
|
|
||||||
(click)="copySpeakerLink(room)"
|
|
||||||
id="copy-speaker-link-btn-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon>content_copy</mat-icon>
|
|
||||||
<span>Copy Speaker Link</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<mat-divider id="actions-divider-{{ room.roomId }}"></mat-divider>
|
|
||||||
<button
|
|
||||||
mat-menu-item
|
|
||||||
(click)="deleteRoom(room)"
|
|
||||||
class="delete-action"
|
|
||||||
id="delete-room-btn-{{ room.roomId }}"
|
|
||||||
>
|
|
||||||
<mat-icon>delete</mat-icon>
|
|
||||||
<span>Delete room</span>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</mat-menu>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<tr mat-header-row *matHeaderRowDef="displayedColumns" id="table-header-row"></tr>
|
<!-- Settings Button -->
|
||||||
<tr
|
@if (canEditRoom(room)) {
|
||||||
mat-row
|
<button
|
||||||
*matRowDef="let row; columns: displayedColumns"
|
mat-icon-button
|
||||||
[class.selected-row]="isRoomSelected(row)"
|
matTooltip="Room Settings"
|
||||||
[class.marked-for-deletion]="row.markedForDeletion"
|
(click)="editRoom(room)"
|
||||||
id="table-row-{{ row.roomId }}"
|
[disabled]="loading"
|
||||||
></tr>
|
id="edit-room-btn-{{ room.roomId }}"
|
||||||
</table>
|
>
|
||||||
</div>
|
<mat-icon class="ov-settings-icon">settings</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Load More Section -->
|
<!-- More Actions Menu -->
|
||||||
@if (showLoadMore) {
|
<button
|
||||||
<div class="load-more-container" id="load-more-container">
|
mat-icon-button
|
||||||
<button mat-button class="load-more-btn" (click)="loadMore.emit()" [disabled]="loading" id="load-more-btn">
|
[matMenuTriggerFor]="actionsMenu"
|
||||||
<mat-icon>expand_more</mat-icon>
|
matTooltip="More Actions"
|
||||||
<span>Load More Rooms</span>
|
[disabled]="loading"
|
||||||
</button>
|
id="more-actions-btn-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-menu #actionsMenu="matMenu" id="actions-menu-{{ room.roomId }}">
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="viewRecordings(room)"
|
||||||
|
id="view-recordings-btn-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
<mat-icon class="ov-recording-icon">video_library</mat-icon>
|
||||||
|
<span>View Recordings</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (!room.markedForDeletion) {
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="copyModeratorLink(room)"
|
||||||
|
id="copy-moderator-link-btn-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
<mat-icon>content_copy</mat-icon>
|
||||||
|
<span>Copy Moderator Link</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="copySpeakerLink(room)"
|
||||||
|
id="copy-speaker-link-btn-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
<mat-icon>content_copy</mat-icon>
|
||||||
|
<span>Copy Speaker Link</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-divider id="actions-divider-{{ room.roomId }}"></mat-divider>
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="deleteRoom(room)"
|
||||||
|
class="delete-action"
|
||||||
|
id="delete-room-btn-{{ room.roomId }}"
|
||||||
|
>
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
<span>Delete room</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</mat-menu>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<tr mat-header-row *matHeaderRowDef="displayedColumns" id="table-header-row"></tr>
|
||||||
|
<tr
|
||||||
|
mat-row
|
||||||
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
|
[class.selected-row]="isRoomSelected(row)"
|
||||||
|
[class.marked-for-deletion]="row.markedForDeletion"
|
||||||
|
id="table-row-{{ row.roomId }}"
|
||||||
|
></tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
<!-- Empty State -->
|
|
||||||
<div class="no-rooms-state">
|
|
||||||
<div class="empty-content">
|
|
||||||
<h3>No rooms created yet</h3>
|
|
||||||
<p>No rooms found. Create your first room to start hosting meetings and manage your video conferences.</p>
|
|
||||||
|
|
||||||
<div class="getting-started-actions">
|
<!-- Load More Section -->
|
||||||
<button mat-button (click)="createRoom()" class="create-room-btn primary-button">
|
@if (showLoadMore) {
|
||||||
<mat-icon>add</mat-icon>
|
<div class="load-more-container" id="load-more-container">
|
||||||
Create Your First Room
|
<button
|
||||||
|
mat-button
|
||||||
|
class="load-more-btn"
|
||||||
|
(click)="loadMoreRooms()"
|
||||||
|
[disabled]="loading"
|
||||||
|
id="load-more-btn"
|
||||||
|
>
|
||||||
|
<mat-icon>expand_more</mat-icon>
|
||||||
|
<span>Load More Rooms</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,22 @@
|
|||||||
.rooms-toolbar {
|
.rooms-toolbar {
|
||||||
@extend .ov-data-toolbar;
|
@extend .ov-data-toolbar;
|
||||||
|
|
||||||
|
.toolbar-left {
|
||||||
|
::ng-deep .search-btn {
|
||||||
|
padding: var(--ov-meet-spacing-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-right {
|
.toolbar-right {
|
||||||
gap: var(--ov-meet-spacing-sm);
|
gap: var(--ov-meet-spacing-sm);
|
||||||
|
|
||||||
::ng-deep .refresh-btn {
|
::ng-deep .refresh-btn {
|
||||||
padding: var(--ov-meet-spacing-sm);
|
padding: var(--ov-meet-spacing-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .clear-btn {
|
||||||
|
padding: var(--ov-meet-spacing-sm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,10 +261,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.getting-started-actions {
|
.getting-started-actions {
|
||||||
display: flex;
|
@extend .action-buttons;
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--ov-meet-spacing-md);
|
.create-room-btn {
|
||||||
align-items: center;
|
@extend .refresh-btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-filters-btn {
|
||||||
|
@extend .refresh-btn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -92,10 +92,12 @@ export interface RoomTableAction {
|
|||||||
export class RoomsListsComponent implements OnInit, OnChanges {
|
export class RoomsListsComponent implements OnInit, OnChanges {
|
||||||
// Input properties
|
// Input properties
|
||||||
@Input() rooms: MeetRoom[] = [];
|
@Input() rooms: MeetRoom[] = [];
|
||||||
|
@Input() showSearchBox = true;
|
||||||
@Input() showFilters = false;
|
@Input() showFilters = false;
|
||||||
@Input() showSelection = true;
|
@Input() showSelection = true;
|
||||||
@Input() showLoadMore = false;
|
@Input() showLoadMore = false;
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
|
@Input() initialFilters: { nameFilter: string; statusFilter: string } = { nameFilter: '', statusFilter: '' };
|
||||||
|
|
||||||
// Host binding for styling when rooms are selected
|
// Host binding for styling when rooms are selected
|
||||||
@HostBinding('class.has-selections')
|
@HostBinding('class.has-selections')
|
||||||
@ -106,13 +108,15 @@ export class RoomsListsComponent implements OnInit, OnChanges {
|
|||||||
// Output events
|
// Output events
|
||||||
@Output() roomAction = new EventEmitter<RoomTableAction>();
|
@Output() roomAction = new EventEmitter<RoomTableAction>();
|
||||||
@Output() filterChange = new EventEmitter<{ nameFilter: string; statusFilter: string }>();
|
@Output() filterChange = new EventEmitter<{ nameFilter: string; statusFilter: string }>();
|
||||||
@Output() loadMore = new EventEmitter<void>();
|
@Output() loadMore = new EventEmitter<{ nameFilter: string; statusFilter: string }>();
|
||||||
@Output() refresh = new EventEmitter<void>();
|
@Output() refresh = new EventEmitter<{ nameFilter: string; statusFilter: string }>();
|
||||||
|
|
||||||
// Filter controls
|
// Filter controls
|
||||||
nameFilterControl = new FormControl('');
|
nameFilterControl = new FormControl('');
|
||||||
statusFilterControl = new FormControl('');
|
statusFilterControl = new FormControl('');
|
||||||
|
|
||||||
|
showEmptyFilterMessage = false; // Show message when no rooms match filters
|
||||||
|
|
||||||
// Selection state
|
// Selection state
|
||||||
selectedRooms = signal<Set<string>>(new Set());
|
selectedRooms = signal<Set<string>>(new Set());
|
||||||
allSelected = signal(false);
|
allSelected = signal(false);
|
||||||
@ -137,30 +141,39 @@ export class RoomsListsComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
if (changes['rooms']) {
|
if (changes['rooms']) {
|
||||||
|
// Update selected rooms based on current rooms
|
||||||
const validIds = new Set(this.rooms.map((r) => r.roomId));
|
const validIds = new Set(this.rooms.map((r) => r.roomId));
|
||||||
const filteredSelection = new Set([...this.selectedRooms()].filter((id) => validIds.has(id)));
|
const filteredSelection = new Set([...this.selectedRooms()].filter((id) => validIds.has(id)));
|
||||||
this.selectedRooms.set(filteredSelection);
|
this.selectedRooms.set(filteredSelection);
|
||||||
this.updateSelectionState();
|
this.updateSelectionState();
|
||||||
|
|
||||||
|
// Show message when no rooms match filters
|
||||||
|
if (this.rooms.length === 0 && this.hasActiveFilters()) {
|
||||||
|
this.showEmptyFilterMessage = true;
|
||||||
|
} else {
|
||||||
|
this.showEmptyFilterMessage = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== INITIALIZATION METHODS =====
|
// ===== INITIALIZATION METHODS =====
|
||||||
|
|
||||||
private setupFilters() {
|
private setupFilters() {
|
||||||
// Set up name filter with debounce
|
// Set up initial filter values
|
||||||
this.nameFilterControl.valueChanges.pipe(debounceTime(300), distinctUntilChanged()).subscribe((value) => {
|
this.nameFilterControl.setValue(this.initialFilters.nameFilter);
|
||||||
this.filterChange.emit({
|
this.statusFilterControl.setValue(this.initialFilters.statusFilter);
|
||||||
nameFilter: value || '',
|
|
||||||
statusFilter: this.statusFilterControl.value || ''
|
// Set up name filter change detection
|
||||||
});
|
this.nameFilterControl.valueChanges.subscribe((value) => {
|
||||||
|
// Emit filter change if value is empty
|
||||||
|
if (!value) {
|
||||||
|
this.emitFilterChange();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up status filter
|
// Set up status filter change detection
|
||||||
this.statusFilterControl.valueChanges.subscribe((value) => {
|
this.statusFilterControl.valueChanges.subscribe(() => {
|
||||||
this.filterChange.emit({
|
this.emitFilterChange();
|
||||||
nameFilter: this.nameFilterControl.value || '',
|
|
||||||
statusFilter: value || ''
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,8 +274,31 @@ export class RoomsListsComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadMoreRooms() {
|
||||||
|
const nameFilter = this.nameFilterControl.value || '';
|
||||||
|
const statusFilter = this.statusFilterControl.value || '';
|
||||||
|
this.loadMore.emit({ nameFilter, statusFilter });
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshRooms() {
|
||||||
|
const nameFilter = this.nameFilterControl.value || '';
|
||||||
|
const statusFilter = this.statusFilterControl.value || '';
|
||||||
|
this.refresh.emit({ nameFilter, statusFilter });
|
||||||
|
}
|
||||||
|
|
||||||
// ===== FILTER METHODS =====
|
// ===== FILTER METHODS =====
|
||||||
|
|
||||||
|
triggerSearch() {
|
||||||
|
this.emitFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitFilterChange() {
|
||||||
|
this.filterChange.emit({
|
||||||
|
nameFilter: this.nameFilterControl.value || '',
|
||||||
|
statusFilter: this.statusFilterControl.value || ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hasActiveFilters(): boolean {
|
hasActiveFilters(): boolean {
|
||||||
return !!(this.nameFilterControl.value || this.statusFilterControl.value);
|
return !!(this.nameFilterControl.value || this.statusFilterControl.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,28 @@
|
|||||||
@if (isLoading) {
|
<!-- Loading State -->
|
||||||
@if (showLoadingSpinner) {
|
@if (isInitializing && showInitialLoader) {
|
||||||
<!-- Enhanced Loading State with delay -->
|
<div class="ov-page-loading">
|
||||||
<div class="ov-page-loading">
|
<div class="loading-content">
|
||||||
<div class="loading-content">
|
<div class="loading-header">
|
||||||
<div class="loading-header">
|
<div class="loading-title">
|
||||||
<div class="loading-title">
|
<mat-icon class="ov-room-icon loading-icon">video_chat</mat-icon>
|
||||||
<mat-icon class="ov-room-icon loading-icon"> video_chat </mat-icon>
|
<h1>Loading Rooms</h1>
|
||||||
<h1>Loading Rooms</h1>
|
|
||||||
</div>
|
|
||||||
<p class="loading-subtitle">Please wait while we fetch your rooms...</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="loading-subtitle">Please wait while we fetch your rooms...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="loading-spinner-container">
|
<div class="loading-spinner-container">
|
||||||
<mat-spinner diameter="48"></mat-spinner>
|
<mat-spinner diameter="48"></mat-spinner>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
} @else {
|
}
|
||||||
|
|
||||||
|
@if (!isInitializing) {
|
||||||
<div class="ov-page-container ov-mb-xxl" id="rooms-page-container">
|
<div class="ov-page-container ov-mb-xxl" id="rooms-page-container">
|
||||||
|
<!-- Rooms Header -->
|
||||||
<div class="page-header" id="rooms-page-header">
|
<div class="page-header" id="rooms-page-header">
|
||||||
<div class="title" id="rooms-title">
|
<div class="title" id="rooms-title">
|
||||||
<mat-icon class="ov-room-icon" id="rooms-icon"> video_chat </mat-icon>
|
<mat-icon class="ov-room-icon" id="rooms-icon">video_chat</mat-icon>
|
||||||
<h1 id="rooms-heading">Rooms</h1>
|
<h1 id="rooms-heading">Rooms</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="subtitle" id="rooms-subtitle">
|
<p class="subtitle" id="rooms-subtitle">
|
||||||
@ -29,16 +30,21 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
<div class="page-content" id="rooms-content">
|
<div class="page-content" id="rooms-content">
|
||||||
<ov-rooms-lists
|
<ov-rooms-lists
|
||||||
id="rooms-list"
|
id="rooms-list"
|
||||||
[rooms]="rooms()"
|
[rooms]="rooms()"
|
||||||
[loading]="isLoading"
|
[loading]="isLoading"
|
||||||
|
[showSearchBox]="true"
|
||||||
|
[showFilters]="false"
|
||||||
[showSelection]="true"
|
[showSelection]="true"
|
||||||
[showLoadMore]="hasMoreRooms"
|
[showLoadMore]="hasMoreRooms"
|
||||||
|
[initialFilters]="initialFilters"
|
||||||
(roomAction)="onRoomAction($event)"
|
(roomAction)="onRoomAction($event)"
|
||||||
(loadMore)="loadMoreRooms()"
|
(loadMore)="loadMoreRooms($event)"
|
||||||
(refresh)="refreshRooms()"
|
(refresh)="refreshRooms($event)"
|
||||||
|
(filterChange)="refreshRooms($event)"
|
||||||
></ov-rooms-lists>
|
></ov-rooms-lists>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,8 +49,16 @@ export class RoomsComponent implements OnInit {
|
|||||||
// searchTerm = '';
|
// searchTerm = '';
|
||||||
|
|
||||||
rooms = signal<MeetRoom[]>([]);
|
rooms = signal<MeetRoom[]>([]);
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
isInitializing = true;
|
||||||
|
showInitialLoader = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
showLoadingSpinner = false;
|
|
||||||
|
initialFilters = {
|
||||||
|
nameFilter: '',
|
||||||
|
statusFilter: ''
|
||||||
|
};
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
hasMoreRooms = false;
|
hasMoreRooms = false;
|
||||||
@ -69,7 +77,15 @@ export class RoomsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const delayLoader = setTimeout(() => {
|
||||||
|
this.showInitialLoader = true;
|
||||||
|
}, 200);
|
||||||
|
|
||||||
await this.loadRooms();
|
await this.loadRooms();
|
||||||
|
|
||||||
|
clearTimeout(delayLoader);
|
||||||
|
this.showInitialLoader = false;
|
||||||
|
this.isInitializing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onRoomAction(action: RoomTableAction) {
|
async onRoomAction(action: RoomTableAction) {
|
||||||
@ -101,24 +117,34 @@ export class RoomsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadRooms() {
|
private async loadRooms(filters?: { nameFilter: string; statusFilter: string }, refresh = false) {
|
||||||
this.isLoading = true;
|
const delayLoader = setTimeout(() => {
|
||||||
const delaySpinner = setTimeout(() => {
|
this.isLoading = true;
|
||||||
this.showLoadingSpinner = true;
|
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const roomFilters: MeetRoomFilters = {
|
const roomFilters: MeetRoomFilters = {
|
||||||
maxItems: 50,
|
maxItems: 50,
|
||||||
nextPageToken: this.nextPageToken
|
nextPageToken: !refresh ? this.nextPageToken : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Apply room ID filter if provided
|
||||||
|
// if (filters?.nameFilter) {
|
||||||
|
// roomFilters.roomName = filters.nameFilter;
|
||||||
|
// }
|
||||||
|
|
||||||
const response = await this.roomService.listRooms(roomFilters);
|
const response = await this.roomService.listRooms(roomFilters);
|
||||||
|
|
||||||
// TODO: Filter rooms
|
// TODO: Filter rooms by status
|
||||||
|
|
||||||
// Update rooms list
|
if (!refresh) {
|
||||||
const currentRooms = this.rooms();
|
// Update rooms list
|
||||||
this.rooms.set([...currentRooms, ...response.rooms]);
|
const currentRooms = this.rooms();
|
||||||
|
this.rooms.set([...currentRooms, ...response.rooms]);
|
||||||
|
} else {
|
||||||
|
// Replace rooms list
|
||||||
|
this.rooms.set(response.rooms);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Sort rooms
|
// TODO: Sort rooms
|
||||||
// this.dataSource.data = this.rooms();
|
// this.dataSource.data = this.rooms();
|
||||||
@ -131,9 +157,8 @@ export class RoomsComponent implements OnInit {
|
|||||||
this.notificationService.showAlert('Error loading rooms');
|
this.notificationService.showAlert('Error loading rooms');
|
||||||
this.log.e('Error loading rooms:', error);
|
this.log.e('Error loading rooms:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
clearTimeout(delayLoader);
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
clearTimeout(delaySpinner);
|
|
||||||
this.showLoadingSpinner = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,16 +213,13 @@ export class RoomsComponent implements OnInit {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async loadMoreRooms() {
|
async loadMoreRooms(filters?: { nameFilter: string; statusFilter: string }) {
|
||||||
if (!this.hasMoreRooms || this.isLoading) return;
|
if (!this.hasMoreRooms || this.isLoading) return;
|
||||||
await this.loadRooms();
|
await this.loadRooms(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshRooms() {
|
async refreshRooms(filters?: { nameFilter: string; statusFilter: string }) {
|
||||||
this.rooms.set([]);
|
await this.loadRooms(filters, true);
|
||||||
this.nextPageToken = undefined;
|
|
||||||
this.hasMoreRooms = false;
|
|
||||||
await this.loadRooms();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createRoom() {
|
private async createRoom() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user