diff --git a/src/lib/registries/workflow-registry.mts b/src/lib/registries/workflow-registry.mts index bc08755..49d1354 100644 --- a/src/lib/registries/workflow-registry.mts +++ b/src/lib/registries/workflow-registry.mts @@ -92,8 +92,10 @@ export default class WorkflowRegistry { } /** Add a new workflow to the list */ - public add(workflow: Workflow): void { - this.setWorkflows([...this.workflows, workflow]); + public add(first: Workflow, ...rest: Workflow[]): void; + + public add(...workflow: Workflow[]): void { + this.setWorkflows([...this.workflows, ...workflow]); } /** Overwrite the workflow at the given index */ diff --git a/src/lib/util/export-modal.mts b/src/lib/util/export-modal.mts new file mode 100644 index 0000000..327b944 --- /dev/null +++ b/src/lib/util/export-modal.mts @@ -0,0 +1,48 @@ +import {Observable} from 'rxjs'; +import type {SweetAlertOptions} from 'sweetalert2'; + +export function showExportModal(title: string, contents: any): void { + const html = document.createElement('textarea'); + html.addEventListener('click', () => { + html.select(); + }); + html.textContent = JSON.stringify(contents); + html.readOnly = true; + + Swal // eslint-disable-line @typescript-eslint/no-floating-promises + .fire({ + confirmButtonText: 'OK', + html, + showCancelButton: false, + showConfirmButton: true, + title, + }); +} + +/** @return Pasted JSON */ +export function showImportModal( + title: string, + validate: Exclude +): Observable { + return new Observable(subscriber => { + Swal + .fire({ + cancelButtonText: 'Cancel', + confirmButtonText: 'Import', + input: 'textarea', + inputValidator: v => v ? validate(v) : 'Nothing typed in', + showCancelButton: true, + showConfirmButton: true, + title, + }) + .then(rsp => { + if (rsp.isConfirmed) { + subscriber.next(rsp.value); + } + subscriber.complete(); + }) + .catch(e => { + subscriber.error(e); + }); + }); +} diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 039bd69..d8af26d 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -14,6 +14,7 @@ import SidenavIcon from './components/sidenav-icon'; import {useBorderClassHost, usePrimaryExecutionHost, useRunningHost} from './global-ctx'; import DebugPage from './pages/debug-page'; import HelpPage from './pages/help-page'; +import ImportExportPage from './pages/import-export-page'; import NewWorkflow from './pages/new-workflow'; import WorkflowsDashboard from './pages/workflows-dashboard'; @@ -32,6 +33,7 @@ export default function App({sidenavIcon}: AppProps): VNod + diff --git a/src/ui/components/btn.tsx b/src/ui/components/btn.tsx index 55fd925..94e8d7f 100644 --- a/src/ui/components/btn.tsx +++ b/src/ui/components/btn.tsx @@ -21,5 +21,5 @@ export default function Btn({btnRef, kind, size, ...rest}: Props): VNode { clazz += ` btn-${size}`; } - return ; + return ; } diff --git a/src/ui/pages/import-export-page.tsx b/src/ui/pages/import-export-page.tsx new file mode 100644 index 0000000..d1de272 --- /dev/null +++ b/src/ui/pages/import-export-page.tsx @@ -0,0 +1,93 @@ +import type {VNode} from 'preact'; +import {useCallback} from 'preact/hooks'; +import {Workflow} from '../../lib/data/workflow.mjs'; +import WorkflowRegistry from '../../lib/registries/workflow-registry.mjs'; +import {EMPTY_ARR} from '../../lib/util.mjs'; +import {showExportModal, showImportModal} from '../../lib/util/export-modal.mjs'; +import {BlockDiv} from '../components/block'; +import Btn from '../components/btn'; +import PageContainer from '../components/page-container'; +import autoId from '../util/id-gen.mjs'; + +export const IMPORT_PAGE_ID = autoId(); +const btnSizeClass = 'sm btn-block'; + +export default function ImportExportPage(): VNode { + return ( + + + + + + + + ); +} + +function ImportAll(): VNode { + const onClick = useCallback(() => { + showImportModal('Import workflow', json => { + try { + const parsed = JSON.parse(json); + if (!Array.isArray(parsed)) { + return 'Expected a JSON array'; + } else if (!parsed.length) { + return 'The array is empty'; + } + + for (let i = 0; i < parsed.length; ++i) { + if (!Workflow.fromJSON(parsed[i])) { + return `Invalid workflow at index ${i}`; + } + } + + return null; + } catch (e) { + return e.message; + } + }) + .subscribe(rsp => { + const parsed = (JSON.parse(rsp) as any[]).map(Workflow.fromJSON); + WorkflowRegistry.inst.add(...parsed as [Workflow, ...Workflow[]]); + alertDone(); + }); + }, EMPTY_ARR); + + return Import workflows; +} + +function ImportOne(): VNode { + const onClick = useCallback(() => { + showImportModal('Import workflow', json => { + try { + return Workflow.fromJSON(JSON.parse(json)) ? null : 'Invalid workflow'; + } catch (e) { + return e.message; + } + }) + .subscribe(rsp => { + WorkflowRegistry.inst.add(Workflow.fromJSON(JSON.parse(rsp))!); + alertDone(); + }); + }, EMPTY_ARR); + + return Import workflow; +} + +function ExportAll(): VNode { + const onClick = useCallback(() => { + showExportModal('Export workflows', WorkflowRegistry.inst.workflows); + }, EMPTY_ARR); + + return Export workflows; +} + +function alertDone() { + Swal // eslint-disable-line @typescript-eslint/no-floating-promises + .fire({ + confirmButtonText: 'OK', + showCancelButton: false, + showConfirmButton: true, + text: 'Done!', + }); +} diff --git a/src/ui/pages/workflows-dashboard.tsx b/src/ui/pages/workflows-dashboard.tsx index 105cb3e..c90ea16 100644 --- a/src/ui/pages/workflows-dashboard.tsx +++ b/src/ui/pages/workflows-dashboard.tsx @@ -10,6 +10,7 @@ import {Workflow} from '../../lib/data/workflow.mjs'; import WorkflowRegistry from '../../lib/registries/workflow-registry.mjs'; import {EMPTY_ARR} from '../../lib/util.mjs'; import {alertConfirm} from '../../lib/util/alert'; +import {showExportModal} from '../../lib/util/export-modal.mjs'; import swapElements from '../../lib/util/swap-elements.mjs'; import {BorderedBlock} from '../components/block'; import Btn from '../components/btn'; @@ -260,11 +261,17 @@ function BtnsNotRunning({refresh}: SelectedWorkflowBtnsProps): VNode { activeWorkflow.value = cloned; }, [activeWorkflow]); + const doExport = useCallback(() => { + const wf = activeWorkflow.peek()!; + showExportModal(`Export ${wf.name}`, wf); + }, [activeWorkflow]); + return (
{'Run'} {'Edit'} {'Copy'} + {'Export'} {'Delete'}
); diff --git a/src/ui/ui.mts b/src/ui/ui.mts index b3e571f..e6022f6 100644 --- a/src/ui/ui.mts +++ b/src/ui/ui.mts @@ -4,6 +4,7 @@ import type {Obj} from '../public_api'; import './assets/styles.scss'; import {DEBUG_PAGE_ID} from './pages/debug-page'; import {HELP_PAGE_ID} from './pages/help-page'; +import {IMPORT_PAGE_ID} from './pages/import-export-page'; import {NEW_WORKFLOW_PAGE_ID} from './pages/new-workflow'; import {WORKFLOWS_DASHBOARD_ID} from './pages/workflows-dashboard'; @@ -51,6 +52,15 @@ interface BaseCat { cat({name: 'New Action Workflow'}), ], }, + { + ...pageCommon, + containerID: IMPORT_PAGE_ID, + customName: heading('Import/export'), + id: 'importExportWorkflows', + sidebarSubItems: [ + cat({name: 'Import/export'}), + ], + }, { ...pageCommon, containerID: HELP_PAGE_ID,