feat(formation): badge, card and staff deep links on the project page - #2019
feat(formation): badge, card and staff deep links on the project page#2019manishdixitlfx wants to merge 15 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Essentials Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
Pull request overview
Adds feature-flagged Formation-stage UI to project dashboards and selectors, including LF Staff admin-tool deep links.
Changes:
- Adds Formation stage classification and display helpers.
- Adds dashboard badges, subtitles, selector tags, and a Formation sidebar card.
- Expands project-lens stages and adds unit coverage.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
packages/shared/src/utils/project.utils.ts |
Adds Formation stage helpers. |
packages/shared/src/utils/project.utils.spec.ts |
Tests stage classification. |
packages/shared/src/utils/date-time.utils.ts |
Adds announcement-date formatting. |
packages/shared/src/interfaces/project.interface.ts |
Shares resolved staff-row type. |
packages/shared/src/interfaces/navigation.interface.ts |
Adds Formation metadata to lens items. |
packages/shared/src/constants/project-formation.constants.ts |
Defines Formation labels and Draft sentinel. |
packages/shared/src/constants/index.ts |
Exports Formation constants. |
packages/shared/src/constants/feature-flags.constants.ts |
Defines the Formation feature flag. |
apps/lfx-one/src/server/services/navigation.service.ts |
Expands stage filtering and maps Formation labels. |
apps/lfx-one/src/server/services/navigation.service.spec.ts |
Tests navigation filtering and mapping. |
apps/lfx-one/src/app/shared/services/sidebar-nav.service.ts |
Documents omitted navigation surfaces. |
apps/lfx-one/src/app/shared/services/project-context.service.ts |
Exposes active-project Formation state. |
apps/lfx-one/src/app/shared/services/project-context.service.spec.ts |
Tests Formation and write signals. |
apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.ts |
Adds feature-flag state. |
apps/lfx-one/src/app/shared/components/project-selector/project-selector.component.html |
Renders Formation selector tags. |
apps/lfx-one/src/app/modules/dashboards/project-dashboard/project-dashboard.component.ts |
Loads Formation subtitle data. |
apps/lfx-one/src/app/modules/dashboards/project-dashboard/project-dashboard.component.spec.ts |
Tests badge and subtitle states. |
apps/lfx-one/src/app/modules/dashboards/project-dashboard/project-dashboard.component.html |
Renders Formation dashboard UI. |
apps/lfx-one/src/app/modules/dashboards/components/project-staff-card/project-staff-card.component.ts |
Uses the shared staff-row type. |
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.ts |
Implements Formation card behavior. |
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.spec.ts |
Tests card states and links. |
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.html |
Defines Formation card markup. |
apps/lfx-one/src/app/modules/dashboards/components/dashboard-sidebar/dashboard-sidebar.component.ts |
Adds opt-in Formation card gating. |
apps/lfx-one/src/app/modules/dashboards/components/dashboard-sidebar/dashboard-sidebar.component.spec.ts |
Tests sidebar gating. |
apps/lfx-one/src/app/modules/dashboards/components/dashboard-sidebar/dashboard-sidebar.component.html |
Mounts the Formation card. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| public readonly activeProjectFormationSubStage: Signal<string | null> = computed(() => getFormationSubStageLabel(this.activeProject()?.stage)); | ||
|
|
||
| /** True when the current active context is in Draft or any Formation sub-stage. */ | ||
| public readonly isActiveProjectInFormation: Signal<boolean> = computed(() => isFormationStage(this.activeProject()?.stage)); |
| return toSignal( | ||
| toObservable(computed(() => (this.isLFStaff() ? this.projectUid() : null))).pipe( | ||
| filter((uid): uid is string => !!uid), | ||
| switchMap((uid) => this.projectService.getProjectSfid(uid)) |
| logoUrl: string | null; | ||
| isFoundation: boolean; | ||
| /** Formation sub-stage label from `getFormationSubStageLabel`, or `null` outside Formation. */ | ||
| formationSubStage: string | null; |
| ProjectStage.FormationOnHold, | ||
| ProjectStage.FormationDisengaged, | ||
| ]); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.ts:137
- Clear the previous mapping when
projectUidchanges.toSignalretains the last SFID while the new HTTP request is pending, so after switching between two Formation projects the card temporarily combines the new project's slug/intake data with the old project's admin-tool URL; a staff user can therefore open the wrong project. MirrorinitSelectedFoundationSfid()by starting each inner lookup withnull(and importstartWith) so the link stays hidden until the current UID resolves.
toObservable(computed(() => (this.isLFStaff() ? this.projectUid() : null))).pipe(
filter((uid): uid is string => !!uid),
switchMap((uid) => this.projectService.getProjectSfid(uid))
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
Previously missed (4) — in code that hasn't changed since the last review.
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.ts:67
- This gates the admin-tool affordance on organization-wide staff membership, but #1955 requires the resolved Formation permission from #2148 and explicitly says participants/item owners must not receive it. The repository's permission model also treats persona/team membership as presentation context, not action authority. Resolve the Formation relation and check that permission before rendering the link.
apps/lfx-one/src/app/shared/services/sidebar-nav.service.ts:465 - The linked #1955 requirement still calls for a stage-scoped Formation navigation item. This comment records that the requirement is intentionally omitted, but it does not resolve the acceptance-criteria mismatch. Either add a distinct destination and restore the item, or update the issue/scope before merging.
apps/lfx-one/src/server/services/navigation.service.ts:32 - Adding these stages to the server-side query makes On Hold and Disengaged projects appear in every user's picker even when
formation-enabledis false. That makes the dark-launched feature observable before rollout; the client-side tag gate cannot hide the rows themselves. Gate this expansion with a server feature flag or defer it until the rollout is enabled.
apps/lfx-one/src/app/modules/dashboards/project-dashboard/project-dashboard.component.html:28 - This user-facing caption says a Confidential formation can become visible by invitation, but Epic 1 explicitly excludes Confidential formations from invitations. Remove “until invited” so the page does not promise an unavailable visibility path.
apps/lfx-one/src/app/shared/services/project-context.service.ts:292
toSignalretains the previous emission while the newgetProjectrequest is pending. Now thatactiveProjectsupplies rendered Formation data, switching from project A to B temporarily combines B's heading/context with A's stage, slug, and admin-link inputs. PreservecanWritesafely without retaining cross-project display data—for example, track writer resolution separately or key the emitted project to the current context.
// organizer mid-edit. Formation's badge/card briefly showing the previous project's stage
// during a switch is an accepted, pre-existing trade-off (this is the same staleness
// canWrite itself already had before this ticket).
return this.projectService.getProject(ctx.slug, false);
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.ts:116
- On a second project UID,
switchMapcancels the prior request but the signal continues exposing the prior SFID until the new lookup emits. The card for project B can therefore render an admin URL for project A, allowing staff to open the wrong project. Start each inner SFID lookup withnulland cover a Formation-to-Formation switch in the test.
toObservable(computed(() => (this.isLFStaff() ? this.projectUid() : null))).pipe(
filter((uid): uid is string => !!uid),
switchMap((uid) => this.projectService.getProjectSfid(uid))
| data-testid="formation-card-admin-tool-link"> | ||
| Open in admin tool ↗ |
…GH-1955) Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
- FormationCardComponent reads ProjectContextService.activeProject instead of an independent getProject fetch, removing a duplicate HTTP call and making hasError meaningful again (it now reflects the settings fetch, which can genuinely fail). - Log before every catchError fallback that previously failed silently. - Validate repository_url scheme before binding it to [href]. - Move the ED/PM/opportunity-owner StaffRow type to packages/shared/src/interfaces so ProjectStaffCardComponent and FormationCardComponent share one definition instead of two. - Replace the admin-tool buttons + window.open with plain anchors. - Remove the sidebar "Formation" nav item: pointing it at the same route as Dashboard highlighted both entries simultaneously, and routerLinkActive ignores fragment/queryParams so no URL variant fixes it without a new route (out of scope here). The badge and sidebar card already surface Formation status on that page. - Drop the unused TooltipModule import and isFormationStage export; wire isFormationStage into ProjectContextService.isActiveProjectInFormation instead of leaving it uncalled. - Misc: data-testid on the new interactive elements, consistent date formatting/fallback between the card and the dashboard subtitle, text-xs instead of an arbitrary text-[11px]. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…1955) - Remove the outer catchError in ProjectContextService.initActiveProjectDetails — ProjectService.getProject already logs and resolves to null on failure, so the wrapper was unreachable dead code. - Extract FormationCardComponent's repositoryUrl computed into a private initRepositoryUrl(), matching settings/sfid's init-function pattern for multi-statement signals. - Update FORMATION_ENABLED_FLAG's doc comment: it no longer gates a nav item (removed in the prior fix commit). Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…GH-1955) - FormationCardComponent no longer has a projectUid input — it derives its own uid from ProjectContextService.activeProject, closing the divergence risk of a caller passing a different uid than the active project. - Scope loading/hasError to the staff-rows + announcement-date section only; the sub-stage pill, slug, intake links, and admin-tool links all come from the already-resolved activeProject/sfid and no longer disappear when only the settings fetch fails. - Use the shared isValidUrl (protocol allowlist + dangerous-pattern denylist + private/localhost rejection) instead of a hand-rolled scheme check for repositoryUrl. - Use formatIsoDateLabel for announcement_date on both the card and the dashboard subtitle — Angular's DatePipe throws on an unparseable non-empty string, and announcement_date is unvalidated upstream data. - Add startWith(null) to ProjectContextService.initActiveProjectDetails so canWrite/Formation signals clear immediately on a project switch instead of briefly reading the previous project. - Extract staff/announcementDateLabel/editStageUrl/setUpUrl into private init functions for consistency with settings/sfid/repositoryUrl. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
… signal (GH-1955) - Revert startWith(null) on ProjectContextService.initActiveProjectDetails. canWrite reads this signal, and evictOnWriteAccessLoss() (vote/survey/ mailing-list manage pages) takes the first true->false transition as a genuine access loss and navigates away — a transient null on every project switch would evict an organizer mid-edit, not just on a real loss of write access. The prior "matches initSelectedFoundationSfid" justification didn't hold: that signal feeds no eviction path. Formation's badge/card briefly showing the previous project's stage during a switch is the same pre-existing staleness canWrite already had before this ticket, not a regression to fix here. - Remove FormationCardComponent's unused `loaded` signal — the template's outer gate moved to `project()` in the prior commit, so `loaded` was write-only dead state. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
… test (GH-1955) - Add shared formatAnnouncementDateLabel to date-time.utils.ts and use it from both FormationCardComponent and ProjectDashboardComponent, replacing the duplicated formatIsoDateLabel(date) ?? 'Not set' inline in each. - Widen the Formation card's error copy — it covers both the announcement date and staff contacts, not just contacts. - Correct the class doc comment: sfid() is an independent, uncached fetch that starts null (unlike project(), which is resolved by the time this card can mount) — the admin-links block pops in once it resolves. - Add a regression test locking in that a project switch never emits a transient null/false on activeProject/canWrite while the new fetch is in flight — the exact bug the prior two commits introduced and reverted. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…ype lookup (GH-1955) - Correct the admin-tool deep link: verified against lfx-pcc's actual routing, PCC has no ?tab= query param at all — its real route is the path segment /project/:id/setup. Collapse the two invented "Edit stage"/"Set up" links (which both silently resolved to the same wrong page) into one verified "Set up in admin tool" link; a stage-specific destination needs a product/PCC decision before a second link can exist. - Fix a project-entity mismatch: DashboardSidebarComponent is shared by three dashboards (project/ED/board-member) that each pass a different entity as projectUid, but FormationCardComponent reads ProjectContextService.activeProject regardless of which one. Add an explicit showFormationCard input, defaulting false, set true only by ProjectDashboardComponent where projectUid and activeProject are guaranteed to agree. - Harden getFormationSubStageLabel/isFormationStage against prototype pollution: use Object.hasOwn instead of `in`/bracket access, so a stage string colliding with an Object.prototype member name (toString, constructor, ...) can't read a function off the prototype chain instead of null. - Gate the SFID fetch on isLFStaff — non-staff viewers were paying for a round trip whose result they can never see. - Add a loading guard to the dashboard subtitle's announcement-date clause — it was asserting "Not set" for every Formation project until the settings fetch resolved, then flipping to the real date. - Add project-dashboard.component.spec.ts — the one new user-visible surface (badge/subtitle) had no coverage. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…1955) - Correct the admin-tool link again: the prior "verified" /setup path was checked against PCC's v1 frontend, but environment.urls.pcc resolves to v2, which has no /setup route (confirmed against v2-frontend's pages-routing.module.ts and serverless.yml host mappings). Stop guessing a third destination — link to the one route confirmed to resolve, the bare /project/:id page, until product/PCC names the real one. - Suppress the dashboard subtitle's announcement-date clause on a failed settings fetch too, not just while loading — otherwise a fetch error still fell through to a false "Not set". - Collapse isFormationStage's now-redundant DRAFT_STAGE_SENTINEL clause; getFormationSubStageLabel already covers it. - Add prototype-pollution regression tests (toString/constructor/ hasOwnProperty as a stage value) for the Object.hasOwn hardening. - Add dashboard-sidebar.component.spec.ts locking in the showFormationCard opt-in default and gating. - Fix project-dashboard.component.spec.ts: awaiting fixture.whenStable() (per the zoneless-CD testing convention) lets @defer(on idle) blocks actually resolve during the test, so the real RecentProgress/ MyMeetings/PendingActions children were instantiating and pulling in a full DI tree neither test provided. Stub all three. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…placeholder (GH-1955) - encodeURIComponent the SFID before interpolating it into the PCC deep link. - Relabel the admin-tool link "Open in admin tool" — it now points to the bare project page, not a setup page, and "Set up" promised an affordance that isn't there. - Fix DRAFT_STAGE_SENTINEL's doc comment: the literal check now lives only in getFormationSubStageLabel (isFormationStage delegates to it), not in both functions. - Show an em-dash placeholder for the dashboard subtitle's announcement date while it loads, instead of omitting the clause outright. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
GH-1955) - Class docstring now explains the two AC substitutions this ticket makes on data/permissions that don't exist yet: formation_admin (confirmed absent from every linuxfoundation repo, not just this one) gated via isLFStaff instead, and the ED/PM/opportunity-owner contact rows duplicating ProjectStaffCardComponent (approved trade-off, ticket's formation-specific contact fields don't exist upstream). - initAdminToolUrl's route survey now also names v2's project-formation route and why it was rejected (no :id param — it's the global Formations queue from #1956, not a per-project page). - sidebar-nav.service.ts's reduced-surface comment now names Insights alongside EasyCLA/Crowdfunding/public stats — all four have no project-scoped nav surface to gate, not just three. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…H-1955) Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
…map (GH-1955) Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
405465d to
1775f9a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated 4 comments.
Suppressed comments (2)
apps/lfx-one/src/app/shared/services/project-context.service.ts:103
- These Formation signals expose the previous project's stage while a new context fetch is pending because
toSignalretains its last value. The dashboard heading already switches to the new context, so it can temporarily show project A's badge/card—and its admin link—under project B. Preserve the stale value only forcanWrite, and make Formation state null unless the fetched project's UID matches the active context.
public readonly activeProjectFormationSubStage: Signal<string | null> = computed(() => getFormationSubStageLabel(this.activeProject()?.stage));
/** True when the current active context is in Draft or any Formation sub-stage. */
public readonly isActiveProjectInFormation: Signal<boolean> = computed(() => isFormationStage(this.activeProject()?.stage));
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.html:52
- #1955 requires two distinct staff deep links—“Edit stage” and “Set up”—to the admin tool's Project Definition, but this renders one generic “Open” link. The unresolved PCC destination should be confirmed before merging; then render both required actions rather than silently dropping them.
Open in admin tool ↗
| * `FormationConfidential` is intentionally excluded (GH-1955): the name implies pre-announcement | ||
| * secrecy, so a Confidential project should not be listed by name in the shared project picker. |
| // GH-1955: a separate "Formation" nav item was tried here and removed on review — pointing it at | ||
| // the same '/project/overview' route as Dashboard means both entries render highlighted-active | ||
| // simultaneously (sidebar.component.html's routerLinkActive ignores fragment and queryParams, so | ||
| // neither can disambiguate two entries sharing one path). The Formation badge/subtitle and sidebar | ||
| // card already surface "you are in Formation" on that page; a real destination (a distinct route) |
| } | ||
|
|
||
| const includeMeetingCoordinator = req.query['meeting_coordinator'] === 'true'; | ||
| const includeAuditor = req.query['auditor'] === 'true'; |
| * **Staff-only gating**: FGA defines `writer`/`auditor`/`participant`/`item_owner` on `formation`; | ||
| * GH-1954 grants LF Staff `auditor` (not `writer`) on non-public projects, so the guard checks | ||
| * `auditor` OR `writer` on the project (see `initIsAuditor` below) — a `writer`-only guard would | ||
| * hide this deep link from most of staff. |
Thread includeAuditor through getProjectBySlug so ?auditor=true works symmetrically on both the slug and uid routes; add the missing logger.success on the uid route. Switch the meeting_coordinator/auditor FGA checks to checkSingleAccessStrict so a transient upstream failure actually surfaces as undefined instead of silently degrading to false, matching the fields' documented semantics. Correct the inverted "writer is a superset" rationale (auditor is the FGA superset per model.fga). Drop initIsAuditor's dead catchError (ProjectService.getProject already resolves failures to null) and fix the docblock's FGA-type/label inaccuracies, renaming the "Staff only" chip to "Admin only" since the gate also admits project writers and inherited foundation auditors. Update the spec to match: rename the staff fixture param to auditor, assert the getProject call args, and cover the writer===true OR-branch. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated 2 comments.
Suppressed comments (4)
apps/lfx-one/src/server/services/navigation.service.ts:16
- The current #1955 acceptance criteria explicitly require no Confidential-specific UI gate: direct-grant holders should see the project, while query-service authorization excludes everyone else. This exclusion also makes
fetchSelectedItemreject an authorized Confidential selection, so these projects disappear from the selector despite the updated requirement. IncludeFormationConfidentialand rely on the access-filtered query.
* `FormationConfidential` is intentionally excluded (GH-1955): the name implies pre-announcement
* secrecy, so a Confidential project should not be listed by name in the shared project picker.
* Its own dashboard page still renders normally for anyone already authorized to view it.
apps/lfx-one/src/app/shared/services/sidebar-nav.service.ts:499
- This leaves a current #1955 acceptance criterion unimplemented. The issue now requires the Formation nav item to return with a distinct Formation route; it no longer treats the missing destination as an open product question. Add the flagged route and point the nav item at it rather than documenting the omission as final.
// GH-1955: a separate "Formation" nav item was tried here and removed on review — pointing it at
// the same '/project/overview' route as Dashboard means both entries render highlighted-active
// simultaneously (sidebar.component.html's routerLinkActive ignores fragment and queryParams, so
// neither can disambiguate two entries sharing one path). The Formation badge/subtitle and sidebar
// card already surface "you are in Formation" on that page; a real destination (a distinct route)
// is needed before a dedicated nav item can ship — flagged for product/design.
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.html:52
- #1955 requests two distinct deep links—“Edit stage” and “Set up”—into the project definition. A single generic project-page link does not satisfy either action, so the deep-link portion of this PR remains incomplete. The actual PCC destinations need to be confirmed and represented separately before this is considered implemented.
Open in admin tool ↗
apps/lfx-one/src/app/shared/services/project-context.service.ts:292
- Keeping the previous
Projectvalue during a context switch now affects more than the pre-existingcanWriteboolean: the new header and Formation card can show project A’s stage, slug, announcement date, and admin link while the page heading already names project B. On a slow request this presents an actionable link for the wrong project. Preserve the old value only for the write-access eviction logic, and clear or context-key the display-facing project data as soon asactiveContextchanges.
// Deliberately no startWith(null) here, unlike initSelectedFoundationSfid below: canWrite
// reads this signal, and evictOnWriteAccessLoss() (vote/survey/mailing-list manage pages)
// takes the *first* true→false transition as a genuine access loss and navigates away. A
// transient null on every project switch — not just a real loss of access — would evict an
// organizer mid-edit. Formation's badge/card briefly showing the previous project's stage
// during a switch is an accepted, pre-existing trade-off (this is the same staleness
// canWrite itself already had before this ticket).
return this.projectService.getProject(ctx.slug, false);
| data-testid="formation-card-admin-tool-link"> | ||
| Open in admin tool ↗ | ||
| </a> | ||
| <lfx-tag value="Admin only" severity="secondary" [rounded]="true" /> |
| // Only run the auditor FGA check when the caller explicitly requests it (FormationCardComponent's | ||
| // staff deep-link guard) — same rationale, and the same Strict variant, as meeting_coordinator above. | ||
| if (includeAuditor) { | ||
| const isAuditor = await this.accessCheckService | ||
| .checkSingleAccessStrict(req, { resource: 'project', id: project.uid, access: 'auditor' }) |
- Split the writer-skip comment's auditor and meeting_coordinator rationale: auditor is implied by writer per model.fga, but meeting_coordinator is a direct-only grant — the skip is safe only because every consumer already ORs the field with writer === true. - Use the err field (not error: message) in the two logger.warning calls so stack traces and custom properties serialize correctly. - De-duplicate the near-identical logger.success/res.json blocks in getProjectBySlug's uuid/slug branches. - Fix the spec's getProjectSpy helper to merge getProjectResult with projectOverrides/auditor instead of silently discarding them. - Add service/controller test coverage for the auditor and meeting_coordinator gating added in the prior commit. Signed-off-by: Manish Dixit <mdixit@linuxfoundation.org>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 31 changed files in this pull request and generated no new comments.
Suppressed comments (4)
apps/lfx-one/src/app/shared/services/project-context.service.ts:103
- On
/project/overview, selecting another project updatesactiveContextwithout recreating the route, butactiveProjectintentionally retains the prior response while the next request is pending. These new computations therefore keep the old Formation state active beside the new project's heading, and the mounted card can temporarily show the previous project's slug/date. Key Formation state toactiveContextUid()so the UI clears during that mismatch while leaving the deliberately stickycanWritebehavior unchanged.
public readonly activeProjectFormationSubStage: Signal<string | null> = computed(() => getFormationSubStageLabel(this.activeProject()?.stage));
/** True when the current active context is in Draft or any Formation sub-stage. */
public readonly isActiveProjectInFormation: Signal<boolean> = computed(() => isFormationStage(this.activeProject()?.stage));
apps/lfx-one/src/app/shared/services/sidebar-nav.service.ts:499
- The current #1955 acceptance criteria require a distinct Formation route and nav destination; they explicitly say the previously removed same-route item must come back once it has that destination. This comment records the old decision but leaves that required surface unimplemented, so the ticket is not complete until the route and flag-gated nav item are added (or the issue scope is changed).
// GH-1955: a separate "Formation" nav item was tried here and removed on review — pointing it at
// the same '/project/overview' route as Dashboard means both entries render highlighted-active
// simultaneously (sidebar.component.html's routerLinkActive ignores fragment and queryParams, so
// neither can disambiguate two entries sharing one path). The Formation badge/subtitle and sidebar
// card already surface "you are in Formation" on that page; a real destination (a distinct route)
// is needed before a dedicated nav item can ship — flagged for product/design.
apps/lfx-one/src/app/modules/dashboards/components/formation-card/formation-card.component.html:52
- #1955 requests two actionable deep links—“Edit stage in admin tool” and “Set up in admin tool”—to the project-definition destination. This generic project-page link provides neither requested action, so the deep-link portion remains incomplete. Resolve the actual admin-tool destinations before merging or explicitly rescope the issue.
Open in admin tool ↗
apps/lfx-one/src/server/services/navigation.service.ts:33
- The current #1955/#1965 requirements explicitly supersede this exclusion: Confidential formations must appear like any other formation for users whose project grant lets the query return them, with access control as the only visibility gate. Leaving
FormationConfidentialout here also makesfetchSelectedItemdiscard an authorized current selection. Include it in the allowed-stage set instead of adding a second confidentiality policy in navigation.
ProjectStage.FormationOnHold,
ProjectStage.FormationDisengaged,
]);
Summary
Part of the Formation Checklist epic (#1965). Adds Formation-stage UI to the project dashboard, gated behind the new
FORMATION_ENABLED_FLAG:FORMATION · <sub-stage>) + subtitle on the project dashboard, with a distinct amber/lock treatment and visibility caption for the Confidential sub-stageFormationCardComponentin the sidebar: sub-stage pill, announcement date, project slug, and a staff-only deep link into the admin toolstage,formation_date,announcement_date)Scope decisions made during implementation
Several fields/behaviors the ticket asked for don't exist anywhere in the sync pipeline (verified against the live
lfx-v2-project-servicecontract viagh api, not a local checkout). Decided with the ticket owner before implementation:executive_director/program_manager/opportunity_ownerhere as a stand-in (duplicatingProjectStaffCardComponentdirectly above it in the sidebar); those rows are now removed entirely rather than duplicated, since Epic 1 doesn't call for formation-specific contacts on this card at all. If any of the intake fields (repository_url,logo_url) turn out to be populated independently of intake, they can come back later as ordinary project fields — not as an intake block.formation_adminpermission: doesn't exist anywhere in thelinuxfoundationorg. Staff-only gating usesPersonaService.isLFStaffinstead (LF-staff team membership) — a materially wider audience than a hypothetical formation-specific grant, but the closest real thing available. This is now tracked as open guard question FGA model change for the formation checklist: newformationtypes vs. new grants onproject#2148: the FGA model defineswriter/auditor/participant/item_owneronformation, and Per-project lf-staff tuples replacing ROOT inheritance; withheld on Formation - Confidential #1954 grants LF Staffauditor, notwriter— awriter-based guard would hide the deep link from most of staff, so the correct relation is an open architecture decision. Marked with a// TODO(#2148)at the guard site information-card.component.ts; guard behavior is unchanged in this PR.FormationConfidentialstays excluded from the shared project picker (consistent with the name implying pre-announcement secrecy); its own dashboard page still renders normally for anyone already authorized to view it. Epic 1 also excludes Confidential formations end-to-end at the grant layer (Per-project lf-staff tuples replacing ROOT inheritance; withheld on Formation - Confidential #1954), which narrows the title badge's amber/lock treatment's practical audience to lf-formation/lf-legal viewers who do have access — still a coherent visual signal for that audience.Found and fixed during review (not part of the original diff)
The post-commit reviewer trio caught several real issues across iteration, most notably:
/project/overview) is the same route Dashboard already uses —routerLinkActivecan't disambiguate two entries on one path (it ignores fragment/queryParams). Removed the nav item rather than ship a visibly broken sidebar; the badge + card already surface Formation status on that page. Formation badge, Formation card and staff-only admin-tool deep links on the project page #1955 as currently written still asks for this nav item — its bullet needs to be struck or given a distinct destination; that's an issue-text edit, not a code change this PR can make.DashboardSidebarComponentis shared by the project, ED, and board-member dashboards, each of which passes a different entity asprojectUid— butFormationCardComponentreadProjectContextService.activeProjectregardless. Fixed with an explicitshowFormationCardopt-in input, defaultingfalse, settrueonly byProjectDashboardComponentwhere the two are guaranteed to agree.?tab=query param that doesn't exist anywhere in PCC's routing, then a/setuproute that only exists on PCC's v1 frontend (environment.urls.pccresolves to v2). The link now points at the one route confirmed to resolve (/project/:id) — flagging for product/PCC: the real "edit stage" / "set up" destination(s) still need to be named.evictOnWriteAccessLoss()regression: an earlier review-driven fix addedstartWith(null)to a shared context signal to avoid stale-project flicker, which would have caused vote/survey/mailing-list manage pages to evict a user mid-edit on every project switch. Caught, reverted, and locked in with a regression test.stage in OBJECT), a duplicate HTTP fetch, several silentcatchErrors, and a missing loading guard that briefly asserted "Not set" before data arrived.Open items for product/design (not blockers, documented here per review)
PROJECT_LENS_ALLOWED_STAGESnow includesFormationOnHold/FormationDisengaged— this is server-side and unflagged (unlike the rest of this PR's UI), so those projects become picker-visible to all users ahead of the flagged UI reaching them. Doesn't widen data access (the picker is a UX convenience over already-reachable projects), but is a rollout-sequencing call worth a second look. Recommendation: this can be moved behind a flag cheaply — a newServerFeatureFlagentry (mirroringWeeklyBriefSlack/MarketingOpsFga) gating whether these two stages are added to the set, same pattern already used elsewhere inserver-feature-flag.helper.ts. Not implemented in this PR; flagging for a decision.[attr.title]on a non-focusable<span>) was copied verbatim fromProjectStaffCardComponent's existing pattern. Retired onFormationCardComponentitself as part of the Epic 1 scope trim (the contact rows it applied to are gone), butProjectStaffCardComponent's own instance is untouched — still inherited a11y debt there.Test plan
yarn lint && yarn format && yarn test && yarn buildall passisLFStaffpresent/absent — unit testedcanWriteflip during a project switchRefs #1955. Depends on nothing; #1958 reads the same
FORMATION_ENABLED_FLAGconstant added here.