feat: Add subflow export functionality with source name tracking
- Implement export button in subflow preview dialog - Add sourceName parameter to track flow origin - Add deriveSourceName() utility to normalize source names - Improve dialog layout with flexbox for action buttons - Fix confirm dialog z-index (10000) - Add exporting state signal for export button UI
This commit is contained in:
parent
3dbb140131
commit
4c4b57781f
|
|
@ -4,6 +4,7 @@ import { FlowData } from '@models/flow';
|
|||
type SubflowPreviewDialogState = {
|
||||
title: string;
|
||||
flowData: FlowData;
|
||||
sourceName: string;
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
|
@ -12,14 +13,29 @@ export class SubflowPreviewDialogService {
|
|||
|
||||
readonly state = this._state.asReadonly();
|
||||
|
||||
open(flowData: FlowData, title?: string) {
|
||||
open(flowData: FlowData, title?: string, sourceName?: string) {
|
||||
const normalizedSourceName = sourceName?.trim() || this.deriveSourceName(title);
|
||||
this._state.set({
|
||||
title: title?.trim() || 'Subflow Preview',
|
||||
flowData
|
||||
flowData,
|
||||
sourceName: normalizedSourceName
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this._state.set(null);
|
||||
}
|
||||
|
||||
private deriveSourceName(title?: string): string {
|
||||
const normalizedTitle = title?.trim();
|
||||
if (!normalizedTitle) return 'container';
|
||||
|
||||
const suffix = ' subflow';
|
||||
const lowerTitle = normalizedTitle.toLowerCase();
|
||||
if (lowerTitle.endsWith(suffix)) {
|
||||
return normalizedTitle.slice(0, normalizedTitle.length - suffix.length).trim() || 'container';
|
||||
}
|
||||
|
||||
return normalizedTitle;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
@if(!!this.state()) {
|
||||
|
||||
<div class="fixed inset-0 z-9999 flex items-center justify-center">
|
||||
<div class="fixed inset-0 flex items-center justify-center" style="z-index: 10000;">
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/40" (click)="close(false)">
|
||||
|
|
|
|||
|
|
@ -677,7 +677,7 @@ export class ContainerNodeComponent implements OnDestroy {
|
|||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (!this.subFlow) return;
|
||||
this.subflowPreview.open(this.subFlow, `${this.name} subflow`);
|
||||
this.subflowPreview.open(this.subFlow, `${this.name} subflow`, this.name);
|
||||
}
|
||||
|
||||
async openFieldPreview(field: ContainerFieldView, event?: Event) {
|
||||
|
|
|
|||
|
|
@ -448,7 +448,8 @@ export class TaskStepNodeComponent {
|
|||
event?.stopPropagation();
|
||||
const subFlow = this.subFlow();
|
||||
if (!subFlow) return;
|
||||
this.subflowPreview.open(subFlow, `${this.name || this.nodeTitle()} subflow`);
|
||||
const sourceName = this.name || this.nodeTitle();
|
||||
this.subflowPreview.open(subFlow, `${sourceName} subflow`, sourceName);
|
||||
}
|
||||
|
||||
openFieldPreview(field: DisplayField, event?: Event) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@
|
|||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.subflow-preview__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.subflow-preview__eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,13 @@
|
|||
<div class="subflow-preview__eyebrow">Read-only Subflow</div>
|
||||
<h3 class="subflow-preview__title">{{ state()!.title }}</h3>
|
||||
</div>
|
||||
<button type="button" mat-stroked-button (click)="close($event)">Close</button>
|
||||
<div class="subflow-preview__actions">
|
||||
<button type="button" mat-stroked-button (click)="exportSubflow($event)" [disabled]="exporting()">
|
||||
<mat-icon>file_upload</mat-icon>
|
||||
Export
|
||||
</button>
|
||||
<button type="button" mat-stroked-button (click)="close($event)">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subflow-preview__canvas">
|
||||
|
|
|
|||
|
|
@ -1,20 +1,32 @@
|
|||
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { ReteEditor } from '@shared/rete-editor/rete-editor';
|
||||
import { SubflowPreviewDialogService } from '@services/dialogs/subflow-preview-dialog';
|
||||
import { FlowsService } from '@services/flows/flows';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { FlowData } from '@models/flow';
|
||||
import { NotificationService } from '@services/notifications/notification';
|
||||
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subflow-preview-dialog-host',
|
||||
imports: [CommonModule, MatButtonModule, ReteEditor],
|
||||
imports: [CommonModule, MatButtonModule, MatIconModule, ReteEditor],
|
||||
templateUrl: './subflow-preview-dialog.html',
|
||||
styleUrl: './subflow-preview-dialog.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class SubflowPreviewDialogHostComponent {
|
||||
private dialog = inject(SubflowPreviewDialogService);
|
||||
private flowsService = inject(FlowsService);
|
||||
private editorState = inject(EditorStateHolder);
|
||||
private notification = inject(NotificationService);
|
||||
private confirm = inject(ConfirmDialogService);
|
||||
|
||||
readonly state = this.dialog.state;
|
||||
readonly exporting = signal(false);
|
||||
readonly flowId = computed(() => {
|
||||
const state = this.state();
|
||||
return state ? `subflow-preview:${state.title}` : 'subflow-preview';
|
||||
|
|
@ -25,4 +37,66 @@ export class SubflowPreviewDialogHostComponent {
|
|||
event?.stopPropagation();
|
||||
this.dialog.close();
|
||||
}
|
||||
|
||||
async exportSubflow(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (this.exporting()) return;
|
||||
|
||||
const state = this.state();
|
||||
if (!state) return;
|
||||
|
||||
this.exporting.set(true);
|
||||
try {
|
||||
const name = this.nextFlowName(`sub_${state.sourceName}`);
|
||||
const createdFlow = await firstValueFrom(this.flowsService.createFlow({
|
||||
name,
|
||||
description: `Extracted from ${state.sourceName}`,
|
||||
data: this.cloneFlowData(state.flowData),
|
||||
status: 'DRAFT'
|
||||
}));
|
||||
|
||||
this.notification.show(`Subflow added to flows list as ${name}.`, 'success');
|
||||
|
||||
const hasUnsavedChanges = this.editorState.hasFlow() && this.editorState.isDirty();
|
||||
const openNow = await this.confirm.open(this.buildExportMessage(name, hasUnsavedChanges));
|
||||
if (openNow) {
|
||||
await this.editorState.openDocument(createdFlow, { skipDirtyCheck: true });
|
||||
this.dialog.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Export subflow failed', error);
|
||||
this.notification.show('Unable to export subflow. Please retry.', 'error');
|
||||
} finally {
|
||||
this.exporting.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private nextFlowName(baseName: string): string {
|
||||
const sanitizedBase = baseName.trim() || 'sub_container';
|
||||
const existing = new Set(this.flowsService.flows().map((flow) => flow.name));
|
||||
if (!existing.has(sanitizedBase)) return sanitizedBase;
|
||||
|
||||
let index = 1;
|
||||
while (existing.has(`${sanitizedBase}(${index})`)) {
|
||||
index++;
|
||||
}
|
||||
return `${sanitizedBase}(${index})`;
|
||||
}
|
||||
|
||||
private cloneFlowData(flowData: FlowData): FlowData {
|
||||
if (typeof structuredClone === 'function') {
|
||||
return structuredClone(flowData);
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(flowData)) as FlowData;
|
||||
}
|
||||
|
||||
private buildExportMessage(flowName: string, hasUnsavedChanges: boolean): string {
|
||||
const unsavedWarning = hasUnsavedChanges
|
||||
? ' The current flow has unsaved changes: opening now will discard your pending edits.'
|
||||
: '';
|
||||
|
||||
return `Subflow added to the flows list as ${flowName}. Do you want to open it now?${unsavedWarning} Note: the container subflow is not updated automatically; once you finish and save this new flow, re-import it into the container.`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue