Skip to content

Commit 154cb09

Browse files
authored
refactor(ui): implement global settings and fix reactivity (#159)
* refactor(ui): implement global settings and fix reactivity Implemented global settings at /settings route and fixed signal reactivity in meetings/settings dashboards to properly respond to project context changes. Key changes: - Created new global settings module with reactive pattern - Fixed meetings dashboard to react to project context changes - Moved settings from project-specific to global route - Applied toObservable + merge pattern for proper reactivity - Removed old project settings components - Updated navigation paths and UI improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> LFXV2-756 Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(ui): prevent race condition in committees dashboard loading state Fixed issue where "no groups found" message would briefly appear alongside the groups table during refresh due to race condition between committeesLoading signal and committees data updates. Changed template from: @if (committeesLoading()) { ... } @if (committees() && !committeesLoading()) { ... } To: @if (committeesLoading()) { ... } @else { ... } This ensures only one state is displayed at a time, preventing the visual glitch where both loading and content states could appear simultaneously. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> LFXV2-756 Signed-off-by: Asitha de Silva <asithade@gmail.com> * fix(ui): complete navigation path updates to global routes Completed the navigation refactoring by updating remaining project-specific paths to use global routes in error handlers and success callbacks: - Committee view error handler now navigates to /groups - Meeting manage success handlers now navigate to /meetings - Removed unused project parameter from handleMeetingSuccess - Simplified handleAttachmentOperationsResults signature This ensures consistent navigation behavior across the application after moving settings, meetings, and committees to global routes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> LFXV2-756 Signed-off-by: Asitha de Silva <asithade@gmail.com> --------- Signed-off-by: Asitha de Silva <asithade@gmail.com>
1 parent d1421dd commit 154cb09

File tree

26 files changed

+189
-195
lines changed

26 files changed

+189
-195
lines changed

apps/lfx-one/src/app/app.routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export const routes: Routes = [
2727
path: 'groups',
2828
loadChildren: () => import('./modules/committees/committees.routes').then((m) => m.COMMITTEE_ROUTES),
2929
},
30+
{
31+
path: 'settings',
32+
loadChildren: () => import('./modules/settings/settings.routes').then((m) => m.SETTINGS_ROUTES),
33+
},
3034
],
3135
},
3236
{

apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ <h2 class="text-lg font-semibold text-gray-900">Menu</h2>
3030
}
3131

3232
<!-- Main Content Area -->
33-
<main class="flex-1 min-w-0 transition-all duration-300 lg:ml-64 px-[21px] md:px-[32px] py-[24px]" data-testid="main-content">
34-
<router-outlet />
33+
<main class="flex-1 min-w-0 transition-all duration-300 lg:ml-64 flex flex-col" data-testid="main-content">
34+
<div class="flex-grow px-5 md:px-8 py-6">
35+
<router-outlet />
36+
</div>
3537
<!-- Footer Component -->
36-
<lfx-footer class="py-8"></lfx-footer>
38+
<lfx-footer class="py-8 px-5 md:px-8 mt-auto"></lfx-footer>
3739
</main>
3840
</div>

apps/lfx-one/src/app/layouts/main-layout/main-layout.component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SidebarComponent } from '@components/sidebar/sidebar.component';
1111
import { COMMITTEE_LABEL } from '@lfx-one/shared/constants';
1212
import { SidebarMenuItem } from '@lfx-one/shared/interfaces';
1313
import { PersonaService } from '@services/persona.service';
14+
import { ProjectContextService } from '@services/project-context.service';
1415
import { filter } from 'rxjs';
1516

1617
@Component({
@@ -26,12 +27,14 @@ export class MainLayoutComponent {
2627
private readonly appService = inject(AppService);
2728
private readonly featureFlagService = inject(FeatureFlagService);
2829
private readonly personaService = inject(PersonaService);
30+
private readonly projectContextService = inject(ProjectContextService);
2931

3032
// Expose mobile sidebar state from service
33+
protected readonly selectedProject = this.projectContextService.selectedProject;
3134
protected readonly showMobileSidebar = this.appService.showMobileSidebar;
3235

3336
// Feature flags
34-
private readonly showProjectsInSidebar = this.featureFlagService.getBooleanFlag('sidebar-projects', true);
37+
private readonly showProjectsInSidebar = this.featureFlagService.getBooleanFlag('sidebar-projects', false);
3538

3639
// Base sidebar navigation items - matching React NavigationSidebar design
3740
private readonly baseSidebarItems: SidebarMenuItem[] = [
@@ -79,7 +82,6 @@ export class MainLayoutComponent {
7982
label: 'Settings',
8083
icon: 'fa-light fa-gear',
8184
routerLink: '/settings',
82-
disabled: true,
8385
},
8486
{
8587
label: 'Profile',

apps/lfx-one/src/app/layouts/project-layout/project-layout.component.html

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,93 +4,6 @@
44
<!-- Header Component -->
55
<lfx-header></lfx-header>
66

7-
<div class="bg-white border-b border-gray-100 py-6 shadow-sm min-h-60">
8-
<div class="container mx-auto px-8">
9-
<!-- Breadcrumb Navigation -->
10-
<div class="mb-4">
11-
<lfx-breadcrumb [model]="breadcrumbItems()">
12-
<ng-template #item let-item>
13-
<a [routerLink]="item.routerLink" class="flex items-center gap-2 text-sm font-medium text-gray-900 hover:text-gray-600 transition-colors">
14-
@if (item.icon) {
15-
<i [class]="item.icon" class="text-gray-500"></i>
16-
}
17-
{{ item.label }}
18-
</a>
19-
</ng-template>
20-
</lfx-breadcrumb>
21-
</div>
22-
23-
<div class="flex flex-col md:flex-row md:items-center justify-between w-full">
24-
<div class="flex justify-between items-start gap-3 w-full">
25-
<div class="flex flex-col gap-1 w-full">
26-
<!-- Project Title -->
27-
<h1 class="text-2xl font-display font-semibold text-gray-900 mb-3">
28-
{{ projectTitle() }}
29-
</h1>
30-
31-
<!-- Project Description -->
32-
@if (projectDescription(); as description) {
33-
<p class="text-gray-600 text-sm mb-6 max-w-3xl leading-relaxed">
34-
{{ description }}
35-
</p>
36-
}
37-
</div>
38-
39-
<!-- Category Badge -->
40-
@if (categoryLabel(); as category) {
41-
<div class="flex justify-start">
42-
<img [src]="projectLogo()" class="h-16 w-full" />
43-
</div>
44-
}
45-
</div>
46-
</div>
47-
48-
<!-- Menu Items Section -->
49-
<div class="md:flex items-center justify-start md:justify-between">
50-
<div class="flex items-center gap-3 flex-wrap">
51-
@for (menu of menuItems(); track menu.label) {
52-
<a
53-
[routerLink]="menu.routerLink"
54-
class="pill"
55-
routerLinkActive="bg-blue-50 text-blue-600 border-0"
56-
data-testid="menu-item"
57-
[routerLinkActiveOptions]="menu.routerLinkActiveOptions">
58-
@if (menu.icon) {
59-
<i [class]="menu.icon"></i>
60-
}
61-
{{ menu.label }}
62-
</a>
63-
}
64-
@if (hasWriterAccess()) {
65-
<div class="flex md:hidden">
66-
<a
67-
routerLink="/project/{{ projectSlug() }}/settings"
68-
class="pill"
69-
routerLinkActive="bg-blue-50 text-blue-600 border-0 block md:hidden"
70-
data-testid="mobile-menu-item"
71-
[routerLinkActiveOptions]="{ exact: true }">
72-
<i class="fa-light fa-gear"></i>
73-
<span>Settings</span>
74-
</a>
75-
</div>
76-
}
77-
</div>
78-
@if (hasWriterAccess()) {
79-
<div class="items-center gap-3 hidden md:flex">
80-
<a
81-
routerLink="/project/{{ projectSlug() }}/settings"
82-
class="pill"
83-
routerLinkActive="bg-blue-50 text-blue-600 border-0"
84-
data-testid="menu-item"
85-
[routerLinkActiveOptions]="{ exact: true }">
86-
<i class="fa-light fa-gear"></i>
87-
<span>Settings</span>
88-
</a>
89-
</div>
90-
}
91-
</div>
92-
</div>
93-
</div>
947
@if (project()) {
958
<router-outlet></router-outlet>
969
}

apps/lfx-one/src/app/layouts/project-layout/project-layout.component.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { CommonModule } from '@angular/common';
55
import { Component, computed, inject, input, Signal, signal } from '@angular/core';
66
import { toSignal } from '@angular/core/rxjs-interop';
77
import { ActivatedRoute, RouterModule } from '@angular/router';
8-
import { BreadcrumbComponent } from '@components/breadcrumb/breadcrumb.component';
98
import { FilterButton, Project } from '@lfx-one/shared/interfaces';
109
import { ProjectService } from '@services/project.service';
1110
import { HeaderComponent } from '@shared/components/header/header.component';
@@ -16,7 +15,7 @@ import { of, switchMap } from 'rxjs';
1615
@Component({
1716
selector: 'lfx-project-layout',
1817
standalone: true,
19-
imports: [CommonModule, RouterModule, HeaderComponent, BreadcrumbComponent, ChipModule],
18+
imports: [CommonModule, RouterModule, HeaderComponent, ChipModule],
2019
templateUrl: './project-layout.component.html',
2120
styleUrl: './project-layout.component.scss',
2221
})

apps/lfx-one/src/app/modules/committees/committee-dashboard/committee-dashboard.component.html

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@
99
<p class="text-gray-600">Loading {{ committeeLabel.toLowerCase() }} details...</p>
1010
</div>
1111
</div>
12-
}
13-
14-
@if (committees() && !committeesLoading()) {
12+
} @else {
1513
<div class="w-full space-y-6">
1614
<!-- Page Title with Create Group Button -->
1715
<div class="flex items-center justify-between">
18-
<h1 class="font-['Inter'] text-xl font-semibold">{{ committeeLabel }}</h1>
16+
<h1 class="font-inter text-xl font-semibold">{{ committeeLabelPlural }}</h1>
1917
@if (canCreateGroup()) {
2018
<lfx-button
2119
[label]="'Create ' + committeeLabel"

apps/lfx-one/src/app/modules/committees/components/committee-form/committee-form.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
77
<!-- Left Column - Basic Information -->
88
<div class="space-y-6">
9-
<h2 class="text-lg mb-4" style="font-family: Inter, sans-serif; font-weight: 400">Basic Information</h2>
9+
<h2 class="text-lg mb-4 font-inter font-normal">Basic Information</h2>
1010

1111
<!-- Committee Name -->
1212
<div class="flex flex-col gap-2">
@@ -88,7 +88,7 @@ <h2 class="text-lg mb-4" style="font-family: Inter, sans-serif; font-weight: 400
8888

8989
<!-- Right Column - Settings -->
9090
<div class="space-y-6">
91-
<h2 class="text-lg mb-4" style="font-family: Inter, sans-serif; font-weight: 400">Settings</h2>
91+
<h2 class="text-lg mb-4 font-inter font-normal">Settings</h2>
9292

9393
<div class="space-y-4">
9494
<!-- Business Email Required -->

apps/lfx-one/src/app/modules/committees/components/committee-table/committee-table.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
{{ committee.category || 'Other' }}
2626
</span>
2727
<div class="flex items-center gap-2">
28-
<p class="font-['Inter:Medium',sans-serif] font-medium leading-[16px] not-italic text-[#0f172b] text-[12px] text-nowrap whitespace-pre">
28+
<p class="font-inter font-medium leading-[16px] not-italic text-[#0f172b] text-[12px] text-nowrap whitespace-pre">
2929
{{ committee.name }}
3030
</p>
3131
@if (!committee.public) {

apps/lfx-one/src/app/modules/meetings/meetings-dashboard/meetings-dashboard.component.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33

44
import { CommonModule } from '@angular/common';
55
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
6-
import { toSignal } from '@angular/core/rxjs-interop';
6+
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
77
import { MeetingCardComponent } from '@app/shared/components/meeting-card/meeting-card.component';
88
import { ProjectContextService } from '@app/shared/services/project-context.service';
99
import { ButtonComponent } from '@components/button/button.component';
1010
import { Meeting, ProjectContext } from '@lfx-one/shared/interfaces';
1111
import { getCurrentOrNextOccurrence } from '@lfx-one/shared/utils';
1212
import { MeetingService } from '@services/meeting.service';
1313
import { PersonaService } from '@services/persona.service';
14-
import { BehaviorSubject, map, switchMap, tap } from 'rxjs';
14+
import { BehaviorSubject, catchError, map, merge, of, switchMap, tap } from 'rxjs';
1515

1616
import { MeetingsTopBarComponent } from './components/meetings-top-bar/meetings-top-bar.component';
1717

@@ -40,18 +40,24 @@ export class MeetingsDashboardComponent {
4040
public isNonFoundationProjectSelected: Signal<boolean>;
4141

4242
public constructor() {
43+
// Initialize project context first (needed for reactive data loading)
44+
this.project = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation());
45+
46+
// Initialize permission checks
47+
this.isMaintainer = computed(() => this.personaService.currentPersona() === 'maintainer');
48+
this.isNonFoundationProjectSelected = computed(() => this.projectContextService.selectedProject() !== null);
49+
50+
// Initialize state
4351
this.meetingsLoading = signal<boolean>(true);
4452
this.refresh$ = new BehaviorSubject<void>(undefined);
45-
this.meetings = this.initializeMeetings();
4653
this.currentView = signal<'list' | 'calendar'>('list');
4754
this.searchQuery = signal<string>('');
4855
this.timeFilter = signal<'upcoming' | 'past'>('upcoming');
4956
this.topBarVisibilityFilter = signal<'mine' | 'public'>('mine');
57+
58+
// Initialize data with reactive pattern
59+
this.meetings = this.initializeMeetings();
5060
this.filteredMeetings = this.initializeFilteredMeetings();
51-
this.project = computed(() => this.projectContextService.selectedProject() || this.projectContextService.selectedFoundation());
52-
this.isMaintainer = computed(() => this.personaService.currentPersona() === 'maintainer');
53-
// A non-foundation project is selected if selectedProject is not null
54-
this.isNonFoundationProjectSelected = computed(() => this.projectContextService.selectedProject() !== null);
5561
}
5662

5763
public onViewChange(view: 'list' | 'calendar'): void {
@@ -64,10 +70,23 @@ export class MeetingsDashboardComponent {
6470
}
6571

6672
private initializeMeetings(): Signal<Meeting[]> {
73+
// Convert project signal to observable to react to project changes
74+
const project$ = toObservable(this.project);
75+
6776
return toSignal(
68-
this.refresh$.pipe(
69-
switchMap(() =>
70-
this.meetingService.getMeetings().pipe(
77+
merge(
78+
project$, // Triggers on project context changes
79+
this.refresh$ // Triggers on manual refresh
80+
).pipe(
81+
tap(() => this.meetingsLoading.set(true)),
82+
switchMap(() => {
83+
const project = this.project();
84+
if (!project?.projectId) {
85+
this.meetingsLoading.set(false);
86+
return of([]);
87+
}
88+
89+
return this.meetingService.getMeetings().pipe(
7190
map((meetings) => {
7291
// Sort meetings by current or next occurrence start time (earliest first)
7392
return meetings.sort((a, b) => {
@@ -87,13 +106,16 @@ export class MeetingsDashboardComponent {
87106
return new Date(occurrenceA.start_time).getTime() - new Date(occurrenceB.start_time).getTime();
88107
});
89108
}),
109+
catchError((error) => {
110+
console.error('Failed to load meetings:', error);
111+
this.meetingsLoading.set(false);
112+
return of([]);
113+
}),
90114
tap(() => this.meetingsLoading.set(false))
91-
)
92-
)
115+
);
116+
})
93117
),
94-
{
95-
initialValue: [],
96-
}
118+
{ initialValue: [] }
97119
);
98120
}
99121

apps/lfx-one/src/app/modules/project/committees/committee-view/committee-view.component.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<div class="flex justify-center items-center min-h-96">
88
<div class="text-center">
99
<i class="fa-light fa-spinner-third fa-spin text-3xl text-blue-600 mb-4"></i>
10-
<p class="text-gray-600">Loading committee details...</p>
10+
<p class="text-gray-600">Loading group details...</p>
1111
</div>
1212
</div>
1313
}
@@ -17,9 +17,9 @@
1717
<div class="flex justify-center items-center min-h-96">
1818
<div class="text-center">
1919
<i class="fa-light fa-exclamation-triangle text-3xl text-red-500 mb-4"></i>
20-
<h2 class="text-xl font-semibold text-gray-900 mb-2">Committee Not Found</h2>
21-
<p class="text-gray-600 mb-4">The committee you're looking for doesn't exist or has been removed.</p>
22-
<lfx-button label="Back to Committees" styleClass="p-button-outlined" (onClick)="goBack()"> </lfx-button>
20+
<h2 class="text-xl font-semibold text-gray-900 mb-2">Group Not Found</h2>
21+
<p class="text-gray-600 mb-4">The group you're looking for doesn't exist or has been removed.</p>
22+
<lfx-button label="Back to Groups" styleClass="p-button-outlined" (onClick)="goBack()"> </lfx-button>
2323
</div>
2424
</div>
2525
}
@@ -33,7 +33,7 @@ <h2 class="text-xl font-semibold text-gray-900 mb-2">Committee Not Found</h2>
3333
<div class="flex items-center gap-3 mb-2">
3434
<button (click)="goBack()" class="inline-flex items-center text-gray-500 hover:text-gray-700 transition-colors">
3535
<i class="fa-light fa-arrow-left mr-2"></i>
36-
Back to Committees
36+
Back to Groups
3737
</button>
3838
</div>
3939
<h1 class="text-3xl font-bold text-gray-900 mb-2">{{ committee()?.name }}</h1>

0 commit comments

Comments
 (0)