frontend: enhance rooms lists component with improved loading states, search functionality, and empty filter messaging

This commit is contained in:
juancarmore 2025-08-15 23:07:11 +02:00
parent 1ac05d6157
commit cea5421012
5 changed files with 440 additions and 305 deletions

View File

@ -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> }
} }

View File

@ -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;
}
} }
} }

View File

@ -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);
} }

View File

@ -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>

View File

@ -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() {