feat(workflows): add Gmail sending and testing workflows

- Introduced a new `send-gmail` workflow for sending emails via Gmail SMTP, with configurable parameters for recipients, subject, and body content.
- Added a `test-send-gmail` workflow to facilitate testing the email sending functionality with sample data.
- Implemented a `track` workflow to monitor Gojek trips, enhancing the server's capabilities for tracking and notifications.
- Updated the workflow management interface to include new workflows and improved organization of existing scripts.

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
2026-08-15 18:33:13 +07:00
parent 94ba931d9b
commit 7fe5defc43
22 changed files with 2486 additions and 159 deletions
@@ -0,0 +1,193 @@
import { useMemo, useState } from "react";
import { parse as parseYaml } from "yaml";
import { useHttpAuths, useHttpPages, useScripts, useWorkflows } from "../../api/hooks.js";
import { workflowToFlowchart } from "../../lib/workflow-mermaid.js";
import { parseWorkflowYaml, stringifyWorkflowDoc } from "../../lib/workflow-doc.js";
import { ScriptsTab } from "./ScriptsTab.jsx";
import { TriggersTab } from "./TriggersTab.jsx";
import { YamlTab } from "./YamlTab.jsx";
import { WorkflowTestPanel } from "./WorkflowTestPanel.jsx";
export function useYamlPreview(content) {
return useMemo(() => {
try {
const parsedYaml = parseYaml(content);
return {
parsed: parsedYaml,
parseError: null,
mermaid: workflowToFlowchart(parsedYaml),
};
} catch (err) {
return {
parsed: null,
parseError: err instanceof Error ? err.message : String(err),
mermaid: { chart: "", scriptIds: {} },
};
}
}, [content]);
}
export function WorkflowVisualEditor({
yaml,
onYamlChange,
owner,
file,
extraChrome,
showTest,
savedYaml,
testKey,
}) {
const [tab, setTab] = useState("scripts");
const [lastEdited, setLastEdited] = useState("yaml");
const [doc, setDoc] = useState(() => parseWorkflowYaml(yaml).doc);
const { parsed, parseError, mermaid } = useYamlPreview(yaml);
const parsedDoc = useMemo(() => parseWorkflowYaml(yaml), [yaml]);
const displayDoc = lastEdited === "visual" && doc ? doc : parsedDoc.doc;
const visualDisabled = !displayDoc;
const { data: scripts = [] } = useScripts();
const { data: workflows = [] } = useWorkflows(owner || undefined);
const { data: auths = [] } = useHttpAuths();
const { data: allPages = [] } = useHttpPages();
const pages = allPages.filter((p) => p.kind !== "template");
const scriptCount = displayDoc?.scripts?.length ?? 0;
const triggerCount = displayDoc?.triggers?.length ?? 0;
const unsaved = savedYaml != null && yaml !== savedYaml;
function patchDoc(mutator) {
const base = lastEdited === "visual" && doc ? doc : parsedDoc.doc;
if (!base) return;
const next = structuredClone(base);
mutator(next);
setDoc(next);
onYamlChange(stringifyWorkflowDoc(next));
setLastEdited("visual");
}
function onYamlTabChange(value) {
setLastEdited("yaml");
onYamlChange(value);
}
function selectTab(next) {
if (next !== "yaml" && lastEdited === "yaml") {
const { doc: d } = parseWorkflowYaml(yaml);
if (d) setDoc(d);
}
setTab(next);
}
const defaultData = displayDoc?.extra?.data ?? {};
let testDisabledReason = null;
if (visualDisabled) testDisabledReason = "Fix YAML before running.";
else if (unsaved) testDisabledReason = "Save the workflow before running. Test uses the saved YAML.";
return (
<>
<div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
{extraChrome}
<input
className="input input-sm sm:max-w-xs"
placeholder="name"
value={displayDoc?.name ?? ""}
disabled={visualDisabled}
onChange={(e) =>
patchDoc((d) => {
d.name = e.target.value;
})
}
/>
<textarea
className="textarea textarea-sm min-h-10 w-full sm:max-w-md"
placeholder="description"
rows={1}
value={displayDoc?.description ?? ""}
disabled={visualDisabled}
onChange={(e) =>
patchDoc((d) => {
d.description = e.target.value;
})
}
/>
{unsaved ? <span className="badge badge-warning badge-sm">Unsaved</span> : null}
</div>
<div role="tablist" className="tabs tabs-box shrink-0 w-full sm:w-auto">
<button
type="button"
role="tab"
className={`tab ${tab === "scripts" ? "tab-active" : ""}`}
onClick={() => selectTab("scripts")}
>
Scripts ({scriptCount})
</button>
<button
type="button"
role="tab"
className={`tab ${tab === "triggers" ? "tab-active" : ""}`}
onClick={() => selectTab("triggers")}
>
Triggers ({triggerCount})
</button>
<button
type="button"
role="tab"
className={`tab ${tab === "yaml" ? "tab-active" : ""}`}
onClick={() => selectTab("yaml")}
>
YAML
</button>
</div>
{parseError && tab !== "yaml" ? (
<p className="text-error text-sm shrink-0">
YAML is invalid — switch to the YAML tab to fix it. Visual edits are disabled.
</p>
) : null}
{tab === "scripts" ? (
<ScriptsTab
doc={displayDoc}
onPatch={patchDoc}
disabled={visualDisabled}
scripts={scripts}
workflows={workflows}
owner={owner}
excludeFile={file}
/>
) : null}
{tab === "triggers" ? (
<TriggersTab
doc={displayDoc}
onPatch={patchDoc}
disabled={visualDisabled}
owner={owner}
auths={auths}
pages={pages}
/>
) : null}
{tab === "yaml" ? (
<YamlTab
content={yaml}
onChange={onYamlTabChange}
parseError={parseError}
mermaid={mermaid}
parsed={parsed}
/>
) : null}
{showTest && owner && file ? (
<WorkflowTestPanel
owner={owner}
file={file}
defaultData={defaultData}
disabled={Boolean(testDisabledReason)}
disabledReason={testDisabledReason}
testKey={testKey}
/>
) : null}
</>
);
}