Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Workflows foundations #554

Merged
merged 21 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/ui/src/builder/BuilderSettingsHandlers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,13 @@ const recognisedEvents: ComputedRef<WriterComponentDefinition["events"]> =
});

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];
Expand Down
31 changes: 27 additions & 4 deletions src/ui/src/builder/BuilderSidebarTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@
></BuilderTreeBranch>
</div>
</div>
<div class="addPage">
<button @click="addPage">
<div class="add">
<button v-if="rootType == 'root'" @click="addPage">
<i class="material-symbols-outlined"> add </i>
Add Page
</button>
<button v-if="rootType == 'workflows_root'" @click="addWorkflow">
<i class="material-symbols-outlined"> add </i>
Add Workflow
</button>
</div>
</div>
</template>
Expand All @@ -73,8 +77,17 @@ const { createAndInsertComponent, goToComponentParentPage } =

const searchQuery: Ref<string> = ref(null);
const matchIndex: Ref<number> = 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 {
Expand Down Expand Up @@ -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);
}
</script>

<style scoped>
Expand All @@ -172,7 +195,7 @@ async function addPage() {
padding: 0 12px 12px 12px;
}

.addPage {
.add {
margin-top: auto;
padding: 0 16px 16px 16px;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ui/src/builder/builderManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function generateBuilderManager() {
`.ComponentRenderer [data-writer-id="${componentId}"]`,
);
resolvedInstancePath =
componentFirstElement.dataset.writerInstancePath;
componentFirstElement?.dataset.writerInstancePath;
}

state.value.selection = {
Expand Down
5 changes: 3 additions & 2 deletions src/ui/src/builder/useComponentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
parentId,
content: initContent,
handlers: {},
position: position ?? getNextInsertionPosition(parentId, type)
position: position ?? getNextInsertionPosition(parentId, type),
};

return component;
Expand Down Expand Up @@ -806,7 +806,8 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
): Component["id"] {
const component = wf.getComponentById(componentId);
if (!component || component.type == "root") return null;
if (component.type == "page") return componentId;
if (component.type == "page" || component.type == "workflows_workflow")
return componentId;
return getContainingPageId(component.parentId);
}

Expand Down
233 changes: 27 additions & 206 deletions src/ui/src/components/core/root/CoreRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<template v-for="(vnode, index) in getChildrenVNodes()" :key="index">
<component
:is="vnode"
v-if="vnode.key === `${activePageId}:0`"
v-if="vnode.key === `${displayedPageId}:0`"
></component>
</template>
</div>
Expand All @@ -12,7 +12,6 @@
<script lang="ts">
import { FieldType } from "@/writerTypes";
import * as sharedStyleFields from "@/renderer/sharedStyleFields";
import { nextTick } from "vue";
import { useEvaluator } from "@/renderer/useEvaluator";

const ssHashChangeStub = `
Expand Down Expand Up @@ -67,17 +66,28 @@ export default {
};
</script>
<script setup lang="ts">
import { computed, inject, ref, Ref, watch, onBeforeMount } from "vue";
import {
computed,
inject,
ref,
Ref,
watch,
nextTick,
onBeforeMount,
} from "vue";
import injectionKeys from "@/injectionKeys";
import { changePageInHash, getParsedHash } from "@/core/navigation";

const importedModulesSpecifiers: Record<string, string> = {};
const wf = inject(injectionKeys.core);
const ssbm = inject(injectionKeys.builderManager);
const getChildrenVNodes = inject(injectionKeys.getChildrenVNodes);
const rootEl: Ref<HTMLElement> = ref(null);
const { isComponentVisible } = useEvaluator(wf);

const getFirstPageId = () => {
const displayedPageId = computed(() => {
const activePageId = wf.getActivePageId();
if (activePageId && wf.isChildOf("root", activePageId)) return activePageId;

const pageComponents = wf.getComponents("root", {
includeBMC: true,
includeCMC: true,
Expand All @@ -87,93 +97,8 @@ const getFirstPageId = () => {
const visiblePages = pageComponents.filter((c) => isComponentVisible(c.id));
if (visiblePages.length == 0) return null;
return visiblePages[0].id;
};

const hashRegex = /^((?<pageKey>[^/]*))?(\/(?<routeVars>.*))?$/;
const routeVarRegex = /^(?<key>[^=]+)=(?<value>.*)$/;
const activePageId = computed(() => wf.getActivePageId() ?? getFirstPageId());

watch(activePageId, (newPageId) => {
const page = wf.getComponentById(newPageId);
const pageKey = page.content?.["key"];
if (ssbm && ssbm.getSelectedId() !== newPageId) {
ssbm.setSelection(null);
}
nextTick().then(() => {
window.scrollTo(0, 0);
const rendererEl = document.querySelector(".ComponentRenderer");
rendererEl.parentElement.scrollTo(0, 0);
});
changePageInHash(pageKey);
});

type ParsedHash = {
pageKey?: string;
routeVars: Record<string, string>;
};

function getParsedHash(): ParsedHash {
const docHash = document.location.hash.substring(1);
const hashMatchGroups = docHash.match(hashRegex)?.groups;
let pageKey: string;
let routeVars: Record<string, string> = {};

if (!hashMatchGroups) return { pageKey, routeVars };

pageKey = hashMatchGroups?.pageKey
? decodeURIComponent(hashMatchGroups.pageKey)
: undefined;

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[decodedKey] = decodedValue;
});

return { pageKey, routeVars };
}

function setHash(parsedHash: ParsedHash) {
const { pageKey, routeVars } = parsedHash;

let hash = "";
if (pageKey) {
hash += `${encodeURIComponent(pageKey)}`;
}
if (Object.keys(routeVars).length > 0) {
hash += "/";
hash += Object.entries(routeVars)
.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;
}

function changePageInHash(targetPageKey: string) {
const parsedHash = getParsedHash();
parsedHash.pageKey = targetPageKey;
setHash(parsedHash);
}

function changeRouteVarsInHash(targetRouteVars: Record<string, string>) {
const parsedHash = getParsedHash();
const routeVars = parsedHash?.routeVars ?? {};
parsedHash.routeVars = { ...routeVars, ...targetRouteVars };
setHash(parsedHash);
}

function handleHashChange() {
const parsedHash = getParsedHash();
const event = new CustomEvent("wf-hashchange", {
Expand All @@ -186,126 +111,22 @@ function handleHashChange() {
wf.setActivePageFromKey(parsedHash.pageKey);
}

async function importStylesheet(stylesheetKey: string, path: string) {
const existingEl = document.querySelector(
`[data-writer-stylesheet-key="${stylesheetKey}"]`,
);
existingEl?.remove();
const el = document.createElement("link");
el.dataset.writerStylesheetKey = stylesheetKey;
el.setAttribute("href", path);
el.setAttribute("rel", "stylesheet");
document.head.appendChild(el);
}

async function importScript(scriptKey: string, path: string) {
const existingEl = document.querySelector(
`[data-writer-script-key="${scriptKey}"]`,
);
existingEl?.remove();
const el = document.createElement("script");
el.dataset.writerScriptKey = scriptKey;
el.src = path;
el.setAttribute("rel", "modulepreload");
document.head.appendChild(el);
}

async function importModule(moduleKey: string, specifier: string) {
importedModulesSpecifiers[moduleKey] = specifier;
await import(/* @vite-ignore */ specifier);
}

async function handleFunctionCall(
moduleKey: string,
functionName: string,
args: any[],
) {
const specifier = importedModulesSpecifiers[moduleKey];
const m = await import(/* @vite-ignore */ specifier);

if (!m) {
// eslint-disable-next-line no-console
console.warn(
`The module with key "${moduleKey}" cannot be found. Please check that it has been imported.`,
);
return;
watch(displayedPageId, (newPageId) => {
const page = wf.getComponentById(newPageId);
const pageKey = page.content?.["key"];
if (ssbm && ssbm.getSelectedId() !== newPageId) {
ssbm.setSelection(null);
}
m[functionName](...args);
}

type FileDownloadMailItemPayload = {
data: string;
fileName: string;
};

function addMailSubscriptions() {
wf.addMailSubscription(
"fileDownload",
(mailItem: FileDownloadMailItemPayload) => {
const el = document.createElement("a");
el.href = mailItem.data;
el.download = mailItem.fileName;
el.click();
},
);
wf.addMailSubscription("openUrl", (url: string) => {
const el = document.createElement("a");
el.href = url;
el.target = "_blank";
el.rel = "noopener noreferrer";
el.click();
});
wf.addMailSubscription("pageChange", (pageKey: string) => {
changePageInHash(pageKey);
nextTick().then(() => {
window.scrollTo(0, 0);
document
.querySelector(".ComponentRenderer")
?.parentElement?.scrollTo(0, 0);
});
wf.addMailSubscription(
"routeVarsChange",
(routeVars: Record<string, string>) => {
changeRouteVarsInHash(routeVars);
},
);
wf.addMailSubscription(
"importStylesheet",
({ stylesheetKey, path }: { stylesheetKey: string; path: string }) => {
importStylesheet(stylesheetKey, path);
},
);
wf.addMailSubscription(
"importScript",
({ scriptKey, path }: { scriptKey: string; path: string }) => {
importScript(scriptKey, path);
},
);
wf.addMailSubscription(
"importModule",
({
moduleKey,
specifier,
}: {
moduleKey: string;
specifier: string;
}) => {
importModule(moduleKey, specifier);
},
);
wf.addMailSubscription(
"functionCall",
({
moduleKey,
functionName,
args,
}: {
moduleKey: string;
functionName: string;
args: any[];
}) => {
handleFunctionCall(moduleKey, functionName, args);
},
);
}
changePageInHash(pageKey);
});

onBeforeMount(() => {
addMailSubscriptions();
window.addEventListener("hashchange", () => {
handleHashChange();
});
Expand Down
Loading
Loading