Skip to content

Commit efb80ae

Browse files
authored
feat(dashboards): add social listening sidebar entry for ed persona (LFXV2-1689) (#792)
* feat(dashboards): add social listening sidebar entry for ed persona (LFXV2-1689) - refactor metrics items into a local array to enable conditional append - add social listening entry with pcc deep-link url when foundation uid is present - hide entry when no foundation is selected to avoid broken deep links - reading selectedFoundation() inside computed auto-wires reactive updates on foundation switch Signed-off-by: Audi Young <audi.mycloud@gmail.com> * fix(dashboards): normalize pcc base url for social listening link (LFXV2-1689) Signed-off-by: Audi Young <audi.mycloud@gmail.com> * feat(projects): add bff endpoint for uid to sfid resolution (LFXV2-1689) - Register GET /api/projects/:uid/sfid route, mounted before documents block to keep the /:uid/<verb> family clustered - Add ProjectController.getProjectSfid wrapping the existing NATS-backed getProjectSfidByUid helper - Return { sfid: string | null } with HTTP 200 on lookup failure so callers can hide affordances uniformly Signed-off-by: Audi Young <audi.mycloud@gmail.com> * fix(review): validate uid param in getProjectSfid (LFXV2-1689) - Adopt validateUidParameter (canonical helper per server-helpers.md) to bring getProjectSfid in line with every other /:uid endpoint in this controller - Closes Important finding from post-commit code review on 04c2708 (defense-in-depth consistency gap) Signed-off-by: Audi Young <audi.mycloud@gmail.com> * feat(projects): add client getProjectSfid method (LFXV2-1689) - add getProjectSfid(uid) on client ProjectService hitting /api/projects/:uid/sfid - unwrap { sfid } response to string | null at the rxjs layer - catchError to of(null) so caller hides cleanly on lookup failure - import map from rxjs alphabetically alongside catchError Signed-off-by: Audi Young <audi.mycloud@gmail.com> * fix(review): encode uid and log sfid lookup errors (LFXV2-1689) - wrap uid in encodeURIComponent on /api/projects/:uid/sfid URL - console.error before of(null) so silent 401s/5xx surface in console - aligns with sibling list GETs in project.service.ts that log on failure Signed-off-by: Audi Young <audi.mycloud@gmail.com> * feat(projects): expose selectedFoundationSfid on context service (LFXV2-1689) - add selectedFoundationSfid: Signal<string | null> to ProjectContextService - initializer mirrors initCanWrite shape via toObservable + switchMap - short-circuits to of(null) when no foundation selected (no http call) - switchMap cancels in-flight lookups on foundation switch Signed-off-by: Audi Young <audi.mycloud@gmail.com> * fix(dashboards): use foundation sfid for social listening url (LFXV2-1689) - swap selectedFoundation()?.uid for selectedFoundationSfid() signal - compose PCC URL with /project/<sfid>/reports/social-listening - hide sidebar entry until sfid resolves (no broken UUID-based URL) - no behaviour change to ED gate or sibling Metrics items Signed-off-by: Audi Young <audi.mycloud@gmail.com> * fix(project-context): reset sfid while foundation changes (LFXV2-1689) Signed-off-by: Audi Young <audi.mycloud@gmail.com> * fix(projects): gate SFID lookup on project access (LFXV2-1689) Signed-off-by: Audi Young <audi.mycloud@gmail.com> --------- Signed-off-by: Audi Young <audi.mycloud@gmail.com>
1 parent f8a0529 commit efb80ae

5 files changed

Lines changed: 95 additions & 17 deletions

File tree

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

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -289,24 +289,40 @@ export class MainLayoutComponent {
289289
}
290290

291291
if (this.personaService.currentPersona() === 'executive-director') {
292+
const metricsItems: SidebarMenuItem[] = [
293+
{
294+
label: 'Health Metrics',
295+
icon: 'fa-light fa-chart-line-up',
296+
routerLink: '/foundation/health-metrics',
297+
testId: 'sidebar-metrics-health-metrics',
298+
},
299+
{
300+
label: 'Marketing Impact',
301+
icon: 'fa-light fa-bullhorn',
302+
routerLink: '/foundation/marketing-impact',
303+
testId: 'sidebar-metrics-marketing-impact',
304+
},
305+
];
306+
307+
const foundationSfid = this.projectContextService.selectedFoundationSfid();
308+
if (foundationSfid) {
309+
const pccBaseUrl = environment.urls.pcc;
310+
const baseUrl = pccBaseUrl.endsWith('/') ? pccBaseUrl.slice(0, -1) : pccBaseUrl;
311+
metricsItems.push({
312+
label: 'Social Listening',
313+
icon: 'fa-light fa-ear-listen',
314+
url: `${baseUrl}/project/${foundationSfid}/reports/social-listening`,
315+
target: '_blank',
316+
rel: 'noopener noreferrer',
317+
testId: 'sidebar-metrics-social-listening',
318+
});
319+
}
320+
292321
items.push({
293322
label: 'Metrics',
294323
isSection: true,
295324
expanded: true,
296-
items: [
297-
{
298-
label: 'Health Metrics',
299-
icon: 'fa-light fa-chart-line-up',
300-
routerLink: '/foundation/health-metrics',
301-
testId: 'sidebar-metrics-health-metrics',
302-
},
303-
{
304-
label: 'Marketing Impact',
305-
icon: 'fa-light fa-bullhorn',
306-
routerLink: '/foundation/marketing-impact',
307-
testId: 'sidebar-metrics-marketing-impact',
308-
},
309-
],
325+
items: metricsItems,
310326
});
311327
}
312328

apps/lfx-one/src/app/shared/services/project-context.service.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Router } from '@angular/router';
88
import { isBoardScopedPersona, ProjectContext } from '@lfx-one/shared/interfaces';
99
import { isSameProjectContext } from '@lfx-one/shared/utils';
1010
import { SsrCookieService } from 'ngx-cookie-service-ssr';
11-
import { catchError, map, of, switchMap } from 'rxjs';
11+
import { catchError, map, of, startWith, switchMap } from 'rxjs';
1212

1313
import { LensService } from './lens.service';
1414
import { PersonaService } from './persona.service';
@@ -41,6 +41,9 @@ export class ProjectContextService {
4141
/** Writer permission for the current active context — drives CTA visibility across dashboards. */
4242
public readonly canWrite: Signal<boolean> = this.initCanWrite();
4343

44+
/** Salesforce 18-char ID for the active foundation — resolves PCC deep-link targets. `null` while resolving or unavailable. */
45+
public readonly selectedFoundationSfid: Signal<string | null> = this.initSelectedFoundationSfid();
46+
4447
public constructor() {
4548
// Clean up legacy cookies from the previous cookie-hydrated design.
4649
this.cookieService.delete(this.foundationStorageKey, '/');
@@ -144,4 +147,18 @@ export class ProjectContextService {
144147
{ initialValue: false }
145148
);
146149
}
150+
151+
private initSelectedFoundationSfid(): Signal<string | null> {
152+
return toSignal(
153+
toObservable(this.selectedFoundation).pipe(
154+
switchMap((foundation) => {
155+
if (!foundation?.uid) {
156+
return of(null);
157+
}
158+
return this.projectService.getProjectSfid(foundation.uid).pipe(startWith(null));
159+
})
160+
),
161+
{ initialValue: null }
162+
);
163+
}
147164
}

apps/lfx-one/src/app/shared/services/project.service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
55
import { inject, Injectable, signal, WritableSignal } from '@angular/core';
66
import { CreateProjectDocumentRequest, PendingActionItem, Project, ProjectDocument } from '@lfx-one/shared/interfaces';
7-
import { BehaviorSubject, catchError, Observable, of, shareReplay, take, tap } from 'rxjs';
7+
import { BehaviorSubject, catchError, map, Observable, of, shareReplay, take, tap } from 'rxjs';
88

99
@Injectable({
1010
providedIn: 'root',
@@ -47,6 +47,16 @@ export class ProjectService {
4747
return this.projectCache.get(cacheKey)!;
4848
}
4949

50+
public getProjectSfid(uid: string): Observable<string | null> {
51+
return this.http.get<{ sfid: string | null }>(`/api/projects/${encodeURIComponent(uid)}/sfid`).pipe(
52+
map((res) => res.sfid ?? null),
53+
catchError((error) => {
54+
console.error('Failed to fetch project sfid:', error);
55+
return of(null);
56+
})
57+
);
58+
}
59+
5060
public searchProjects(query: string): Observable<Project[]> {
5161
const params = new HttpParams().set('q', query);
5262

apps/lfx-one/src/server/controllers/project.controller.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ReadableStream as NodeReadableStream } from 'node:stream/web';
1313
import { ServiceValidationError } from '../errors';
1414
import { contentDispositionAttachment } from '../helpers/content-disposition.helper';
1515
import { buildVCalendar, fetchAllMeetingPages, meetingsToVEvents } from '../helpers/ics.helper';
16-
import { getStringQueryParam } from '../helpers/validation.helper';
16+
import { getStringQueryParam, validateUidParameter } from '../helpers/validation.helper';
1717
import { logger } from '../services/logger.service';
1818
import { MeetingService } from '../services/meeting.service';
1919
import { ProjectService } from '../services/project.service';
@@ -135,6 +135,39 @@ export class ProjectController {
135135
}
136136
}
137137

138+
/**
139+
* GET /projects/:uid/sfid — UUID → Salesforce 18-char ID translation via NATS.
140+
* Returns `{ sfid: string | null }`; null on lookup failure so callers can hide cleanly.
141+
* Project access is enforced via getProjectById before the NATS mapping (FGA on upstream).
142+
*/
143+
public async getProjectSfid(req: Request, res: Response, next: NextFunction): Promise<void> {
144+
const { uid } = req.params;
145+
const startTime = logger.startOperation(req, 'get_project_sfid', { project_uid: uid });
146+
147+
try {
148+
if (
149+
!validateUidParameter(uid, req, next, {
150+
operation: 'get_project_sfid',
151+
service: 'project_controller',
152+
})
153+
) {
154+
return;
155+
}
156+
157+
await this.projectService.getProjectById(req, uid, false);
158+
const sfid = await this.projectService.getProjectSfidByUid(req, uid);
159+
160+
logger.success(req, 'get_project_sfid', startTime, {
161+
project_uid: uid,
162+
resolved: sfid != null,
163+
});
164+
165+
res.json({ sfid });
166+
} catch (error) {
167+
next(error);
168+
}
169+
}
170+
138171
/**
139172
* GET /projects/:uid/permissions
140173
*/

apps/lfx-one/src/server/routes/projects.route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ router.put('/:uid/permissions/:username', (req, res, next) => projectController.
2626

2727
router.delete('/:uid/permissions/:username', (req, res, next) => projectController.removeUserFromProjectPermissions(req, res, next));
2828

29+
router.get('/:uid/sfid', (req, res, next) => projectController.getProjectSfid(req, res, next));
30+
2931
// ── Document routes (folders + links + file uploads) ─────────────────────
3032
router.get('/:uid/documents', (req, res, next) => projectController.getProjectDocuments(req, res, next));
3133
router.post('/:uid/documents', (req, res, next) => projectController.createProjectDocument(req, res, next));

0 commit comments

Comments
 (0)