From 4fa449658e39915314ee769e2e63cc791abd44f2 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Thu, 3 Sep 2026 10:58:15 +0200 Subject: [PATCH] Hide flow finalization behind a feature flag Finalizing a flow makes it permanently read-only and cannot be undone, and it is not part of the current workflow, so the controls that create that state are now hidden: the Finalized toggle in the title toolbar and the Finalized entry in the flows list filter. Gated by FLOW_FINALIZATION_ENABLED, matching SWIMLANES_ENABLED, so nothing is deleted and re-enabling is one line. Only the controls are gated. The badge on a flow row, the disabled delete and the read-only editor stay, because rows finalized before the switch still exist and hiding the explanation of why such a flow cannot be edited would make the app inexplicable. For the same reason the toggle reappears for a flow that is already finalized, so its state is never invisible in the place that owns it. A persisted FINALIZED list filter now falls back to showing everything: a filter whose control is hidden would otherwise keep narrowing the list with nothing on screen to clear it. The backend is untouched - finalized still gates editing and deletion there. Co-Authored-By: Claude Opus 5 (1M context) --- src/app/shared/feature-flags.ts | 11 +++++++ src/app/shared/flows-list/flows-list.html | 2 ++ src/app/shared/flows-list/flows-list.spec.ts | 29 +++++++++++++++++++ src/app/shared/flows-list/flows-list.ts | 13 +++++++-- .../shared/title-toolbar/title-toolbar.html | 2 ++ src/app/shared/title-toolbar/title-toolbar.ts | 3 +- 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/app/shared/feature-flags.ts b/src/app/shared/feature-flags.ts index 1c04de8..7c33acb 100644 --- a/src/app/shared/feature-flags.ts +++ b/src/app/shared/feature-flags.ts @@ -10,3 +10,14 @@ export const SWIMLANES_ENABLED = false; * aspirational. See docs/project-memory.md before changing this flag. */ export const PROJECTS_ENABLED = true; + +/** + * Finalizing a flow makes it permanently read-only and is irreversible, and it is not part of the + * current workflow, so the controls that create that state are hidden. + * + * Only the *controls* are gated - the Finalized toggle and the Finalized filter. The indicators + * that explain the state stay visible (the badge on a flow row, the disabled delete, the read-only + * editor), because rows finalized earlier still exist and hiding their explanation would make the + * app inexplicable. See docs/project-memory.md before changing this flag. + */ +export const FLOW_FINALIZATION_ENABLED = false; diff --git a/src/app/shared/flows-list/flows-list.html b/src/app/shared/flows-list/flows-list.html index a67b83e..5ca0db3 100644 --- a/src/app/shared/flows-list/flows-list.html +++ b/src/app/shared/flows-list/flows-list.html @@ -27,7 +27,9 @@ All Public Private + @if (finalizationEnabled) { Finalized + }
diff --git a/src/app/shared/flows-list/flows-list.spec.ts b/src/app/shared/flows-list/flows-list.spec.ts index f175ff1..4055a01 100644 --- a/src/app/shared/flows-list/flows-list.spec.ts +++ b/src/app/shared/flows-list/flows-list.spec.ts @@ -11,6 +11,7 @@ import { of, throwError } from 'rxjs'; import { ListState } from '@stores/list-state'; import { vi } from 'vitest'; +import { FLOW_FINALIZATION_ENABLED } from '@shared/feature-flags'; import { FlowsList } from './flows-list'; function makeFlow(id: string, name: string, projectId?: string): Flow { @@ -258,4 +259,32 @@ describe('FlowsList', () => { expect(component.activeFilterCount()).toBe(2); }); + + it('offers the Finalized filter only while finalization is enabled', async () => { + const fixture = await build([makeFlow('1', 'Alpha')], []); + fixture.componentInstance.toggleFilters(); + fixture.detectChanges(); + + const values = Array.from( + fixture.nativeElement.querySelectorAll('mat-button-toggle') + ).map((toggle: any) => toggle.textContent.trim()); + + expect(values).toContain('All'); + expect(values.includes('Finalized')).toBe(FLOW_FINALIZATION_ENABLED); + }); + + it('does not keep narrowing the list with a filter whose control is hidden', async () => { + // A persisted Finalized filter would otherwise hide flows with nothing on screen to clear it. + const fixture = await build([makeFlow('1', 'Alpha')], []); + const view = fixture.componentInstance.view; + view.filter = 'FINALIZED'; + + TestBed.resetTestingModule(); + const reopened = await build([makeFlow('1', 'Alpha')], []); + + if (!FLOW_FINALIZATION_ENABLED) { + expect(reopened.componentInstance.filter()).toBe('all'); + expect(reopened.componentInstance.orderedFlows()).toHaveLength(1); + } + }); }); diff --git a/src/app/shared/flows-list/flows-list.ts b/src/app/shared/flows-list/flows-list.ts index 8a940f9..55a1256 100644 --- a/src/app/shared/flows-list/flows-list.ts +++ b/src/app/shared/flows-list/flows-list.ts @@ -20,7 +20,7 @@ import { NotificationService } from '@services/notifications/notification'; import { TaskExecutionsService } from '@services/task-executions/task-executions'; import { ProjectsService } from '@services/projects/projects'; import { EditorStateHolder } from '@stores/flow-editor'; -import { PROJECTS_ENABLED } from '@shared/feature-flags'; +import { FLOW_FINALIZATION_ENABLED, PROJECTS_ENABLED } from '@shared/feature-flags'; import { OrderEvent, OrderField, Ordering, orderDirType } from "@shared/ordering/ordering"; import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder'; import { FlowsGroup } from './flows-group/flows-group'; @@ -63,6 +63,7 @@ export class FlowsList extends ListStateViewHolder { private editorState = inject(EditorStateHolder); readonly projectsEnabled = PROJECTS_ENABLED; + readonly finalizationEnabled = FLOW_FINALIZATION_ENABLED; readonly ungroupedKey = UNGROUPED_PROJECT_KEY; readonly projects = this.projectsService.projects; @@ -98,6 +99,14 @@ export class FlowsList extends ListStateViewHolder { this.filtersOpen.update((open) => !open); } + /** + * A filter whose control is hidden would keep narrowing the list with no way to see or clear it, + * so a persisted Finalized filter falls back to showing everything. + */ + private usableFilter(filter: FlowFilter): FlowFilter { + return filter === 'FINALIZED' && !FLOW_FINALIZATION_ENABLED ? 'all' : (filter || 'all'); + } + constructor() { super('flowsList', {defaultOrder: { orderBy: 'name', orderDir: 'asc' } as OrderViewState, defaultFilter: 'all'}); effect(() => { @@ -125,7 +134,7 @@ export class FlowsList extends ListStateViewHolder { this.flows = existingState.list; this.loading.set(false); if (existingState.filter) - this.filter.set(existingState.filter as FlowFilter || 'all'); + this.filter.set(this.usableFilter(existingState.filter as FlowFilter)); return; } diff --git a/src/app/shared/title-toolbar/title-toolbar.html b/src/app/shared/title-toolbar/title-toolbar.html index 7b6ed4d..2ca8c15 100644 --- a/src/app/shared/title-toolbar/title-toolbar.html +++ b/src/app/shared/title-toolbar/title-toolbar.html @@ -95,6 +95,7 @@ Published + @if (finalizationEnabled || flow()!.finalized) { Finalized + }
}