diff --git a/src/app/models/project.ts b/src/app/models/project.ts index 097f16c..7955039 100644 --- a/src/app/models/project.ts +++ b/src/app/models/project.ts @@ -67,29 +67,3 @@ export function normalizeSharedContext(raw: unknown): ProjectContext { export function projectTemplateReference(name: string): string { return `\${{project.${name}}}`; } - -/** - * Derived from the run's executions; the run keeps no state of its own. - * BLOCKED = the next step still needs inputs or credentials. STOPPED = a step failed. - */ -export type ProjectRunStatus = 'PENDING' | 'RUNNING' | 'BLOCKED' | 'STOPPED' | 'COMPLETED'; - -/** One run of a project: the executions started together, tied by `projectRunId`. */ -export type ProjectRun = { - projectRunId: string; - projectId: string; - name: string; - createdAt: number; - executionCount: number; - status: ProjectRunStatus; - currentExecutionId: string | null; - completedCount: number; - blockedReason: string | null; - executionIds: string[]; -}; - -/** What a project run created, and the flows it deliberately left out. */ -export type ProjectExecutionPlan = { - run: ProjectRun; - skipped: { flowId: string; flowName: string; reason: string }[]; -}; diff --git a/src/app/services/projects/projects-call.base.ts b/src/app/services/projects/projects-call.base.ts index 548fe1c..de0c472 100644 --- a/src/app/services/projects/projects-call.base.ts +++ b/src/app/services/projects/projects-call.base.ts @@ -17,9 +17,6 @@ export abstract class ProjectsCallServiceBase { abstract updateSharedContext(projectId: string, context: ProjectContext): Observable; - /** Sets the order a project run executes the flows in. */ - abstract updateFlowOrder(projectId: string, flowIds: string[]): Observable; - /** * Deletes the project and every flow in it, finalized flows included. Without `confirm` the * backend refuses a non-empty project with 409 rather than destroying anything. diff --git a/src/app/services/projects/projects-call.fake.ts b/src/app/services/projects/projects-call.fake.ts index 533b7ee..80d7dc8 100644 --- a/src/app/services/projects/projects-call.fake.ts +++ b/src/app/services/projects/projects-call.fake.ts @@ -105,10 +105,6 @@ export class ProjectsCallServiceFake extends ProjectsCallServiceBase { }); } - override updateFlowOrder(projectId: string, _flowIds: string[]): Observable { - return defer(() => of(this.requireProject(projectId))); - } - override deleteProject(projectId: string, confirm: boolean): Observable { return defer(() => { this.requireProject(projectId); diff --git a/src/app/services/projects/projects-call.ts b/src/app/services/projects/projects-call.ts index 244a8a5..a1aa355 100644 --- a/src/app/services/projects/projects-call.ts +++ b/src/app/services/projects/projects-call.ts @@ -46,12 +46,6 @@ export class ProjectsCallService extends ProjectsCallServiceBase { .pipe(map((raw) => projectFromApi(raw))); } - override updateFlowOrder(projectId: string, flowIds: string[]): Observable { - return this.http - .put(`${environment.apiUrl}/projects/${encodeURIComponent(projectId)}/flow-order`, { flowIds }) - .pipe(map((raw) => projectFromApi(raw))); - } - override deleteProject(projectId: string, confirm: boolean): Observable { return this.http.delete( `${environment.apiUrl}/projects/${encodeURIComponent(projectId)}?confirm=${confirm}` diff --git a/src/app/services/projects/projects.ts b/src/app/services/projects/projects.ts index a641dc8..8bed9bf 100644 --- a/src/app/services/projects/projects.ts +++ b/src/app/services/projects/projects.ts @@ -115,16 +115,6 @@ export class ProjectsService { ); } - updateFlowOrder(projectId: string, flowIds: string[]) { - return this.projectsCallService.updateFlowOrder(projectId, flowIds).pipe( - tap((updated) => this.patch(updated)), - catchError((err) => { - console.error('Update project flow order failed', err); - return throwError(() => err); - }) - ); - } - /** * Deletes the project and every flow in it. The caller must also refresh the flow cache, which * this cascade leaves stale. diff --git a/src/app/services/task-executions/task-executions-call.base.ts b/src/app/services/task-executions/task-executions-call.base.ts index 5b2d58d..8e6cfff 100644 --- a/src/app/services/task-executions/task-executions-call.base.ts +++ b/src/app/services/task-executions/task-executions-call.base.ts @@ -10,7 +10,6 @@ import { BiasRerunRequest } from '@models/bias-impact'; import { ExecutionEventLogEntry, TaskExecution, TaskExecutionGroup } from '@models/task-execution'; -import { ProjectExecutionPlan, ProjectRun } from '@models/project'; import { Observable } from 'rxjs'; export abstract class TaskExecutionsCallServiceBase { @@ -20,16 +19,6 @@ export abstract class TaskExecutionsCallServiceBase { abstract retrieveStepIterations(executionId: string, stepId: string): Observable; abstract retrieveExecutionEvents(executionId: string): Observable; abstract createTaskExecution(flowId: string): Observable; - /** - * Runs a whole project: creates one execution per flow, all sharing a projectRunId. It does not - * start them - the client still supplies inputs and credentials, exactly as for a single flow. - */ - abstract createProjectExecutions(projectId: string, skipNonExecutable: boolean): Observable; - /** - * Starts, or resumes, a project run. The flows run one at a time in the project's order; each - * step starts only when the previous one succeeded. - */ - abstract startProjectRun(projectId: string, projectRunId: string): Observable; abstract rerunTaskExecution(executionId: string): Observable; abstract runBiasImpactExperiment( executionId: string, diff --git a/src/app/services/task-executions/task-executions-call.fake.ts b/src/app/services/task-executions/task-executions-call.fake.ts index 0fe7df7..7eb5ba7 100644 --- a/src/app/services/task-executions/task-executions-call.fake.ts +++ b/src/app/services/task-executions/task-executions-call.fake.ts @@ -11,7 +11,6 @@ import { BiasRerunRequest } from '@models/bias-impact'; import { ExecutionEventLogEntry, TaskExecution, TaskExecutionGroup } from '@models/task-execution'; -import { ProjectExecutionPlan, ProjectRun } from '@models/project'; import { map, Observable, of, throwError } from 'rxjs'; import { TaskExecutionsCallServiceBase } from './task-executions-call.base'; @@ -526,75 +525,6 @@ export class TaskExecutionsCallServiceFake extends TaskExecutionsCallServiceBase return of(this.withSimulationAvailability(execution)); } - override createProjectExecutions(projectId: string, _skipNonExecutable: boolean): Observable { - // The dev fake seeds flows '1' and '2' into project 'p1'; one execution per flow, sharing a run id. - const projectRunId = crypto.randomUUID(); - const flowIds = projectId === 'p1' ? ['1'] : projectId === 'p2' ? ['2'] : []; - const now = Date.now(); - - const executionIds = flowIds.map((flowId) => { - const execution: TaskExecution = { - id: crypto.randomUUID(), - name: `${projectId} - ${flowId}`, - creationTime: now, - flowId, - sourceFlowId: flowId, - projectId, - projectRunId, - runNumber: this.nextRunNumber(flowId), - simulationAvailable: false, - context: { - inputs: {}, - result: {}, - startTime: null, - endTime: null, - errors: {}, - warnings: {}, - steps: {}, - status: 'CREATED', - waitingSteps: [] - } - }; - this.data.unshift(execution); - return execution.id; - }); - - return of({ - run: { - projectRunId, - projectId, - name: projectId, - createdAt: now, - executionCount: executionIds.length, - status: 'PENDING' as const, - currentExecutionId: executionIds[0] ?? null, - completedCount: 0, - blockedReason: null, - executionIds - }, - skipped: [] - }); - } - - override startProjectRun(projectId: string, projectRunId: string): Observable { - const executionIds = this.data - .filter((execution) => execution.projectRunId === projectRunId) - .map((execution) => execution.id); - - return of({ - projectRunId, - projectId, - name: projectId, - createdAt: Date.now(), - executionCount: executionIds.length, - status: 'RUNNING' as const, - currentExecutionId: executionIds[0] ?? null, - completedCount: 0, - blockedReason: null, - executionIds - }); - } - override rerunTaskExecution(executionId: string): Observable { const source = this.findExecution(executionId); const sourceFlowId = this.executionSourceFlowId(source); diff --git a/src/app/services/task-executions/task-executions-call.ts b/src/app/services/task-executions/task-executions-call.ts index a831dfa..3bc0510 100644 --- a/src/app/services/task-executions/task-executions-call.ts +++ b/src/app/services/task-executions/task-executions-call.ts @@ -28,7 +28,6 @@ import { BiasValueImpact } from '@models/bias-impact'; import { ExecutionEventLogEntry, TaskExecution, TaskExecutionGroup, normalizeExecutionOutcomes } from '@models/task-execution'; -import { ProjectExecutionPlan, ProjectRun } from '@models/project'; import { map, Observable } from 'rxjs'; import { TaskExecutionsCallServiceBase } from './task-executions-call.base'; @@ -70,58 +69,6 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase { ); } - override createProjectExecutions(projectId: string, skipNonExecutable: boolean): Observable { - return this.http - .post( - `${environment.apiUrl}/projects/${encodeURIComponent(projectId)}/execute?skipNonExecutable=${skipNonExecutable}`, - {} - ) - .pipe(map((raw) => this.mapProjectExecutionPlan(raw))); - } - - override startProjectRun(projectId: string, projectRunId: string): Observable { - return this.http - .post( - `${environment.apiUrl}/projects/${encodeURIComponent(projectId)}` - + `/runs/${encodeURIComponent(projectRunId)}/start`, - {} - ) - .pipe(map((raw) => this.mapProjectRun(raw))); - } - - private mapProjectRun(raw: unknown): ProjectRun { - const run = (raw ?? {}) as Record; - const executions = Array.isArray(run['executions']) ? run['executions'] : []; - - return { - projectRunId: String(run['projectRunId'] ?? ''), - projectId: String(run['projectId'] ?? ''), - name: String(run['name'] ?? ''), - createdAt: typeof run['createdAt'] === 'number' ? run['createdAt'] : Date.now(), - executionCount: typeof run['executionCount'] === 'number' ? run['executionCount'] : executions.length, - status: (run['status'] ?? 'PENDING') as ProjectRun['status'], - currentExecutionId: typeof run['currentExecutionId'] === 'string' ? run['currentExecutionId'] : null, - completedCount: typeof run['completedCount'] === 'number' ? run['completedCount'] : 0, - blockedReason: typeof run['blockedReason'] === 'string' ? run['blockedReason'] : null, - executionIds: executions - .map((execution: Record) => String(execution?.['id'] ?? '')) - .filter((id: string) => id.length > 0) - }; - } - - private mapProjectExecutionPlan(raw: unknown): ProjectExecutionPlan { - const value = (raw ?? {}) as Record; - - return { - run: this.mapProjectRun(value['run']), - skipped: (Array.isArray(value['skipped']) ? value['skipped'] : []).map((entry: Record) => ({ - flowId: String(entry?.['flowId'] ?? ''), - flowName: String(entry?.['flowName'] ?? ''), - reason: String(entry?.['reason'] ?? 'Flow is not executable') - })) - }; - } - override rerunTaskExecution(executionId: string): Observable { return this.http.post(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/rerun`, null).pipe( map((raw) => this.mapExecution(raw)) diff --git a/src/app/services/task-executions/task-executions.ts b/src/app/services/task-executions/task-executions.ts index ca83a68..5cda7bd 100644 --- a/src/app/services/task-executions/task-executions.ts +++ b/src/app/services/task-executions/task-executions.ts @@ -130,33 +130,6 @@ export class TaskExecutionsService { ); } - /** - * Runs a whole project. Mirrors createExecution so the pending flag and the /tasks refresh come - * for free. - */ - createProjectExecutions(projectId: string, skipNonExecutable = false) { - this._pendingExecutionCreation.set(true); - return this.taskExecutionsCallService.createProjectExecutions(projectId, skipNonExecutable).pipe( - finalize(() => this._pendingExecutionCreation.set(false)), - tap(() => this.refresh()), - catchError((err) => { - console.error('Create project executions failed', err); - return throwError(() => err); - }) - ); - } - - /** Starts, or resumes, a project run. */ - startProjectRun(projectId: string, projectRunId: string) { - return this.taskExecutionsCallService.startProjectRun(projectId, projectRunId).pipe( - tap(() => this.refresh()), - catchError((err) => { - console.error('Start project run failed', err); - return throwError(() => err); - }) - ); - } - rerunExecution(executionId: string) { this._pendingExecutionCreation.set(true); return this.taskExecutionsCallService.rerunTaskExecution(executionId).pipe( diff --git a/src/app/shared/flows-list/flows-group/flows-group.css b/src/app/shared/flows-list/flows-group/flows-group.css index 808c1d7..0c97dde 100644 --- a/src/app/shared/flows-list/flows-group/flows-group.css +++ b/src/app/shared/flows-list/flows-group/flows-group.css @@ -88,27 +88,6 @@ min-width: 0; } -/* Compact arrows: the sidebar is 320px, so they must not crowd out the flow card. */ -.flows-list-group-move { - display: flex; - flex: 0 0 auto; - flex-direction: column; -} - -.flows-list-group-move button { - width: 22px; - height: 18px; - line-height: 18px; - padding: 0; -} - -.flows-list-group-move mat-icon { - font-size: 16px; - width: 16px; - height: 16px; - line-height: 16px; -} - .flows-list-group-empty { padding: 0.25rem; color: #94a3b8; diff --git a/src/app/shared/flows-list/flows-group/flows-group.html b/src/app/shared/flows-list/flows-group/flows-group.html index 0d754a9..47bbdb1 100644 --- a/src/app/shared/flows-list/flows-group/flows-group.html +++ b/src/app/shared/flows-list/flows-group/flows-group.html @@ -24,15 +24,6 @@ @if (!isUngrouped()) { - - - - - } } diff --git a/src/app/shared/flows-list/flows-group/flows-group.ts b/src/app/shared/flows-list/flows-group/flows-group.ts index 5b6959e..713da99 100644 --- a/src/app/shared/flows-list/flows-group/flows-group.ts +++ b/src/app/shared/flows-list/flows-group/flows-group.ts @@ -37,35 +37,16 @@ export class FlowsGroup { readonly contextRequested = output(); readonly deleteRequested = output(); readonly newFlowRequested = output(); - readonly runRequested = output(); - readonly moveRequested = output<{ project: Project; flowId: string; direction: -1 | 1 }>(); /** The ungrouped bucket is a pseudo-project: it has no actions of its own. */ readonly title = computed(() => this.project()?.name ?? 'No project'); readonly isUngrouped = computed(() => this.project() === null); - /** A project run needs at least one executable flow, and the backend refuses the rest. */ - readonly canRun = computed(() => this.flows().some((flow) => flow.status === 'EXECUTABLE')); - - readonly runTooltip = computed(() => - this.canRun() ? 'Run every executable flow in this project' : 'No executable flow in this project' - ); readonly countLabel = computed(() => { const count = this.flows().length; return `${count} ${count === 1 ? 'flow' : 'flows'}`; }); - /** Reordering is per-project, so it is offered only inside a real project group. */ - readonly canReorder = computed(() => !this.isUngrouped() && this.flows().length > 1); - - move(flowId: string, direction: -1 | 1, event: Event) { - event.stopPropagation(); - const project = this.project(); - if (project) { - this.moveRequested.emit({ project, flowId, direction }); - } - } - onHeaderClick(event: Event) { event.stopPropagation(); this.toggled.emit(); diff --git a/src/app/shared/flows-list/flows-list.html b/src/app/shared/flows-list/flows-list.html index d626710..0d08264 100644 --- a/src/app/shared/flows-list/flows-list.html +++ b/src/app/shared/flows-list/flows-list.html @@ -75,9 +75,7 @@ (editRequested)="editProject($event)" (contextRequested)="editProjectContext($event)" (deleteRequested)="deleteProject($event)" - (newFlowRequested)="createFlowInProject($event)" - (runRequested)="runProject($event)" - (moveRequested)="moveFlowInProject($event)"> + (newFlowRequested)="createFlowInProject($event)"> } diff --git a/src/app/shared/flows-list/flows-list.spec.ts b/src/app/shared/flows-list/flows-list.spec.ts index c12e955..4cc5026 100644 --- a/src/app/shared/flows-list/flows-list.spec.ts +++ b/src/app/shared/flows-list/flows-list.spec.ts @@ -4,14 +4,11 @@ import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; import { provideRouter } from '@angular/router'; import { Flow } from '@models/flow'; import { Project } from '@models/project'; import { FlowsService } from '@services/flows/flows'; import { ProjectsService } from '@services/projects/projects'; -import { TaskExecutionsService } from '@services/task-executions/task-executions'; -import { of, throwError } from 'rxjs'; import { ListState } from '@stores/list-state'; import { vi } from 'vitest'; @@ -43,20 +40,7 @@ function makeProject(id: string, name: string): Project { }; } -let taskExecutionsStub: { - createProjectExecutions: ReturnType; - startProjectRun: ReturnType; -}; - async function build(flows: Flow[], projects: Project[]) { - taskExecutionsStub = { - createProjectExecutions: vi.fn(), - startProjectRun: vi.fn().mockReturnValue(of({ - projectRunId: 'r1', projectId: 'p1', name: 'Recruiting', createdAt: 1, - executionCount: 1, status: 'RUNNING', currentExecutionId: 'e1', - completedCount: 0, blockedReason: null, executionIds: ['e1'] - })) - }; await TestBed.configureTestingModule({ imports: [FlowsList], providers: [ @@ -78,8 +62,7 @@ async function build(flows: Flow[], projects: Project[]) { hasLoadedProjects: vi.fn().mockReturnValue(true), getAllProjects: vi.fn().mockResolvedValue(signal(projects)) } - }, - { provide: TaskExecutionsService, useValue: taskExecutionsStub } + } ] }).compileComponents(); @@ -177,67 +160,6 @@ describe('FlowsList', () => { expect(fixture.componentInstance.view.expandedGroupIds?.has('p1')).toBe(true); }); - it('runs a project and navigates to the created executions', async () => { - const fixture = await build([makeFlow('1', 'Alpha', 'p1')], [makeProject('p1', 'Recruiting')]); - taskExecutionsStub.createProjectExecutions.mockReturnValue(of({ - run: { projectRunId: 'r1', projectId: 'p1', name: 'Recruiting', createdAt: 1, executionCount: 1, executionIds: ['e1'] }, - skipped: [] - })); - const router = TestBed.inject(Router); - const navigate = vi.spyOn(router, 'navigate').mockResolvedValue(true); - - await fixture.componentInstance.runProject(makeProject('p1', 'Recruiting')); - - // Non-executable flows are skipped rather than failing the whole run from the UI. - expect(taskExecutionsStub.createProjectExecutions).toHaveBeenCalledWith('p1', true); - // Creating is not enough: the run must also be started, or nothing would happen. - expect(taskExecutionsStub.startProjectRun).toHaveBeenCalledWith('p1', 'r1'); - expect(navigate).toHaveBeenCalledWith(['/tasks'], { queryParams: { executionId: 'e1' } }); - }); - - it('still navigates when a project run produced no executions', async () => { - const fixture = await build([makeFlow('1', 'Alpha', 'p1')], [makeProject('p1', 'Recruiting')]); - taskExecutionsStub.createProjectExecutions.mockReturnValue(of({ - run: { projectRunId: 'r1', projectId: 'p1', name: 'Recruiting', createdAt: 1, executionCount: 0, executionIds: [] }, - skipped: [{ flowId: '1', flowName: 'Alpha', reason: 'Flow is not executable' }] - })); - const navigate = vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true); - - await fixture.componentInstance.runProject(makeProject('p1', 'Recruiting')); - - expect(navigate).toHaveBeenCalledWith(['/tasks'], { queryParams: {} }); - }); - - it('does not navigate when the project run fails', async () => { - const fixture = await build([makeFlow('1', 'Alpha', 'p1')], [makeProject('p1', 'Recruiting')]); - taskExecutionsStub.createProjectExecutions.mockReturnValue(throwError(() => new Error('boom'))); - const navigate = vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true); - - await fixture.componentInstance.runProject(makeProject('p1', 'Recruiting')); - - expect(navigate).not.toHaveBeenCalled(); - }); - - it('reports a blocked run instead of claiming it is running', async () => { - const fixture = await build([makeFlow('1', 'Alpha', 'p1')], [makeProject('p1', 'Recruiting')]); - taskExecutionsStub.createProjectExecutions.mockReturnValue(of({ - run: { projectRunId: 'r1', projectId: 'p1', name: 'Recruiting', createdAt: 1, executionCount: 1, - status: 'PENDING', currentExecutionId: 'e1', completedCount: 0, blockedReason: null, executionIds: ['e1'] }, - skipped: [] - })); - taskExecutionsStub.startProjectRun.mockReturnValue(of({ - projectRunId: 'r1', projectId: 'p1', name: 'Recruiting', createdAt: 1, executionCount: 1, - status: 'BLOCKED', currentExecutionId: 'e1', completedCount: 0, - blockedReason: 'missing global inputs: requirements', executionIds: ['e1'] - })); - const navigate = vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true); - - await fixture.componentInstance.runProject(makeProject('p1', 'Recruiting')); - - // Still navigates, so the user can supply what the run is waiting for. - expect(navigate).toHaveBeenCalled(); - }); - it('keeps the filters collapsed by default, so the flows are what you see first', async () => { const fixture = await build([makeFlow('1', 'Alpha')], []); diff --git a/src/app/shared/flows-list/flows-list.ts b/src/app/shared/flows-list/flows-list.ts index 839e038..478e028 100644 --- a/src/app/shared/flows-list/flows-list.ts +++ b/src/app/shared/flows-list/flows-list.ts @@ -3,7 +3,6 @@ // Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM. import { ChangeDetectionStrategy, Component, computed, effect, inject, model, signal, Signal, WritableSignal } from '@angular/core'; -import { Router } from '@angular/router'; import { Flow, FlowVisibility } from '@models/flow'; import { firstValueFrom } from 'rxjs'; import { FlowsService } from '@services/flows/flows'; @@ -21,7 +20,6 @@ import { ProjectContextDialogService } from '@services/dialogs/project-context-d import { ProjectDeleteDialogService } from '@services/dialogs/project-delete-dialog'; import { ProjectDialogService } from '@services/dialogs/project-dialog'; 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 { FLOW_FINALIZATION_ENABLED, PROJECTS_ENABLED } from '@shared/feature-flags'; @@ -62,8 +60,6 @@ export class FlowsList extends ListStateViewHolder { private projectDeleteDialog = inject(ProjectDeleteDialogService); private projectContextDialog = inject(ProjectContextDialogService); private notifications = inject(NotificationService); - private taskExecutions = inject(TaskExecutionsService); - private router = inject(Router); private editorState = inject(EditorStateHolder); readonly projectsEnabled = PROJECTS_ENABLED; @@ -299,55 +295,6 @@ export class FlowsList extends ListStateViewHolder { } } - /** - * Creates one execution per executable flow in the project and shows them in /tasks. They are - * created, not started: each still needs its inputs and credentials, exactly as a single run does. - */ - async runProject(project: Project) { - try { - const plan = await firstValueFrom( - this.taskExecutions.createProjectExecutions(project.id, true)); - - // The flows run one at a time in the project's order; each step starts only when the - // previous one succeeded, so this kicks off the first and the backend advances the rest. - const run = await firstValueFrom( - this.taskExecutions.startProjectRun(project.id, plan.run.projectRunId)); - - const skipped = plan.skipped.length; - const skippedNote = skipped > 0 ? `; ${skipped} flow(s) skipped as not executable` : ''; - this.notifications.show( - run.status === 'BLOCKED' - ? `“${project.name}” is waiting: ${run.blockedReason ?? 'the first flow needs inputs'}` - : `Running “${project.name}” — ${plan.run.executionCount} flow(s), one at a time${skippedNote}`, - run.status === 'BLOCKED' ? 'info' : 'success'); - - await this.router.navigate(['/tasks'], { - queryParams: plan.run.executionIds.length ? { executionId: plan.run.executionIds[0] } : {} - }); - } catch { - this.notifications.show('Could not run the project.', 'error'); - } - } - - /** Moves a flow one position within its project, persisting the run order. */ - async moveFlowInProject(event: { project: Project; flowId: string; direction: -1 | 1 }) { - const current = this.groupedFlows().find((group) => group.key === event.project.id)?.flows ?? []; - const index = current.findIndex((flow) => flow.id === event.flowId); - const target = index + event.direction; - if (index < 0 || target < 0 || target >= current.length) return; - - const reordered = [...current]; - [reordered[index], reordered[target]] = [reordered[target], reordered[index]]; - - try { - await firstValueFrom(this.projectsService.updateFlowOrder( - event.project.id, reordered.map((flow) => flow.id))); - await this.flowsService.refresh(true); - } catch { - this.notifications.show('Could not reorder the flows.', 'error'); - } - } - async createFlowInProject(project: Project) { try { const created = await firstValueFrom(this.flowsService.createNewFlow());