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 @@
AllPublicPrivate
+ @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
+ }