Skip to content

Commit 76275ee

Browse files
bramweltclaude
andauthored
fix(navigation): show formation-stage projects in lens dropdown (#616)
* fix(navigation): show formation-stage projects in lens Three independent blockers prevented Formation - Engaged projects from appearing in the project lens dropdown for executive directors: 1. LENS_PERSONA_MAP excluded executive-director from the project lens, so EDs never entered eligibleUids. 2. buildQuery used filters: ['stage:Active'], which excluded Formation - Engaged projects upstream. 3. fetchSelectedItem rejected any project whose stage was not 'Active', so UID-based injection also failed. Fix: add executive-director to LENS_PERSONA_MAP.project, switch the project lens to filters_or for stage matching, and allow Formation - Engaged in fetchSelectedItem. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> * fix(review): address PR #616 review feedback Address review comment from coderabbitai[bot]: - navigation.service.ts: scope Formation - Engaged allowance in fetchSelectedItem to the project lens only, keeping the foundation lens strictly Active to prevent formation-stage foundations from being re-injected via selectedUid (per coderabbitai[bot]) Resolves 1 review thread. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> * fix(navigation): show formation foundations in lens A foundation goes Funded+Membership BEFORE going Active. During that window the foundation lens excluded it (upstream query required stage:Active) while filterPageResources also rejected it (computeIsFoundation requires Active). The project appeared only in the project lens, not the foundation lens. Fix by splitting "is a live foundation" (computeIsFoundation, unchanged) from "eligible for the foundation nav dropdown" (new computeIsFoundationEligible, allows Formation - Engaged). The foundation lens buildQuery now uses filters_or for stage (Active OR Formation - Engaged) combined with AND filters for funding/membership. filterPageResources and fetchSelectedItem for the foundation lens use the new helper. Also add ProjectStage enum mirroring the upstream Goa DSL in lfx-v2-project-service (ProjectStageAttribute) and replace bare string literals throughout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> * refactor(navigation): consolidate to single computeIsFoundation Remove computeIsFoundationEligible. Extend computeIsFoundation to include Formation - Engaged + Membership projects so there is a single source of truth for what constitutes a foundation. Also remove the 'SFDC' reference from ProjectStage enum JSDoc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> * docs(navigation): document filters vs filters_or semantics Add inline JSDoc to LensItemsQuery clarifying that filters is AND logic and filters_or is OR logic, combined with AND between them — matching the upstream query-service contract. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> * fix(navigation): include Formation - Exploratory in project lens Exploratory-stage projects should be visible to persona-eligible users in the project dropdown, same as Engaged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> * docs(interfaces): remove misleading JSDoc from stage The `stage` field is required (no `?`), so the "optional to tolerate pre-rollout records" comment was misleading. The `| string` union already handles non-enum values. Fixes copypastedfrom funding? JSDoc on required field. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Issue: LFXV2-2344 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> * refactor(navigation): extract PROJECT_LENS_ALLOWED_STAGES Extract the project-lens stage allowlist into a shared module-level constant to eliminate duplication between fetchSelectedItem and buildQuery, and fix the comment to accurately describe all three included stages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Issue: LFXV2-3162 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> --------- Signed-off-by: Trevor Bramwell <tbramwell@linuxfoundation.org> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d0069b4 commit 76275ee

7 files changed

Lines changed: 52 additions & 9 deletions

File tree

apps/lfx-one/src/server/services/navigation.service.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: MIT
33

44
import { LENS_PERSONA_MAP, NAV_MAX_UPSTREAM_ITERATIONS, NAV_MIN_ITEMS_PER_RESPONSE } from '@lfx-one/shared/constants';
5-
import { ProjectFunding } from '@lfx-one/shared/enums';
5+
import { ProjectFunding, ProjectStage } from '@lfx-one/shared/enums';
66
import {
77
EnrichedPersonaProject,
88
GetLensItemsParams,
@@ -22,6 +22,9 @@ import { personaDetectionService } from '../utils/persona-helper';
2222
import { logger } from './logger.service';
2323
import { MicroserviceProxyService } from './microservice-proxy.service';
2424

25+
/** Stages eligible for the project lens — Active plus supported pre-launch formation stages. */
26+
const PROJECT_LENS_ALLOWED_STAGES = new Set<string>([ProjectStage.Active, ProjectStage.FormationEngaged, ProjectStage.FormationExploratory]);
27+
2528
/** Powers the foundation/project lens dropdown. Root writers bypass the persona filter. */
2629
export class NavigationService {
2730
private readonly microserviceProxy: MicroserviceProxyService;
@@ -168,8 +171,11 @@ export class NavigationService {
168171
const project = response?.resources?.[0]?.data;
169172
if (!project) return null;
170173
// Mirror the main-pipeline contract so an archived selection doesn't get re-injected.
171-
if (project.stage !== 'Active') return null;
172-
if (lens === 'foundation' && !computeIsFoundation(project)) return null;
174+
if (lens === 'foundation') {
175+
if (!computeIsFoundation(project)) return null;
176+
} else {
177+
if (!PROJECT_LENS_ALLOWED_STAGES.has(project.stage)) return null;
178+
}
173179
return this.toLensItem(project);
174180
} catch (error) {
175181
logger.warning(req, 'fetch_selected_item', 'Failed to fetch selected lens item', { err: error, uid, lens });
@@ -191,8 +197,17 @@ export class NavigationService {
191197

192198
private buildQuery(lens: NavLens, pageToken: string | undefined, name: string | undefined): LensItemsQuery {
193199
// legal_entity_type negation is post-filtered (filter grammar has no exclusions).
194-
const filters = lens === 'foundation' ? ['stage:Active', `funding:${ProjectFunding.Funded}`, 'funding_model:Membership'] : ['stage:Active'];
195-
const base: LensItemsQuery = { type: 'project', filters, sort: 'name_asc' };
200+
const base: LensItemsQuery = { type: 'project', filters: [], sort: 'name_asc' };
201+
if (lens === 'foundation') {
202+
// Funding + membership required (AND); Active or Formation - Engaged accepted (OR).
203+
// This ensures pre-launch foundations appear in the dropdown before they go Active.
204+
base.filters = [`funding:${ProjectFunding.Funded}`, 'funding_model:Membership'];
205+
base.filters_or = [`stage:${ProjectStage.Active}`, `stage:${ProjectStage.FormationEngaged}`];
206+
} else {
207+
// Include active projects plus supported pre-launch formation stages so
208+
// persona-eligible pre-launch projects appear in the project dropdown.
209+
base.filters_or = [...PROJECT_LENS_ALLOWED_STAGES].map((stage) => `stage:${stage}`);
210+
}
196211

197212
if (pageToken) base.page_token = pageToken;
198213
if (name) base.name = name;

packages/shared/src/constants/lens.constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const NAV_LENSES: readonly NavLens[] = ['foundation', 'project'] as const
6363

6464
export const LENS_PERSONA_MAP: Readonly<Record<NavLens, readonly PersonaType[]>> = {
6565
foundation: ['board-member', 'executive-director'],
66-
project: ['contributor', 'maintainer'],
66+
project: ['contributor', 'maintainer', 'executive-director'],
6767
} as const;
6868

6969
export const NAV_MIN_ITEMS_PER_RESPONSE = 15;

packages/shared/src/enums/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './poll.enum';
1212
export * from './survey.enum';
1313
export * from './event.enum';
1414
export * from './project-funding.enum';
15+
export * from './project-stage.enum';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright The Linux Foundation and each contributor to LFX.
2+
// SPDX-License-Identifier: MIT
3+
4+
/**
5+
* Project lifecycle stage.
6+
* Values mirror the upstream Goa enum declared in `lfx-v2-project-service` at
7+
* `api/project/v1/design/types.go` (`ProjectStageAttribute`).
8+
*/
9+
export enum ProjectStage {
10+
FormationExploratory = 'Formation - Exploratory',
11+
FormationEngaged = 'Formation - Engaged',
12+
FormationOnHold = 'Formation - On Hold',
13+
FormationDisengaged = 'Formation - Disengaged',
14+
FormationConfidential = 'Formation - Confidential',
15+
Active = 'Active',
16+
Archived = 'Archived',
17+
Prospect = 'Prospect',
18+
}

packages/shared/src/interfaces/navigation.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export interface GetLensItemsParams {
5252
/** Upstream query-service params for lens-item lookups. */
5353
export interface LensItemsQuery {
5454
type: 'project';
55+
/** AND — all terms must match (e.g. `['funding:Funded', 'funding_model:Membership']`). */
5556
filters: string[];
57+
/** OR — at least one term must match (e.g. `['stage:Active', 'stage:Formation - Engaged']`). Combined with `filters` via AND. */
58+
filters_or?: string[];
5659
sort: 'name_asc';
5760
page_token?: string;
5861
name?: string;

packages/shared/src/interfaces/project.interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: MIT
33

44
import { ProjectFunding } from '../enums/project-funding.enum';
5+
import { ProjectStage } from '../enums/project-stage.enum';
56

67
export interface Project {
78
uid: string;
@@ -12,7 +13,7 @@ export interface Project {
1213
writer?: boolean;
1314
public: boolean;
1415
parent_uid: string;
15-
stage: string;
16+
stage: ProjectStage | string;
1617
category: string;
1718
/** Upstream Goa enum — optional to tolerate records indexed before the attribute was rolled out. */
1819
funding?: ProjectFunding;

packages/shared/src/utils/project.utils.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: MIT
33

44
import { ProjectFunding } from '../enums/project-funding.enum';
5+
import { ProjectStage } from '../enums/project-stage.enum';
56
import type { EnrichedPersonaProject, LensItem, Project, ProjectContext } from '../interfaces';
67

78
export function toProjectContext(project: EnrichedPersonaProject): ProjectContext {
@@ -35,7 +36,11 @@ export function isFoundationProject(project: EnrichedPersonaProject): boolean {
3536
// PCC test-project override (lfx-pcc helper.ts) — kept in sync with hasHealthMetricDashboard.
3637
const FOUNDATION_NAME_OVERRIDES = new Set(['Test Project Group IT', 'Test Project IT']);
3738

38-
/** Active, membership-funded foundation (Funding === 'Funded'), not an Internal Allocation. PCC test projects also qualify. */
39+
/**
40+
* Membership-funded foundation (Funding === 'Funded'), not an Internal Allocation.
41+
* Includes both live (Active) and pre-launch (Formation - Engaged) foundations.
42+
* PCC test projects also qualify.
43+
*/
3944
export function computeIsFoundation(project: Project | null): boolean {
4045
if (!project) {
4146
return false;
@@ -46,7 +51,7 @@ export function computeIsFoundation(project: Project | null): boolean {
4651
}
4752

4853
return (
49-
project.stage === 'Active' &&
54+
(project.stage === ProjectStage.Active || project.stage === ProjectStage.FormationEngaged) &&
5055
project.legal_entity_type !== 'Internal Allocation' &&
5156
project.funding === ProjectFunding.Funded &&
5257
Array.isArray(project.funding_model) &&

0 commit comments

Comments
 (0)