diff --git a/src/ui/src/builder/BuilderSettingsHandlers.vue b/src/ui/src/builder/BuilderSettingsHandlers.vue index 19a637a3d..463fe156d 100644 --- a/src/ui/src/builder/BuilderSettingsHandlers.vue +++ b/src/ui/src/builder/BuilderSettingsHandlers.vue @@ -186,7 +186,13 @@ const recognisedEvents: ComputedRef = }); const userFunctions = computed(() => wf.getUserFunctions()); -const pageKeys = computed(() => wf.getPageKeys()); +const pageKeys = computed(() => { + const pages = wf.getComponents("root"); + const pageKeys = pages + .map((page) => page.content["key"]) + .filter((pageKey) => Boolean(pageKey)); + return pageKeys; +}); const isHandlerInvalid = (eventType: string) => { const handlerFunctionName = component.value.handlers?.[eventType]; diff --git a/src/ui/src/builder/BuilderSidebarTree.vue b/src/ui/src/builder/BuilderSidebarTree.vue index 97ddac982..cdc0e67a8 100644 --- a/src/ui/src/builder/BuilderSidebarTree.vue +++ b/src/ui/src/builder/BuilderSidebarTree.vue @@ -47,11 +47,15 @@ > -
- +
@@ -73,8 +77,17 @@ const { createAndInsertComponent, goToComponentParentPage } = const searchQuery: Ref = ref(null); const matchIndex: Ref = ref(-1); +const rootType = computed(() => { + let targetType = "root"; + if (ssbm.getMode() == "workflows") { + targetType = "workflows_root"; + } + return targetType; +}); const rootComponents = computed(() => { - return wf.getComponents(null, { sortedByPosition: true }); + return wf + .getComponents(null, { sortedByPosition: true }) + .filter((c) => c.type == rootType.value); }); function determineMatch(component: Component, query: string): boolean { @@ -152,6 +165,16 @@ async function addPage() { await nextTick(); ssbm.setSelection(pageId); } + +async function addWorkflow() { + const pageId = createAndInsertComponent( + "workflows_workflow", + "workflows_root", + ); + wf.setActivePageId(pageId); + await nextTick(); + ssbm.setSelection(pageId); +} diff --git a/src/ui/src/components/workflows/WorkflowsWorkflow.vue b/src/ui/src/components/workflows/WorkflowsWorkflow.vue new file mode 100644 index 000000000..69b130c0f --- /dev/null +++ b/src/ui/src/components/workflows/WorkflowsWorkflow.vue @@ -0,0 +1,285 @@ + + + + + + diff --git a/src/ui/src/components/workflows/abstract/WorkflowsNode.vue b/src/ui/src/components/workflows/abstract/WorkflowsNode.vue index 66076d502..04dac5d4f 100644 --- a/src/ui/src/components/workflows/abstract/WorkflowsNode.vue +++ b/src/ui/src/components/workflows/abstract/WorkflowsNode.vue @@ -3,7 +3,33 @@ - + diff --git a/src/ui/src/components/workflows/base/WorkflowArrow.vue b/src/ui/src/components/workflows/base/WorkflowArrow.vue new file mode 100644 index 000000000..3d9c83f82 --- /dev/null +++ b/src/ui/src/components/workflows/base/WorkflowArrow.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/src/ui/src/core/index.ts b/src/ui/src/core/index.ts index fdda46bb0..72b9cefd2 100644 --- a/src/ui/src/core/index.ts +++ b/src/ui/src/core/index.ts @@ -558,14 +558,6 @@ export function generateCore() { setActivePageId(matches[0].id); } - function getPageKeys() { - const pages = getComponents("root"); - const pageKeys = pages - .map((page) => page.content["key"]) - .filter((pageKey) => !!pageKey); - return pageKeys; - } - function setActivePageId(componentId: Component["id"]) { activePageId.value = componentId; } @@ -601,7 +593,6 @@ export function generateCore() { getComponents, setActivePageId, getActivePageId, - getPageKeys, setActivePageFromKey, getComponentDefinition, getSupportedComponentTypes, diff --git a/src/ui/src/core/navigation.ts b/src/ui/src/core/navigation.ts new file mode 100644 index 000000000..f74b01563 --- /dev/null +++ b/src/ui/src/core/navigation.ts @@ -0,0 +1,70 @@ +type ParsedHash = { + pageKey?: string; + routeVars: Map; // Stored as Map to avoid injection e.g. prototype pollution +}; + +const hashRegex = /^((?[^/]*))?(\/(?.*))?$/; +const routeVarRegex = /^(?[^=]+)=(?.*)$/; + +export function getParsedHash(): ParsedHash { + const docHash = document.location.hash.substring(1); + const hashMatchGroups = docHash.match(hashRegex)?.groups; + const routeVars: Map = new Map(); + const pageKey = hashMatchGroups?.pageKey + ? decodeURIComponent(hashMatchGroups.pageKey) + : undefined; + + if (!hashMatchGroups) return { pageKey, routeVars }; + + const routeVarsSegments = hashMatchGroups.routeVars?.split("&") ?? []; + routeVarsSegments.forEach((routeVarSegment) => { + const matchGroups = routeVarSegment.match(routeVarRegex)?.groups; + if (!matchGroups) return; + const { key, value } = matchGroups; + const decodedKey = decodeURIComponent(key); + const decodedValue = decodeURIComponent(value); + routeVars.set(decodedKey, decodedValue); + }); + + return { pageKey, routeVars }; +} + +function setHash(parsedHash: ParsedHash) { + const { pageKey, routeVars } = parsedHash; + + let hash = ""; + if (pageKey) { + hash += `${encodeURIComponent(pageKey)}`; + } + + if (routeVars.keys.length > 0) { + hash += "/"; + hash += Array.from(routeVars.entries()) + .map(([key, value]) => { + // Vars set to null are excluded from the hash + + if (value === null) return null; + return `${encodeURIComponent(key)}=${encodeURIComponent( + value, + )}`; + }) + .filter((segment) => segment) + .join("&"); + } + document.location.hash = hash; +} + +export function changePageInHash(targetPageKey: string) { + const parsedHash = getParsedHash(); + parsedHash.pageKey = targetPageKey; + setHash(parsedHash); +} + +export function changeRouteVarsInHash(targetRouteVars: Record) { + const parsedHash = getParsedHash(); + const routeVars = parsedHash?.routeVars ?? {}; + parsedHash.routeVars = new Map( + Object.entries({ ...routeVars, ...targetRouteVars }), + ); + setHash(parsedHash); +} diff --git a/src/ui/src/core/templateMap.ts b/src/ui/src/core/templateMap.ts index facc6330f..c7261ce97 100644 --- a/src/ui/src/core/templateMap.ts +++ b/src/ui/src/core/templateMap.ts @@ -64,7 +64,9 @@ import CoreMapbox from "../components/core/embed/CoreMapbox.vue"; // WORKFLOWS +import WorkflowsWorkflow from "../components/workflows/WorkflowsWorkflow.vue"; import WorkflowsNode from "../components/workflows/abstract/WorkflowsNode.vue"; +import WorkflowsRoot from "@/components/workflows/WorkflowsRoot.vue"; import { AbstractTemplate, WriterComponentDefinition } from "@/writerTypes"; import { h } from "vue"; @@ -125,7 +127,9 @@ const templateMap = { avatar: CoreAvatar, annotatedtext: CoreAnnotatedText, jsonviewer: CoreJsonViewer, - workflowsnode: WorkflowsNode, + workflows_root: WorkflowsRoot, + workflows_workflow: WorkflowsWorkflow, + workflows_node: WorkflowsNode, }; const abstractTemplateMap: Record = {}; diff --git a/src/ui/src/renderer/ComponentProxy.vue b/src/ui/src/renderer/ComponentProxy.vue index e781aeca5..326623405 100644 --- a/src/ui/src/renderer/ComponentProxy.vue +++ b/src/ui/src/renderer/ComponentProxy.vue @@ -51,12 +51,13 @@ export default { () => isBeingEdited.value && !component.value.isCodeManaged && - component.value.type !== "root", + component.value.type !== "root" && + component.value.type !== "workflows_root", ); const isParentSuitable = (parentId, childType) => { const allowedTypes = !parentId - ? ["root"] + ? ["root", "workflows_root"] : wf.getContainableTypes(parentId); return allowedTypes.includes(childType); }; diff --git a/src/ui/src/renderer/ComponentRenderer.vue b/src/ui/src/renderer/ComponentRenderer.vue index 0b2d1e5a5..ce7f4bbef 100644 --- a/src/ui/src/renderer/ComponentRenderer.vue +++ b/src/ui/src/renderer/ComponentRenderer.vue @@ -9,10 +9,12 @@
@@ -20,35 +22,38 @@