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) <noreply@anthropic.com>
This commit is contained in:
parent
4c973b3922
commit
4fa449658e
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@
|
|||
<mat-button-toggle value="all">All</mat-button-toggle>
|
||||
<mat-button-toggle value="PUBLIC">Public</mat-button-toggle>
|
||||
<mat-button-toggle value="PRIVATE">Private</mat-button-toggle>
|
||||
@if (finalizationEnabled) {
|
||||
<mat-button-toggle value="FINALIZED">Finalized</mat-button-toggle>
|
||||
}
|
||||
</mat-button-toggle-group>
|
||||
|
||||
<div class="flows-list-ordering">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Flow> {
|
|||
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<Flow> {
|
|||
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<Flow> {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
Published
|
||||
</mat-slide-toggle>
|
||||
|
||||
@if (finalizationEnabled || flow()!.finalized) {
|
||||
<mat-slide-toggle
|
||||
[checked]="!!flow()!.finalized"
|
||||
[disabled]="!!flow()!.finalized || finalizeSaving() || notSaved() || blockSyncInProgress()"
|
||||
|
|
@ -102,6 +103,7 @@
|
|||
(change)="onFinalizedToggle($event.checked)">
|
||||
Finalized
|
||||
</mat-slide-toggle>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { ProjectsService } from '@services/projects/projects';
|
|||
import { TaskExecutionsService } from '@services/task-executions/task-executions';
|
||||
import { take } from 'rxjs';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { PROJECTS_ENABLED, SWIMLANES_ENABLED } from '@shared/feature-flags';
|
||||
import { FLOW_FINALIZATION_ENABLED, PROJECTS_ENABLED, SWIMLANES_ENABLED } from '@shared/feature-flags';
|
||||
|
||||
@Component({
|
||||
selector: 'app-title-toolbar',
|
||||
|
|
@ -41,6 +41,7 @@ export class TitleToolbar {
|
|||
private projectsService = inject(ProjectsService);
|
||||
|
||||
readonly projectsEnabled = PROJECTS_ENABLED;
|
||||
readonly finalizationEnabled = FLOW_FINALIZATION_ENABLED;
|
||||
private authorization = inject(Authorization);
|
||||
private taskExecutionsService = inject(TaskExecutionsService);
|
||||
flow = computed(() => this.editorState.currentFlow());
|
||||
|
|
|
|||
Loading…
Reference in New Issue