feat: node creation and serialization logic in workflow editor
This commit is contained in:
@@ -2,7 +2,11 @@ import { showAlert } from '../ui.js';
|
|||||||
import { tesseractLanguages } from '../config/tesseract-languages.js';
|
import { tesseractLanguages } from '../config/tesseract-languages.js';
|
||||||
import { createWorkflowEditor, updateNodeDisplay } from '../workflow/editor';
|
import { createWorkflowEditor, updateNodeDisplay } from '../workflow/editor';
|
||||||
import { executeWorkflow } from '../workflow/engine';
|
import { executeWorkflow } from '../workflow/engine';
|
||||||
import { nodeRegistry, getNodesByCategory } from '../workflow/nodes/registry';
|
import {
|
||||||
|
nodeRegistry,
|
||||||
|
getNodesByCategory,
|
||||||
|
createNodeByType,
|
||||||
|
} from '../workflow/nodes/registry';
|
||||||
import type { BaseWorkflowNode } from '../workflow/nodes/base-node';
|
import type { BaseWorkflowNode } from '../workflow/nodes/base-node';
|
||||||
import type { WorkflowEditor } from '../workflow/editor';
|
import type { WorkflowEditor } from '../workflow/editor';
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +47,7 @@ import {
|
|||||||
|
|
||||||
let workflowEditor: WorkflowEditor | null = null;
|
let workflowEditor: WorkflowEditor | null = null;
|
||||||
let selectedNodeId: string | null = null;
|
let selectedNodeId: string | null = null;
|
||||||
|
let deleteNodeHandler: EventListener | null = null;
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', initializePage);
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
@@ -245,10 +250,14 @@ async function initializePage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('wf-delete-node', ((e: CustomEvent) => {
|
if (deleteNodeHandler) {
|
||||||
|
document.removeEventListener('wf-delete-node', deleteNodeHandler);
|
||||||
|
}
|
||||||
|
deleteNodeHandler = ((e: CustomEvent) => {
|
||||||
const nodeId = e.detail?.nodeId;
|
const nodeId = e.detail?.nodeId;
|
||||||
if (nodeId) deleteNodeById(nodeId);
|
if (nodeId) deleteNodeById(nodeId);
|
||||||
}) as EventListener);
|
}) as EventListener;
|
||||||
|
document.addEventListener('wf-delete-node', deleteNodeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNodeById(nodeId: string) {
|
async function deleteNodeById(nodeId: string) {
|
||||||
@@ -539,14 +548,12 @@ async function addNodeToCanvas(
|
|||||||
if (!workflowEditor) return;
|
if (!workflowEditor) return;
|
||||||
const { editor, area } = workflowEditor;
|
const { editor, area } = workflowEditor;
|
||||||
|
|
||||||
const entry = nodeRegistry[type];
|
|
||||||
if (!entry) {
|
|
||||||
console.error('Node type not found in registry:', type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const node = entry.factory();
|
const node = createNodeByType(type);
|
||||||
|
if (!node) {
|
||||||
|
console.error('Node type not found in registry:', type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
await editor.addNode(node);
|
await editor.addNode(node);
|
||||||
|
|
||||||
const pos = position || getCanvasCenter(area);
|
const pos = position || getCanvasCenter(area);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { LitPlugin, Presets as LitPresets } from '@retejs/lit-plugin';
|
|||||||
import type { ClassicScheme, LitArea2D } from '@retejs/lit-plugin';
|
import type { ClassicScheme, LitArea2D } from '@retejs/lit-plugin';
|
||||||
import { DataflowEngine } from 'rete-engine';
|
import { DataflowEngine } from 'rete-engine';
|
||||||
import type { DataflowEngineScheme } from 'rete-engine';
|
import type { DataflowEngineScheme } from 'rete-engine';
|
||||||
import { html } from 'lit';
|
import { LitElement, html } from 'lit';
|
||||||
import type { BaseWorkflowNode } from './nodes/base-node';
|
import type { BaseWorkflowNode } from './nodes/base-node';
|
||||||
// @ts-ignore -- Vite ?inline import for injecting into Shadow DOM
|
// @ts-ignore -- Vite ?inline import for injecting into Shadow DOM
|
||||||
import phosphorCSS from '@phosphor-icons/web/regular?inline';
|
import phosphorCSS from '@phosphor-icons/web/regular?inline';
|
||||||
@@ -49,6 +49,175 @@ function getStatusInfo(status: string, connected: boolean) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WorkflowNodeElement extends LitElement {
|
||||||
|
static properties = {
|
||||||
|
data: { attribute: false },
|
||||||
|
emit: { attribute: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
declare data: BaseWorkflowNode | undefined;
|
||||||
|
declare emit: ((data: unknown) => void) | undefined;
|
||||||
|
|
||||||
|
createRenderRoot(): HTMLElement | ShadowRoot {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.data) return html``;
|
||||||
|
const node = this.data;
|
||||||
|
const inputs = Object.entries(node.inputs || {});
|
||||||
|
const outputs = Object.entries(node.outputs || {});
|
||||||
|
const color = categoryColors[node.category] || '#6b7280';
|
||||||
|
const emitFn = this.emit;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
position: relative; display: flex; flex-direction: column;
|
||||||
|
align-items: center; width: 280px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
${inputs.length > 0
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
style="display: flex; justify-content: center; gap: 8px; position: relative; z-index: 1; margin-bottom: -7px;"
|
||||||
|
>
|
||||||
|
${inputs.map(([key, input]) =>
|
||||||
|
input
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
style="display: flex; align-items: center; justify-content: center;"
|
||||||
|
>
|
||||||
|
<rete-ref
|
||||||
|
.data=${{
|
||||||
|
type: 'socket',
|
||||||
|
side: 'input',
|
||||||
|
key,
|
||||||
|
nodeId: node.id,
|
||||||
|
payload: input.socket,
|
||||||
|
}}
|
||||||
|
.emit=${emitFn}
|
||||||
|
></rete-ref>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: null}
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
background: #1f2937; border: 1px solid #374151;
|
||||||
|
border-radius: 12px; width: 100%; overflow: hidden;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="height: 3px; border-radius: 10px 10px 0 0; overflow: hidden;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-wf="bar"
|
||||||
|
style="
|
||||||
|
height: 100%; width: 100%;
|
||||||
|
background: #6b7280; opacity: 0.25;
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="padding: 6px 14px; display: flex; align-items: center; gap: 6px;"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-wf="dot"
|
||||||
|
style="
|
||||||
|
width: 7px; height: 7px; border-radius: 50%; background: #6b7280; flex-shrink: 0;
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
data-wf="label"
|
||||||
|
style="font-size: 10px; color: #6b7280; font-weight: 500; flex: 1;"
|
||||||
|
>Not connected</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-wf-delete="${node.id}"
|
||||||
|
style="
|
||||||
|
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 18px; height: 18px; border-radius: 4px;
|
||||||
|
color: #6b7280; transition: all 0.15s;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="height: 1px; background: #374151; margin: 0 14px;"></div>
|
||||||
|
<div
|
||||||
|
style="padding: 10px 14px 12px; display: flex; align-items: flex-start; gap: 10px;"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="ph ${node.icon}"
|
||||||
|
style="font-size: 18px; color: ${color}; flex-shrink: 0; margin-top: 1px; line-height: 1;"
|
||||||
|
></i>
|
||||||
|
<div style="flex: 1; min-width: 0;">
|
||||||
|
<div
|
||||||
|
style="font-size: 13px; font-weight: 600; color: #f3f4f6; line-height: 1.3;"
|
||||||
|
>
|
||||||
|
${node.label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="font-size: 11px; color: #9ca3af; margin-top: 2px; line-height: 1.3;"
|
||||||
|
>
|
||||||
|
${node.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${outputs.length > 0
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
style="display: flex; justify-content: center; gap: 8px; position: relative; z-index: 1; margin-top: -7px;"
|
||||||
|
>
|
||||||
|
${outputs.map(([key, output]) =>
|
||||||
|
output
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
style="display: flex; align-items: center; justify-content: center;"
|
||||||
|
>
|
||||||
|
<rete-ref
|
||||||
|
.data=${{
|
||||||
|
type: 'socket',
|
||||||
|
side: 'output',
|
||||||
|
key,
|
||||||
|
nodeId: node.id,
|
||||||
|
payload: output.socket,
|
||||||
|
}}
|
||||||
|
.emit=${emitFn}
|
||||||
|
></rete-ref>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customElements.get('wf-node')) {
|
||||||
|
customElements.define('wf-node', WorkflowNodeElement);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateNodeDisplay(
|
export function updateNodeDisplay(
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
editor: NodeEditor<ClassicScheme>,
|
editor: NodeEditor<ClassicScheme>,
|
||||||
@@ -111,154 +280,10 @@ export async function createWorkflowEditor(
|
|||||||
customize: {
|
customize: {
|
||||||
node(data) {
|
node(data) {
|
||||||
return ({ emit }: { emit: (data: unknown) => void }) => {
|
return ({ emit }: { emit: (data: unknown) => void }) => {
|
||||||
const node = data.payload as BaseWorkflowNode;
|
return html`<wf-node
|
||||||
const inputs = Object.entries(node.inputs || {});
|
.data=${data.payload}
|
||||||
const outputs = Object.entries(node.outputs || {});
|
.emit=${emit}
|
||||||
const color = categoryColors[node.category] || '#6b7280';
|
></wf-node>`;
|
||||||
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
position: relative; display: flex; flex-direction: column;
|
|
||||||
align-items: center; width: 280px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
${inputs.length > 0
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
style="display: flex; justify-content: center; gap: 8px; position: relative; z-index: 1; margin-bottom: -7px;"
|
|
||||||
>
|
|
||||||
${inputs.map(([key, input]) =>
|
|
||||||
input
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
style="display: flex; align-items: center; justify-content: center;"
|
|
||||||
>
|
|
||||||
<rete-ref
|
|
||||||
.data=${{
|
|
||||||
type: 'socket',
|
|
||||||
side: 'input',
|
|
||||||
key,
|
|
||||||
nodeId: node.id,
|
|
||||||
payload: input.socket,
|
|
||||||
}}
|
|
||||||
.emit=${emit}
|
|
||||||
></rete-ref>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: null
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: null}
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
background: #1f2937; border: 1px solid #374151;
|
|
||||||
border-radius: 12px; width: 100%; overflow: hidden;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style="height: 3px; border-radius: 10px 10px 0 0; overflow: hidden;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-wf="bar"
|
|
||||||
style="
|
|
||||||
height: 100%; width: 100%;
|
|
||||||
background: #6b7280; opacity: 0.25;
|
|
||||||
"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="padding: 6px 14px; display: flex; align-items: center; gap: 6px;"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-wf="dot"
|
|
||||||
style="
|
|
||||||
width: 7px; height: 7px; border-radius: 50%; background: #6b7280; flex-shrink: 0;
|
|
||||||
"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
data-wf="label"
|
|
||||||
style="font-size: 10px; color: #6b7280; font-weight: 500; flex: 1;"
|
|
||||||
>Not connected</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-wf-delete="${node.id}"
|
|
||||||
style="
|
|
||||||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
||||||
width: 18px; height: 18px; border-radius: 4px;
|
|
||||||
color: #6b7280; transition: all 0.15s;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="12"
|
|
||||||
height="12"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="height: 1px; background: #374151; margin: 0 14px;"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
style="padding: 10px 14px 12px; display: flex; align-items: flex-start; gap: 10px;"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="ph ${node.icon}"
|
|
||||||
style="font-size: 18px; color: ${color}; flex-shrink: 0; margin-top: 1px; line-height: 1;"
|
|
||||||
></i>
|
|
||||||
<div style="flex: 1; min-width: 0;">
|
|
||||||
<div
|
|
||||||
style="font-size: 13px; font-weight: 600; color: #f3f4f6; line-height: 1.3;"
|
|
||||||
>
|
|
||||||
${node.label}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style="font-size: 11px; color: #9ca3af; margin-top: 2px; line-height: 1.3;"
|
|
||||||
>
|
|
||||||
${node.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
${outputs.length > 0
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
style="display: flex; justify-content: center; gap: 8px; position: relative; z-index: 1; margin-top: -7px;"
|
|
||||||
>
|
|
||||||
${outputs.map(([key, output]) =>
|
|
||||||
output
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
style="display: flex; align-items: center; justify-content: center;"
|
|
||||||
>
|
|
||||||
<rete-ref
|
|
||||||
.data=${{
|
|
||||||
type: 'socket',
|
|
||||||
side: 'output',
|
|
||||||
key,
|
|
||||||
nodeId: node.id,
|
|
||||||
payload: output.socket,
|
|
||||||
}}
|
|
||||||
.emit=${emit}
|
|
||||||
></rete-ref>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: null
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
socket() {
|
socket() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export abstract class BaseWorkflowNode extends ClassicPreset.Node {
|
|||||||
width = 280;
|
width = 280;
|
||||||
height = 140;
|
height = 140;
|
||||||
execStatus: 'idle' | 'running' | 'completed' | 'error' = 'idle';
|
execStatus: 'idle' | 'running' | 'completed' | 'error' = 'idle';
|
||||||
|
nodeType: string = '';
|
||||||
|
|
||||||
constructor(label: string) {
|
constructor(label: string) {
|
||||||
super(label);
|
super(label);
|
||||||
|
|||||||
@@ -593,7 +593,9 @@ export const nodeRegistry: Record<string, NodeRegistryEntry> = {
|
|||||||
export function createNodeByType(type: string): BaseWorkflowNode | null {
|
export function createNodeByType(type: string): BaseWorkflowNode | null {
|
||||||
const entry = nodeRegistry[type];
|
const entry = nodeRegistry[type];
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
return entry.factory();
|
const node = entry.factory();
|
||||||
|
node.nodeType = type;
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNodesByCategory(): Record<
|
export function getNodesByCategory(): Record<
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ interface SerializedConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNodeType(node: BaseWorkflowNode): string | null {
|
function getNodeType(node: BaseWorkflowNode): string | null {
|
||||||
const constructorName = node.constructor.name;
|
return node.nodeType || null;
|
||||||
return constructorName || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeWorkflow(
|
function serializeWorkflow(
|
||||||
@@ -79,6 +78,22 @@ async function deserializeWorkflow(
|
|||||||
editor: NodeEditor<ClassicScheme>,
|
editor: NodeEditor<ClassicScheme>,
|
||||||
area: AreaPlugin<ClassicScheme, AreaExtra>
|
area: AreaPlugin<ClassicScheme, AreaExtra>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
!data ||
|
||||||
|
!Array.isArray((data as any).nodes) ||
|
||||||
|
!Array.isArray((data as any).connections)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid workflow file: missing nodes or connections array.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((data as any).version !== WORKFLOW_VERSION) {
|
||||||
|
console.warn(
|
||||||
|
`Workflow version mismatch: expected ${WORKFLOW_VERSION}, got ${(data as any).version}. Attempting load anyway.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
for (const conn of editor.getConnections()) {
|
for (const conn of editor.getConnections()) {
|
||||||
await editor.removeConnection(conn.id);
|
await editor.removeConnection(conn.id);
|
||||||
}
|
}
|
||||||
@@ -129,9 +144,6 @@ async function deserializeWorkflow(
|
|||||||
|
|
||||||
if (skippedTypes.length > 0) {
|
if (skippedTypes.length > 0) {
|
||||||
console.warn('Skipped unknown node types during load:', skippedTypes);
|
console.warn('Skipped unknown node types during load:', skippedTypes);
|
||||||
throw new Error(
|
|
||||||
`Some nodes could not be loaded: ${skippedTypes.join(', ')}. They may have been removed or renamed.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,14 +227,11 @@ export function exportWorkflow(
|
|||||||
const json = JSON.stringify(data, null, 2);
|
const json = JSON.stringify(data, null, 2);
|
||||||
const blob = new Blob([json], { type: 'application/json' });
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
try {
|
const a = document.createElement('a');
|
||||||
const a = document.createElement('a');
|
a.href = url;
|
||||||
a.href = url;
|
a.download = 'workflow.json';
|
||||||
a.download = 'workflow.json';
|
a.click();
|
||||||
a.click();
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
} finally {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function importWorkflow(
|
export async function importWorkflow(
|
||||||
@@ -233,13 +242,16 @@ export async function importWorkflow(
|
|||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = '.json';
|
input.accept = '.json';
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
|
settled = true;
|
||||||
const file = input.files?.[0];
|
const file = input.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const data = JSON.parse(text) as SerializedWorkflow;
|
const data = JSON.parse(text) as SerializedWorkflow;
|
||||||
@@ -250,6 +262,15 @@ export async function importWorkflow(
|
|||||||
reject(new Error(`Failed to import workflow: ${message}`));
|
reject(new Error(`Failed to import workflow: ${message}`));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
window.removeEventListener('focus', onFocus);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!settled) resolve();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
window.addEventListener('focus', onFocus);
|
||||||
|
|
||||||
input.click();
|
input.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user