@aglyn/plugins-workflows 1.0.0-beta.228 → 1.0.0-beta.230

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +12 -12
  2. package/src/lib/components/host-actions-card.component.js +20 -3
  3. package/src/lib/components/host-actions-card.component.js.map +1 -1
  4. package/src/lib/components/host-workflows-card.component.d.ts +7 -0
  5. package/src/lib/components/host-workflows-card.component.js +35 -5
  6. package/src/lib/components/host-workflows-card.component.js.map +1 -1
  7. package/src/lib/components/org-automation-editor.component.d.ts +9 -0
  8. package/src/lib/components/org-automation-editor.component.js +32 -1
  9. package/src/lib/components/org-automation-editor.component.js.map +1 -1
  10. package/src/lib/components/org-automations-card.component.js +41 -9
  11. package/src/lib/components/org-automations-card.component.js.map +1 -1
  12. package/src/lib/components/workflow-zones.d.ts +53 -1
  13. package/src/lib/components/workflow-zones.js +14 -0
  14. package/src/lib/components/workflow-zones.js.map +1 -1
  15. package/src/lib/components/workflows-console-page.js +5 -3
  16. package/src/lib/components/workflows-console-page.js.map +1 -1
  17. package/src/lib/engine/run-event-actions.js +21 -5
  18. package/src/lib/engine/run-event-actions.js.map +1 -1
  19. package/src/lib/engine/webhook-delivery.d.ts +24 -0
  20. package/src/lib/engine/webhook-delivery.js +24 -0
  21. package/src/lib/engine/webhook-delivery.js.map +1 -0
  22. package/src/lib/model/webhooks.d.ts +8 -2
  23. package/src/lib/model/webhooks.js +7 -1
  24. package/src/lib/model/webhooks.js.map +1 -1
  25. package/src/lib/plugin.js +12 -3
  26. package/src/lib/plugin.js.map +1 -1
  27. package/src/lib/server/automation-run-figures.d.ts +38 -0
  28. package/src/lib/server/automation-run-figures.js +182 -0
  29. package/src/lib/server/automation-run-figures.js.map +1 -0
  30. package/src/lib/server-console.js +5 -0
  31. package/src/lib/server-console.js.map +1 -1
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import { checkEntitlement, hostEventLabel, pluginDocsHelp } from "@aglyn/aglyn";
20
20
  import { hostIdsFromScope, isOrgWideScope } from "@aglyn/aglyn/app-utils/scope-tokens";
21
+ import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
21
22
  import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
23
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
23
24
  import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
@@ -26,8 +27,9 @@ import { useFirestore, useFirestoreCollection } from "@aglyn/tenant-feature-inst
26
27
  import { Alert, Button, Chip, Menu, MenuItem, Stack, Switch, Typography } from "@mui/material";
27
28
  import { collection, documentId, limit, orderBy, query, where } from "firebase/firestore";
28
29
  import { useCallback, useMemo, useState } from "react";
29
- import { ORG_AUTOMATIONS_COLLECTION, ORG_AUTOMATIONS_MAX, orgAutomationPausedHostIds, readOrgAutomation } from "../model/org-automations.js";
30
- import { newOrgAutomationDraft, OrgAutomationEditor, orgAutomationBody, orgAutomationDraft } from "./org-automation-editor.component.js";
30
+ import { ORG_AUTOMATION_STEP_TYPES, ORG_AUTOMATION_TRIGGER_EVENTS, ORG_AUTOMATIONS_COLLECTION, ORG_AUTOMATIONS_MAX, orgAutomationPausedHostIds, readOrgAutomation } from "../model/org-automations.js";
31
+ import { newOrgAutomationDraft, OrgAutomationEditor, orgAutomationBody, orgAutomationDraft, orgAutomationDraftFromProposal } from "./org-automation-editor.component.js";
32
+ import { ORG_AUTOMATIONS_ZONE } from "./workflow-zones.js";
31
33
  import { useOrgAutomationsApi } from "./use-org-automations-api.js";
32
34
  import { orgSiteName } from "./workflows-org-mount.js";
33
35
  import { HOST_ACTION_STEP_LABELS } from "../model/host-actions.js";
@@ -99,19 +101,47 @@ import { HOST_ACTION_STEP_LABELS } from "../model/host-actions.js";
99
101
  }), [
100
102
  enqueueSnackbar
101
103
  ]);
104
+ const refuseUnentitled = useCallback(()=>{
105
+ enqueueSnackbar('Org automations are built from the actions builder, which requires a Pro plan — see Billing to upgrade', {
106
+ variant: 'warning',
107
+ persist: false
108
+ });
109
+ }, [
110
+ enqueueSnackbar
111
+ ]);
102
112
  const handleAdd = useCallback(()=>{
113
+ if (!entitled) return refuseUnentitled();
114
+ setDraft(newOrgAutomationDraft());
115
+ }, [
116
+ entitled,
117
+ refuseUnentitled
118
+ ]);
119
+ /*
120
+ * For the `orgAutomations` zone (AGL-3603): an automation another plugin
121
+ * proposed opens in the editor as a new one, unsaved and switched off —
122
+ * the same door Add org automation opens, so the plan is asked first and
123
+ * Save is still the only write.
124
+ */ const propose = useCallback((proposal)=>{
103
125
  if (!entitled) {
104
- enqueueSnackbar('Org automations are built from the actions builder, which requires a Pro plan — see Billing to upgrade', {
105
- variant: 'warning',
106
- persist: false
107
- });
108
- return;
126
+ refuseUnentitled();
127
+ return false;
109
128
  }
110
- setDraft(newOrgAutomationDraft());
129
+ const opened = orgAutomationDraftFromProposal(proposal);
130
+ if (!opened) return false;
131
+ setDraft(opened);
132
+ return true;
111
133
  }, [
112
134
  entitled,
113
- enqueueSnackbar
135
+ refuseUnentitled
114
136
  ]);
137
+ const ExtensionZone = useConsoleWidgetSlot();
138
+ const automationsZone = ExtensionZone && canEdit ? /*#__PURE__*/ _jsx(ExtensionZone, {
139
+ slot: ORG_AUTOMATIONS_ZONE.id,
140
+ orgId: mount.orgId,
141
+ triggers: ORG_AUTOMATION_TRIGGER_EVENTS,
142
+ steps: ORG_AUTOMATION_STEP_TYPES,
143
+ propose: propose
144
+ }) : null;
115
145
  const handleSave = useCallback(async ()=>{
116
146
  if (!draft) return;
117
147
  const body = orgAutomationBody(draft);
@@ -212,6 +242,8 @@ import { HOST_ACTION_STEP_LABELS } from "../model/host-actions.js";
212
242
  help: pluginDocsHelp('orgAutomations', {
213
243
  anchor: '#what-an-org-automation-is'
214
244
  }),
245
+ // Other ways to start an org automation (AGL-3603), in the header.
246
+ actions: automationsZone != null ? automationsZone : undefined,
215
247
  contentGutterX: true,
216
248
  contentGutterY: true,
217
249
  children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/org-automations-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n hostEventLabel,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport {\n hostIdsFromScope,\n isOrgWideScope,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Menu,\n MenuItem,\n Stack,\n Switch,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n orgAutomationPausedHostIds,\n type OrgAutomationRow,\n readOrgAutomation,\n} from '../model/org-automations'\nimport {\n newOrgAutomationDraft,\n OrgAutomationEditor,\n orgAutomationBody,\n type OrgAutomationDraft,\n orgAutomationDraft,\n} from './org-automation-editor.component'\nimport { useOrgAutomationsApi } from './use-org-automations-api'\nimport { orgSiteName, type WorkflowsOrgMount } from './workflows-org-mount'\nimport { HOST_ACTION_STEP_LABELS, type HostActionStepType } from '../model/host-actions'\n\nexport interface OrgAutomationsCardProps {\n mount: WorkflowsOrgMount\n org?: Partial<AglynOrgBilling>\n /**\n * Whether the reader may write the organization's automations — an owner,\n * admin or editor. The route decides; this keeps a viewer from being\n * offered controls that are about to refuse them.\n */\n canEdit: boolean\n}\n\n/** The sites an automation runs on, by id: every org site for `['org']`. */\nfunction placedSiteIds(\n row: Pick<OrgAutomationRow, 'visibleTo'>,\n mount: WorkflowsOrgMount,\n): string[] {\n return isOrgWideScope(row.visibleTo)\n ? mount.hosts.map((host) => host.id)\n : hostIdsFromScope(row.visibleTo)\n}\n\n/** \"Runs on every site\" or \"Runs on Shop, Blog\". */\nfunction placementLine(\n row: Pick<OrgAutomationRow, 'visibleTo'>,\n mount: WorkflowsOrgMount,\n): string {\n if (isOrgWideScope(row.visibleTo)) return 'Runs on every site'\n const names = hostIdsFromScope(row.visibleTo).map((id) => orgSiteName(mount, id))\n return names.length ? `Runs on ${names.join(', ')}` : 'Runs on no site'\n}\n\n/**\n * THE ORGANIZATION'S AUTOMATIONS (AGL-3302): what the organization runs on\n * the sites it chooses.\n *\n * One ceilinged read of the live automations — the org hub admits only\n * org-wide members, who read every one — and every write through\n * `automations/manage` or `automations/pause`, because the collection is\n * closed to client writes. Each row says where it runs and where it is\n * paused; a paused site is resumed from its chip, and any placed site can be\n * paused from the row, which is the same control the site's own hub offers.\n */\nexport function OrgAutomationsCard(props: OrgAutomationsCardProps) {\n const { mount, org, canEdit } = props\n const firestore = useFirestore()\n const api = useOrgAutomationsApi()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n\n /*\n * Every LIVE automation, ceilinged at the organization's cap plus the probe\n * row. `deletedAt` is always written — `null` while live — so equality asks\n * for exactly the live ones and deleted documents never take a place in the\n * window. Ordered by document id, which the automatic single-field index\n * carries behind the equality, so the window is named rather than whatever\n * order Firestore returns; the list is sorted by name once it is in hand.\n */\n const { data: rows } = useFirestoreCollection<OrgAutomationRow>(\n () =>\n query(\n collection(firestore, 'orgs', mount.orgId, ORG_AUTOMATIONS_COLLECTION),\n where('deletedAt', '==', null),\n orderBy(documentId()),\n limit(ORG_AUTOMATIONS_MAX + 1),\n ),\n [firestore, mount.orgId],\n { idField: '$id' },\n )\n const automations = useMemo(\n () =>\n [...(rows ?? [])]\n .slice(0, ORG_AUTOMATIONS_MAX)\n .sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''))),\n [rows],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const shown = useMemo(\n () => automations.slice(page * pageSize, page * pageSize + pageSize),\n [automations, page, pageSize],\n )\n\n const [draft, setDraft] = useState<OrgAutomationDraft | null>(null)\n /**\n * The editor has been opened at least once — the latch the pickers key on,\n * so Cancel and the next Edit do not buy the option lists twice.\n */\n const [editorOpened, setEditorOpened] = useState(false)\n if (draft && !editorOpened) setEditorOpened(true)\n const [saving, setSaving] = useState(false)\n const [pauseMenu, setPauseMenu] = useState<{\n anchor: HTMLElement\n row: OrgAutomationRow\n } | null>(null)\n\n const entitled = checkEntitlement(org, 'actions')\n const fail = useCallback(\n (error: unknown) =>\n enqueueSnackbar((error as Error)?.message || 'The request could not be completed', {\n variant: 'warning',\n persist: false,\n }),\n [enqueueSnackbar],\n )\n\n const handleAdd = useCallback(() => {\n if (!entitled) {\n enqueueSnackbar(\n 'Org automations are built from the actions builder, which requires a Pro plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n return\n }\n setDraft(newOrgAutomationDraft())\n }, [entitled, enqueueSnackbar])\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n const body = orgAutomationBody(draft)\n // The route's own reader, asked first so a mistake reads the same here as\n // it would coming back from the server — and costs no round trip.\n const read = readOrgAutomation(body)\n if (read.ok === false) {\n return void enqueueSnackbar(read.problem, { variant: 'warning', persist: false })\n }\n setSaving(true)\n try {\n await api('manage', {\n orgId: mount.orgId,\n action: draft.id ? 'update' : 'create',\n ...(draft.id ? { automationId: draft.id } : {}),\n automation: body,\n })\n setDraft(null)\n enqueueSnackbar('Org automation saved', { variant: 'success', persist: false })\n } catch (error) {\n fail(error)\n } finally {\n setSaving(false)\n }\n }, [draft, api, mount.orgId, enqueueSnackbar, fail])\n\n const handleToggle = useCallback(\n (row: OrgAutomationRow) => async (event: { target: { checked: boolean } }) => {\n try {\n await api('manage', {\n orgId: mount.orgId,\n action: 'setEnabled',\n automationId: row.$id,\n enabled: event.target.checked,\n })\n } catch (error) {\n fail(error)\n }\n },\n [api, mount.orgId, fail],\n )\n\n const handleDelete = useCallback(\n (row: OrgAutomationRow) => async () => {\n const confirmed = await confirm({\n title: 'Delete this org automation?',\n description:\n `\"${row.name}\" stops running on every site it is placed on, and ` +\n 'anyone waiting inside it stops too.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await api('manage', {\n orgId: mount.orgId,\n action: 'delete',\n automationId: row.$id,\n })\n } catch (error) {\n fail(error)\n }\n },\n [confirm, api, mount.orgId, fail],\n )\n\n const setPaused = useCallback(\n async (row: OrgAutomationRow, hostId: string, paused: boolean) => {\n setPauseMenu(null)\n try {\n await api('pause', { hostId, automationId: row.$id, paused })\n } catch (error) {\n fail(error)\n }\n },\n [api, fail],\n )\n\n return (\n <CardDisplay\n header={'Org automations'}\n help={pluginDocsHelp('orgAutomations', { anchor: '#what-an-org-automation-is' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Write an automation once and run it on every site you choose. ' +\n 'Each run is that site’s own: its email goes from that site, it ' +\n 'counts on that site’s action runs, and the site can pause it ' +\n 'for itself. Pro plans and up.'}\n </Typography>\n {shown.map((row) => {\n const paused = orgAutomationPausedHostIds(row)\n const placed = placedSiteIds(row, mount)\n const pausable = placed.filter((hostId) => !paused.includes(hostId))\n return (\n <Stack\n key={row.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Switch\n size=\"small\"\n checked={row.enabled !== false}\n disabled={!canEdit}\n onChange={handleToggle(row)}\n slotProps={{ input: { 'aria-label': `Switch ${row.name} on or off` } }}\n />\n <Stack sx={{ flex: 1, minWidth: 0 }} spacing={0.25}>\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`on ${hostEventLabel(row.trigger?.event)} · ${(row.steps ?? [])\n .map(\n (step) =>\n HOST_ACTION_STEP_LABELS[step.type as HostActionStepType] ??\n step.type,\n )\n .join(' → ')}`}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {placementLine(row, mount)}\n </Typography>\n {paused.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>\n {paused.map((hostId) => (\n <Chip\n key={hostId}\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label={`Paused on ${orgSiteName(mount, hostId)}`}\n {...(canEdit\n ? { onDelete: () => void setPaused(row, hostId, false) }\n : {})}\n />\n ))}\n </Stack>\n ) : null}\n </Stack>\n {canEdit ? (\n <>\n <Button size=\"small\" onClick={() => setDraft(orgAutomationDraft(row))}>\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n disabled={!pausable.length}\n aria-haspopup=\"menu\"\n onClick={(event) =>\n setPauseMenu({ anchor: event.currentTarget, row })\n }\n >\n {'Pause on…'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(row)}>\n {'Delete'}\n </Button>\n </>\n ) : null}\n </Stack>\n )\n })}\n {automations.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No org automations yet.'}\n </Typography>\n ) : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={automations.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n {(rows?.length ?? 0) > ORG_AUTOMATIONS_MAX ? (\n <Alert severity=\"info\">\n {`Showing ${ORG_AUTOMATIONS_MAX} org automations, the most an ` +\n 'organization holds.'}\n </Alert>\n ) : null}\n {canEdit ? (\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add org automation'}\n </Button>\n ) : null}\n </Stack>\n <Menu\n anchorEl={pauseMenu?.anchor ?? null}\n open={Boolean(pauseMenu)}\n onClose={() => setPauseMenu(null)}\n >\n {pauseMenu\n ? placedSiteIds(pauseMenu.row, mount)\n .filter(\n (hostId) =>\n !orgAutomationPausedHostIds(pauseMenu.row).includes(hostId),\n )\n .map((hostId) => (\n <MenuItem\n key={hostId}\n onClick={() => void setPaused(pauseMenu.row, hostId, true)}\n >\n {`Pause on ${orgSiteName(mount, hostId)}`}\n </MenuItem>\n ))\n : null}\n </Menu>\n <OrgAutomationEditor\n mount={mount}\n draft={draft}\n onDraft={(update) =>\n setDraft((previous) => (previous ? update(previous) : previous))\n }\n opened={editorOpened}\n saving={saving}\n onSave={() => void handleSave()}\n onClose={() => setDraft(null)}\n />\n </CardDisplay>\n )\n}\nOrgAutomationsCard.displayName = 'OrgAutomationsCard'\n\nexport default OrgAutomationsCard\n"],"names":["checkEntitlement","hostEventLabel","pluginDocsHelp","hostIdsFromScope","isOrgWideScope","CardDisplay","useConfirmationContext","ListPagination","TABLE_PAGE_SIZE_DEFAULT","useSnackbar","useFirestore","useFirestoreCollection","Alert","Button","Chip","Menu","MenuItem","Stack","Switch","Typography","collection","documentId","limit","orderBy","query","where","useCallback","useMemo","useState","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","orgAutomationPausedHostIds","readOrgAutomation","newOrgAutomationDraft","OrgAutomationEditor","orgAutomationBody","orgAutomationDraft","useOrgAutomationsApi","orgSiteName","HOST_ACTION_STEP_LABELS","placedSiteIds","row","mount","visibleTo","hosts","map","host","id","placementLine","names","length","join","OrgAutomationsCard","props","org","canEdit","firestore","api","enqueueSnackbar","confirm","data","rows","orgId","idField","automations","slice","sort","a","b","String","name","localeCompare","page","setPage","pageSize","setPageSize","shown","draft","setDraft","editorOpened","setEditorOpened","saving","setSaving","pauseMenu","setPauseMenu","entitled","fail","error","message","variant","persist","handleAdd","handleSave","body","read","ok","problem","action","automationId","automation","handleToggle","event","$id","enabled","target","checked","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","setPaused","hostId","paused","header","help","anchor","contentGutterX","contentGutterY","spacing","placed","pausable","filter","includes","direction","sx","alignItems","size","disabled","onChange","slotProps","input","flex","minWidth","noWrap","trigger","steps","step","type","flexWrap","gap","label","onDelete","onClick","aria-haspopup","currentTarget","rowCount","count","onPageChange","onPageSizeChange","severity","alignSelf","anchorEl","open","Boolean","onClose","onDraft","update","previous","opened","onSave","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/org-automations-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n hostEventLabel,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport {\n hostIdsFromScope,\n isOrgWideScope,\n} from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { TABLE_PAGE_SIZE_DEFAULT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n Chip,\n Menu,\n MenuItem,\n Stack,\n Switch,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n ORG_AUTOMATION_STEP_TYPES,\n ORG_AUTOMATION_TRIGGER_EVENTS,\n ORG_AUTOMATIONS_COLLECTION,\n ORG_AUTOMATIONS_MAX,\n orgAutomationPausedHostIds,\n type OrgAutomationRow,\n readOrgAutomation,\n} from '../model/org-automations'\nimport {\n newOrgAutomationDraft,\n OrgAutomationEditor,\n orgAutomationBody,\n type OrgAutomationDraft,\n orgAutomationDraft,\n orgAutomationDraftFromProposal,\n} from './org-automation-editor.component'\nimport { ORG_AUTOMATIONS_ZONE, type OrgAutomationProposal } from './workflow-zones'\nimport { useOrgAutomationsApi } from './use-org-automations-api'\nimport { orgSiteName, type WorkflowsOrgMount } from './workflows-org-mount'\nimport { HOST_ACTION_STEP_LABELS, type HostActionStepType } from '../model/host-actions'\n\nexport interface OrgAutomationsCardProps {\n mount: WorkflowsOrgMount\n org?: Partial<AglynOrgBilling>\n /**\n * Whether the reader may write the organization's automations — an owner,\n * admin or editor. The route decides; this keeps a viewer from being\n * offered controls that are about to refuse them.\n */\n canEdit: boolean\n}\n\n/** The sites an automation runs on, by id: every org site for `['org']`. */\nfunction placedSiteIds(\n row: Pick<OrgAutomationRow, 'visibleTo'>,\n mount: WorkflowsOrgMount,\n): string[] {\n return isOrgWideScope(row.visibleTo)\n ? mount.hosts.map((host) => host.id)\n : hostIdsFromScope(row.visibleTo)\n}\n\n/** \"Runs on every site\" or \"Runs on Shop, Blog\". */\nfunction placementLine(\n row: Pick<OrgAutomationRow, 'visibleTo'>,\n mount: WorkflowsOrgMount,\n): string {\n if (isOrgWideScope(row.visibleTo)) return 'Runs on every site'\n const names = hostIdsFromScope(row.visibleTo).map((id) => orgSiteName(mount, id))\n return names.length ? `Runs on ${names.join(', ')}` : 'Runs on no site'\n}\n\n/**\n * THE ORGANIZATION'S AUTOMATIONS (AGL-3302): what the organization runs on\n * the sites it chooses.\n *\n * One ceilinged read of the live automations — the org hub admits only\n * org-wide members, who read every one — and every write through\n * `automations/manage` or `automations/pause`, because the collection is\n * closed to client writes. Each row says where it runs and where it is\n * paused; a paused site is resumed from its chip, and any placed site can be\n * paused from the row, which is the same control the site's own hub offers.\n */\nexport function OrgAutomationsCard(props: OrgAutomationsCardProps) {\n const { mount, org, canEdit } = props\n const firestore = useFirestore()\n const api = useOrgAutomationsApi()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n\n /*\n * Every LIVE automation, ceilinged at the organization's cap plus the probe\n * row. `deletedAt` is always written — `null` while live — so equality asks\n * for exactly the live ones and deleted documents never take a place in the\n * window. Ordered by document id, which the automatic single-field index\n * carries behind the equality, so the window is named rather than whatever\n * order Firestore returns; the list is sorted by name once it is in hand.\n */\n const { data: rows } = useFirestoreCollection<OrgAutomationRow>(\n () =>\n query(\n collection(firestore, 'orgs', mount.orgId, ORG_AUTOMATIONS_COLLECTION),\n where('deletedAt', '==', null),\n orderBy(documentId()),\n limit(ORG_AUTOMATIONS_MAX + 1),\n ),\n [firestore, mount.orgId],\n { idField: '$id' },\n )\n const automations = useMemo(\n () =>\n [...(rows ?? [])]\n .slice(0, ORG_AUTOMATIONS_MAX)\n .sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''))),\n [rows],\n )\n const [page, setPage] = useState(0)\n const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT)\n const shown = useMemo(\n () => automations.slice(page * pageSize, page * pageSize + pageSize),\n [automations, page, pageSize],\n )\n\n const [draft, setDraft] = useState<OrgAutomationDraft | null>(null)\n /**\n * The editor has been opened at least once — the latch the pickers key on,\n * so Cancel and the next Edit do not buy the option lists twice.\n */\n const [editorOpened, setEditorOpened] = useState(false)\n if (draft && !editorOpened) setEditorOpened(true)\n const [saving, setSaving] = useState(false)\n const [pauseMenu, setPauseMenu] = useState<{\n anchor: HTMLElement\n row: OrgAutomationRow\n } | null>(null)\n\n const entitled = checkEntitlement(org, 'actions')\n const fail = useCallback(\n (error: unknown) =>\n enqueueSnackbar((error as Error)?.message || 'The request could not be completed', {\n variant: 'warning',\n persist: false,\n }),\n [enqueueSnackbar],\n )\n\n const refuseUnentitled = useCallback(() => {\n enqueueSnackbar(\n 'Org automations are built from the actions builder, which requires a Pro plan — see Billing to upgrade',\n { variant: 'warning', persist: false },\n )\n }, [enqueueSnackbar])\n\n const handleAdd = useCallback(() => {\n if (!entitled) return refuseUnentitled()\n setDraft(newOrgAutomationDraft())\n }, [entitled, refuseUnentitled])\n\n /*\n * For the `orgAutomations` zone (AGL-3603): an automation another plugin\n * proposed opens in the editor as a new one, unsaved and switched off —\n * the same door Add org automation opens, so the plan is asked first and\n * Save is still the only write.\n */\n const propose = useCallback(\n (proposal: OrgAutomationProposal) => {\n if (!entitled) {\n refuseUnentitled()\n return false\n }\n const opened = orgAutomationDraftFromProposal(proposal)\n if (!opened) return false\n setDraft(opened)\n return true\n },\n [entitled, refuseUnentitled],\n )\n const ExtensionZone = useConsoleWidgetSlot()\n const automationsZone =\n ExtensionZone && canEdit ? (\n <ExtensionZone\n slot={ORG_AUTOMATIONS_ZONE.id}\n orgId={mount.orgId}\n triggers={ORG_AUTOMATION_TRIGGER_EVENTS}\n steps={ORG_AUTOMATION_STEP_TYPES}\n propose={propose}\n />\n ) : null\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n const body = orgAutomationBody(draft)\n // The route's own reader, asked first so a mistake reads the same here as\n // it would coming back from the server — and costs no round trip.\n const read = readOrgAutomation(body)\n if (read.ok === false) {\n return void enqueueSnackbar(read.problem, { variant: 'warning', persist: false })\n }\n setSaving(true)\n try {\n await api('manage', {\n orgId: mount.orgId,\n action: draft.id ? 'update' : 'create',\n ...(draft.id ? { automationId: draft.id } : {}),\n automation: body,\n })\n setDraft(null)\n enqueueSnackbar('Org automation saved', { variant: 'success', persist: false })\n } catch (error) {\n fail(error)\n } finally {\n setSaving(false)\n }\n }, [draft, api, mount.orgId, enqueueSnackbar, fail])\n\n const handleToggle = useCallback(\n (row: OrgAutomationRow) => async (event: { target: { checked: boolean } }) => {\n try {\n await api('manage', {\n orgId: mount.orgId,\n action: 'setEnabled',\n automationId: row.$id,\n enabled: event.target.checked,\n })\n } catch (error) {\n fail(error)\n }\n },\n [api, mount.orgId, fail],\n )\n\n const handleDelete = useCallback(\n (row: OrgAutomationRow) => async () => {\n const confirmed = await confirm({\n title: 'Delete this org automation?',\n description:\n `\"${row.name}\" stops running on every site it is placed on, and ` +\n 'anyone waiting inside it stops too.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n try {\n await api('manage', {\n orgId: mount.orgId,\n action: 'delete',\n automationId: row.$id,\n })\n } catch (error) {\n fail(error)\n }\n },\n [confirm, api, mount.orgId, fail],\n )\n\n const setPaused = useCallback(\n async (row: OrgAutomationRow, hostId: string, paused: boolean) => {\n setPauseMenu(null)\n try {\n await api('pause', { hostId, automationId: row.$id, paused })\n } catch (error) {\n fail(error)\n }\n },\n [api, fail],\n )\n\n return (\n <CardDisplay\n header={'Org automations'}\n help={pluginDocsHelp('orgAutomations', { anchor: '#what-an-org-automation-is' })}\n // Other ways to start an org automation (AGL-3603), in the header.\n actions={automationsZone ?? undefined}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Write an automation once and run it on every site you choose. ' +\n 'Each run is that site’s own: its email goes from that site, it ' +\n 'counts on that site’s action runs, and the site can pause it ' +\n 'for itself. Pro plans and up.'}\n </Typography>\n {shown.map((row) => {\n const paused = orgAutomationPausedHostIds(row)\n const placed = placedSiteIds(row, mount)\n const pausable = placed.filter((hostId) => !paused.includes(hostId))\n return (\n <Stack\n key={row.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Switch\n size=\"small\"\n checked={row.enabled !== false}\n disabled={!canEdit}\n onChange={handleToggle(row)}\n slotProps={{ input: { 'aria-label': `Switch ${row.name} on or off` } }}\n />\n <Stack sx={{ flex: 1, minWidth: 0 }} spacing={0.25}>\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`on ${hostEventLabel(row.trigger?.event)} · ${(row.steps ?? [])\n .map(\n (step) =>\n HOST_ACTION_STEP_LABELS[step.type as HostActionStepType] ??\n step.type,\n )\n .join(' → ')}`}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {placementLine(row, mount)}\n </Typography>\n {paused.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ flexWrap: 'wrap', gap: 0.5 }}>\n {paused.map((hostId) => (\n <Chip\n key={hostId}\n size=\"small\"\n variant=\"outlined\"\n color=\"warning\"\n label={`Paused on ${orgSiteName(mount, hostId)}`}\n {...(canEdit\n ? { onDelete: () => void setPaused(row, hostId, false) }\n : {})}\n />\n ))}\n </Stack>\n ) : null}\n </Stack>\n {canEdit ? (\n <>\n <Button size=\"small\" onClick={() => setDraft(orgAutomationDraft(row))}>\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n disabled={!pausable.length}\n aria-haspopup=\"menu\"\n onClick={(event) =>\n setPauseMenu({ anchor: event.currentTarget, row })\n }\n >\n {'Pause on…'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(row)}>\n {'Delete'}\n </Button>\n </>\n ) : null}\n </Stack>\n )\n })}\n {automations.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No org automations yet.'}\n </Typography>\n ) : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={shown.length}\n count={automations.length}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n {(rows?.length ?? 0) > ORG_AUTOMATIONS_MAX ? (\n <Alert severity=\"info\">\n {`Showing ${ORG_AUTOMATIONS_MAX} org automations, the most an ` +\n 'organization holds.'}\n </Alert>\n ) : null}\n {canEdit ? (\n <Button\n size=\"small\"\n color=\"primary\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={handleAdd}\n >\n {'Add org automation'}\n </Button>\n ) : null}\n </Stack>\n <Menu\n anchorEl={pauseMenu?.anchor ?? null}\n open={Boolean(pauseMenu)}\n onClose={() => setPauseMenu(null)}\n >\n {pauseMenu\n ? placedSiteIds(pauseMenu.row, mount)\n .filter(\n (hostId) =>\n !orgAutomationPausedHostIds(pauseMenu.row).includes(hostId),\n )\n .map((hostId) => (\n <MenuItem\n key={hostId}\n onClick={() => void setPaused(pauseMenu.row, hostId, true)}\n >\n {`Pause on ${orgSiteName(mount, hostId)}`}\n </MenuItem>\n ))\n : null}\n </Menu>\n <OrgAutomationEditor\n mount={mount}\n draft={draft}\n onDraft={(update) =>\n setDraft((previous) => (previous ? update(previous) : previous))\n }\n opened={editorOpened}\n saving={saving}\n onSave={() => void handleSave()}\n onClose={() => setDraft(null)}\n />\n </CardDisplay>\n )\n}\nOrgAutomationsCard.displayName = 'OrgAutomationsCard'\n\nexport default OrgAutomationsCard\n"],"names":["checkEntitlement","hostEventLabel","pluginDocsHelp","hostIdsFromScope","isOrgWideScope","useConsoleWidgetSlot","CardDisplay","useConfirmationContext","ListPagination","TABLE_PAGE_SIZE_DEFAULT","useSnackbar","useFirestore","useFirestoreCollection","Alert","Button","Chip","Menu","MenuItem","Stack","Switch","Typography","collection","documentId","limit","orderBy","query","where","useCallback","useMemo","useState","ORG_AUTOMATION_STEP_TYPES","ORG_AUTOMATION_TRIGGER_EVENTS","ORG_AUTOMATIONS_COLLECTION","ORG_AUTOMATIONS_MAX","orgAutomationPausedHostIds","readOrgAutomation","newOrgAutomationDraft","OrgAutomationEditor","orgAutomationBody","orgAutomationDraft","orgAutomationDraftFromProposal","ORG_AUTOMATIONS_ZONE","useOrgAutomationsApi","orgSiteName","HOST_ACTION_STEP_LABELS","placedSiteIds","row","mount","visibleTo","hosts","map","host","id","placementLine","names","length","join","OrgAutomationsCard","props","org","canEdit","firestore","api","enqueueSnackbar","confirm","data","rows","orgId","idField","automations","slice","sort","a","b","String","name","localeCompare","page","setPage","pageSize","setPageSize","shown","draft","setDraft","editorOpened","setEditorOpened","saving","setSaving","pauseMenu","setPauseMenu","entitled","fail","error","message","variant","persist","refuseUnentitled","handleAdd","propose","proposal","opened","ExtensionZone","automationsZone","slot","triggers","steps","handleSave","body","read","ok","problem","action","automationId","automation","handleToggle","event","$id","enabled","target","checked","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","setPaused","hostId","paused","header","help","anchor","actions","undefined","contentGutterX","contentGutterY","spacing","placed","pausable","filter","includes","direction","sx","alignItems","size","disabled","onChange","slotProps","input","flex","minWidth","noWrap","trigger","step","type","flexWrap","gap","label","onDelete","onClick","aria-haspopup","currentTarget","rowCount","count","onPageChange","onPageSizeChange","severity","alignSelf","anchorEl","open","Boolean","onClose","onDraft","update","previous","onSave","displayName"],"mappings":"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"}
@@ -57,7 +57,8 @@ export interface AutomationTarget {
57
57
  /**
58
58
  * The editor of one SAVED automation — an action or a workflow — on the
59
59
  * Automation page (AGL-2919). A widget here reads the automation as it is
60
- * stored and changes nothing in the editor.
60
+ * stored and changes nothing in the editor; what it drafts is a new
61
+ * automation, which `openAction` opens.
61
62
  */
62
63
  export interface AutomationEditorZoneProps {
63
64
  hostId: string;
@@ -65,6 +66,13 @@ export interface AutomationEditorZoneProps {
65
66
  orgId: string | undefined;
66
67
  /** The automation the editor has open, as it is stored. */
67
68
  target: AutomationTarget;
69
+ /**
70
+ * Opens a listed action in the Actions editor in place of the one open —
71
+ * a changed copy a widget drafted, say — and answers `false` when the id
72
+ * names no action the list has read yet (AGL-3603). The Actions editor
73
+ * hands it; a workflow's editor does not.
74
+ */
75
+ openAction?: (actionId: string) => boolean;
68
76
  }
69
77
  /**
70
78
  * One FAILED run in an automation's run history (AGL-2919), drawn once per
@@ -81,5 +89,49 @@ export interface AutomationRunZoneProps {
81
89
  runId: string;
82
90
  }
83
91
  export declare const HOST_AUTOMATIONS_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<HostAutomationsZoneProps>;
92
+ /**
93
+ * The workspace's Org automations section, in its card's header (AGL-3603):
94
+ * another way to start an org automation, drawn for a member who may write
95
+ * them. `triggers` and `steps` are what an org automation may start on and
96
+ * hold — this plugin's own lists, handed over so a widget never restates
97
+ * them. `propose` opens an automation in the section's editor as a NEW one,
98
+ * unsaved and switched off, and answers `false` when the editor cannot take
99
+ * it. A widget here writes nothing: the editor's Save, through the section's
100
+ * route, is the write.
101
+ */
102
+ export interface OrgAutomationsZoneProps {
103
+ orgId: string;
104
+ triggers: readonly string[];
105
+ steps: readonly string[];
106
+ propose: (automation: OrgAutomationProposal) => boolean;
107
+ }
108
+ /** An automation a widget proposes to the Org automations editor. */
109
+ export interface OrgAutomationProposal {
110
+ name: string;
111
+ trigger: {
112
+ event: string;
113
+ conditions?: ReadonlyArray<{
114
+ field: string;
115
+ op: string;
116
+ value?: string;
117
+ }>;
118
+ combinator?: 'and' | 'or';
119
+ };
120
+ steps: ReadonlyArray<Record<string, unknown> & {
121
+ type: string;
122
+ }>;
123
+ }
124
+ export declare const ORG_AUTOMATIONS_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<OrgAutomationsZoneProps>;
84
125
  export declare const AUTOMATION_EDITOR_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<AutomationEditorZoneProps>;
85
126
  export declare const AUTOMATION_RUN_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<AutomationRunZoneProps>;
127
+ /**
128
+ * The query parameter that asks the Actions section to open one of its
129
+ * actions in the editor once its list has read it (AGL-3603) — how the
130
+ * Workflows section's `openAction` hands over an action a widget drafted
131
+ * there, since the action lives in the other section.
132
+ */
133
+ export declare const OPEN_ACTION_PARAM = "action";
134
+ /** The Actions section's address, opening `actionId` in its editor. */
135
+ export declare function openActionHref(actionsHref: string, actionId: string): string;
136
+ /** The action the address asks the Actions section to open, or `null`. */
137
+ export declare function requestedActionId(search: string): string | null;
@@ -16,7 +16,21 @@
16
16
  */ import { definePluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
17
17
  export const WORKFLOW_USAGE_ZONE = definePluginZone('workflowUsage');
18
18
  export const HOST_AUTOMATIONS_ZONE = definePluginZone('hostAutomations');
19
+ export const ORG_AUTOMATIONS_ZONE = definePluginZone('orgAutomations');
19
20
  export const AUTOMATION_EDITOR_ZONE = definePluginZone('automationEditor');
20
21
  export const AUTOMATION_RUN_ZONE = definePluginZone('automationRun');
22
+ /**
23
+ * The query parameter that asks the Actions section to open one of its
24
+ * actions in the editor once its list has read it (AGL-3603) — how the
25
+ * Workflows section's `openAction` hands over an action a widget drafted
26
+ * there, since the action lives in the other section.
27
+ */ export const OPEN_ACTION_PARAM = 'action';
28
+ /** The Actions section's address, opening `actionId` in its editor. */ export function openActionHref(actionsHref, actionId) {
29
+ return `${actionsHref}?${OPEN_ACTION_PARAM}=${encodeURIComponent(actionId)}`;
30
+ }
31
+ /** The action the address asks the Actions section to open, or `null`. */ export function requestedActionId(search) {
32
+ const id = new URLSearchParams(search).get(OPEN_ACTION_PARAM);
33
+ return id && /^[A-Za-z0-9_-]{1,128}$/.test(id) ? id : null;
34
+ }
21
35
 
22
36
  //# sourceMappingURL=workflow-zones.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/workflow-zones.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { WhereUsedResult } from '@aglyn/aglyn/app-utils/where-used'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * Where a workflow is used, shown by whichever plugin draws references.\n *\n * The Automation page asks the platform's where-used scan what a workflow\n * computes and holds the answer. Laying that answer out — the rows, the deep\n * links into a published version, the flag on a legacy name token — is the\n * same dialog a variable and a function open, and it belongs to the plugin\n * that owns those. This page hosts the zone and hands it the answer.\n */\nexport interface WorkflowUsageZoneProps {\n hostId: string\n /** The scan this page already ran; `null` closes whatever is drawn. */\n usage: { name: string; result: WhereUsedResult } | null\n onClose: () => void\n}\n\nexport const WORKFLOW_USAGE_ZONE =\n definePluginZone<WorkflowUsageZoneProps>('workflowUsage')\n\n/**\n * The Automation page's Actions, beside Add action and Recipes (AGL-2919):\n * another way to start an automation. `openAction` opens a listed action in\n * the Actions editor, and answers `false` when the id names no action the\n * list has read yet — which is how a widget whose draft was just written\n * tells a list that has not caught up from one that opened it. A widget here\n * writes nothing through the page.\n */\nexport interface HostAutomationsZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n openAction: (actionId: string) => boolean\n}\n\n/** One automation on the Automation page: an action, or a workflow (AGL-2919). */\nexport interface AutomationTarget {\n type: 'action' | 'workflow'\n id: string\n name: string\n}\n\n/**\n * The editor of one SAVED automation — an action or a workflow — on the\n * Automation page (AGL-2919). A widget here reads the automation as it is\n * stored and changes nothing in the editor.\n */\nexport interface AutomationEditorZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the editor has open, as it is stored. */\n target: AutomationTarget\n}\n\n/**\n * One FAILED run in an automation's run history (AGL-2919), drawn once per\n * failed row. A widget here reads the run as it was recorded and changes\n * nothing.\n */\nexport interface AutomationRunZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the run belongs to. */\n target: AutomationTarget\n /** The run's entry in the site's activity log. */\n runId: string\n}\n\nexport const HOST_AUTOMATIONS_ZONE =\n definePluginZone<HostAutomationsZoneProps>('hostAutomations')\n\nexport const AUTOMATION_EDITOR_ZONE =\n definePluginZone<AutomationEditorZoneProps>('automationEditor')\n\nexport const AUTOMATION_RUN_ZONE =\n definePluginZone<AutomationRunZoneProps>('automationRun')\n"],"names":["definePluginZone","WORKFLOW_USAGE_ZONE","HOST_AUTOMATIONS_ZONE","AUTOMATION_EDITOR_ZONE","AUTOMATION_RUN_ZONE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,gBAAgB,QAAQ,2CAA0C;AAkB3E,OAAO,MAAMC,sBACXD,iBAAyC,iBAAgB;AAoD3D,OAAO,MAAME,wBACXF,iBAA2C,mBAAkB;AAE/D,OAAO,MAAMG,yBACXH,iBAA4C,oBAAmB;AAEjE,OAAO,MAAMI,sBACXJ,iBAAyC,iBAAgB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/workflow-zones.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { WhereUsedResult } from '@aglyn/aglyn/app-utils/where-used'\nimport { definePluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\n\n/**\n * Where a workflow is used, shown by whichever plugin draws references.\n *\n * The Automation page asks the platform's where-used scan what a workflow\n * computes and holds the answer. Laying that answer out — the rows, the deep\n * links into a published version, the flag on a legacy name token — is the\n * same dialog a variable and a function open, and it belongs to the plugin\n * that owns those. This page hosts the zone and hands it the answer.\n */\nexport interface WorkflowUsageZoneProps {\n hostId: string\n /** The scan this page already ran; `null` closes whatever is drawn. */\n usage: { name: string; result: WhereUsedResult } | null\n onClose: () => void\n}\n\nexport const WORKFLOW_USAGE_ZONE =\n definePluginZone<WorkflowUsageZoneProps>('workflowUsage')\n\n/**\n * The Automation page's Actions, beside Add action and Recipes (AGL-2919):\n * another way to start an automation. `openAction` opens a listed action in\n * the Actions editor, and answers `false` when the id names no action the\n * list has read yet — which is how a widget whose draft was just written\n * tells a list that has not caught up from one that opened it. A widget here\n * writes nothing through the page.\n */\nexport interface HostAutomationsZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n openAction: (actionId: string) => boolean\n}\n\n/** One automation on the Automation page: an action, or a workflow (AGL-2919). */\nexport interface AutomationTarget {\n type: 'action' | 'workflow'\n id: string\n name: string\n}\n\n/**\n * The editor of one SAVED automation — an action or a workflow — on the\n * Automation page (AGL-2919). A widget here reads the automation as it is\n * stored and changes nothing in the editor; what it drafts is a new\n * automation, which `openAction` opens.\n */\nexport interface AutomationEditorZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the editor has open, as it is stored. */\n target: AutomationTarget\n /**\n * Opens a listed action in the Actions editor in place of the one open —\n * a changed copy a widget drafted, say — and answers `false` when the id\n * names no action the list has read yet (AGL-3603). The Actions editor\n * hands it; a workflow's editor does not.\n */\n openAction?: (actionId: string) => boolean\n}\n\n/**\n * One FAILED run in an automation's run history (AGL-2919), drawn once per\n * failed row. A widget here reads the run as it was recorded and changes\n * nothing.\n */\nexport interface AutomationRunZoneProps {\n hostId: string\n /** The org the page names; `undefined` while it resolves. */\n orgId: string | undefined\n /** The automation the run belongs to. */\n target: AutomationTarget\n /** The run's entry in the site's activity log. */\n runId: string\n}\n\nexport const HOST_AUTOMATIONS_ZONE =\n definePluginZone<HostAutomationsZoneProps>('hostAutomations')\n\n/**\n * The workspace's Org automations section, in its card's header (AGL-3603):\n * another way to start an org automation, drawn for a member who may write\n * them. `triggers` and `steps` are what an org automation may start on and\n * hold — this plugin's own lists, handed over so a widget never restates\n * them. `propose` opens an automation in the section's editor as a NEW one,\n * unsaved and switched off, and answers `false` when the editor cannot take\n * it. A widget here writes nothing: the editor's Save, through the section's\n * route, is the write.\n */\nexport interface OrgAutomationsZoneProps {\n orgId: string\n triggers: readonly string[]\n steps: readonly string[]\n propose: (automation: OrgAutomationProposal) => boolean\n}\n\n/** An automation a widget proposes to the Org automations editor. */\nexport interface OrgAutomationProposal {\n name: string\n trigger: {\n event: string\n conditions?: ReadonlyArray<{ field: string; op: string; value?: string }>\n combinator?: 'and' | 'or'\n }\n steps: ReadonlyArray<Record<string, unknown> & { type: string }>\n}\n\nexport const ORG_AUTOMATIONS_ZONE =\n definePluginZone<OrgAutomationsZoneProps>('orgAutomations')\n\nexport const AUTOMATION_EDITOR_ZONE =\n definePluginZone<AutomationEditorZoneProps>('automationEditor')\n\nexport const AUTOMATION_RUN_ZONE =\n definePluginZone<AutomationRunZoneProps>('automationRun')\n\n/**\n * The query parameter that asks the Actions section to open one of its\n * actions in the editor once its list has read it (AGL-3603) — how the\n * Workflows section's `openAction` hands over an action a widget drafted\n * there, since the action lives in the other section.\n */\nexport const OPEN_ACTION_PARAM = 'action'\n\n/** The Actions section's address, opening `actionId` in its editor. */\nexport function openActionHref(actionsHref: string, actionId: string): string {\n return `${actionsHref}?${OPEN_ACTION_PARAM}=${encodeURIComponent(actionId)}`\n}\n\n/** The action the address asks the Actions section to open, or `null`. */\nexport function requestedActionId(search: string): string | null {\n const id = new URLSearchParams(search).get(OPEN_ACTION_PARAM)\n return id && /^[A-Za-z0-9_-]{1,128}$/.test(id) ? id : null\n}\n"],"names":["definePluginZone","WORKFLOW_USAGE_ZONE","HOST_AUTOMATIONS_ZONE","ORG_AUTOMATIONS_ZONE","AUTOMATION_EDITOR_ZONE","AUTOMATION_RUN_ZONE","OPEN_ACTION_PARAM","openActionHref","actionsHref","actionId","encodeURIComponent","requestedActionId","search","id","URLSearchParams","get","test"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,gBAAgB,QAAQ,2CAA0C;AAkB3E,OAAO,MAAMC,sBACXD,iBAAyC,iBAAgB;AA4D3D,OAAO,MAAME,wBACXF,iBAA2C,mBAAkB;AA8B/D,OAAO,MAAMG,uBACXH,iBAA0C,kBAAiB;AAE7D,OAAO,MAAMI,yBACXJ,iBAA4C,oBAAmB;AAEjE,OAAO,MAAMK,sBACXL,iBAAyC,iBAAgB;AAE3D;;;;;CAKC,GACD,OAAO,MAAMM,oBAAoB,SAAQ;AAEzC,qEAAqE,GACrE,OAAO,SAASC,eAAeC,WAAmB,EAAEC,QAAgB;IAClE,OAAO,GAAGD,YAAY,CAAC,EAAEF,kBAAkB,CAAC,EAAEI,mBAAmBD,WAAW;AAC9E;AAEA,wEAAwE,GACxE,OAAO,SAASE,kBAAkBC,MAAc;IAC9C,MAAMC,KAAK,IAAIC,gBAAgBF,QAAQG,GAAG,CAACT;IAC3C,OAAOO,MAAM,yBAAyBG,IAAI,CAACH,MAAMA,KAAK;AACxD"}
@@ -34,7 +34,7 @@ import { workflowsOrgMount } from "./workflows-org-mount.js";
34
34
  * would CONSTRUCT all three every render, and each card opens its Firestore
35
35
  * listens on mount — which is the entire cost this page exists to stop paying.
36
36
  * Only the returned branch is ever built.
37
- */ function sectionBody(section, hostId, org) {
37
+ */ function sectionBody(section, hostId, org, /** The Actions section's address, where a drafted action is opened. */ actionsHref) {
38
38
  switch(section){
39
39
  case 'workflows':
40
40
  return(/*
@@ -52,7 +52,8 @@ import { workflowsOrgMount } from "./workflows-org-mount.js";
52
52
  }),
53
53
  /*#__PURE__*/ _jsx(HostWorkflowsCard, {
54
54
  hostId: hostId,
55
- org: org
55
+ org: org,
56
+ actionsHref: actionsHref
56
57
  })
57
58
  ]
58
59
  }));
@@ -148,6 +149,7 @@ import { workflowsOrgMount } from "./workflows-org-mount.js";
148
149
  * site and an ACTION belongs to the site too; an interaction belongs to the
149
150
  * document that holds it and is edited on the element, not here.
150
151
  */ export function WorkflowsConsolePage(props) {
152
+ var _sections_find;
151
153
  const { hostId, orgMount, org, permissions, section, sections, basePath } = props;
152
154
  /*
153
155
  * Mounted with no site: the organization's hub (AGL-3302). Memoised so the
@@ -176,7 +178,7 @@ import { workflowsOrgMount } from "./workflows-org-mount.js";
176
178
  }
177
179
  return /*#__PURE__*/ _jsx(HubSections, {
178
180
  sections: sections,
179
- children: sectionBody(section, hostId, org)
181
+ children: sectionBody(section, hostId, org, (_sections_find = sections.find((one)=>one.id === 'actions')) == null ? void 0 : _sections_find.href)
180
182
  });
181
183
  }
182
184
  WorkflowsConsolePage.displayName = 'WorkflowsConsolePage';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/workflows-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { AglynOrgBilling, ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { Stack } from '@mui/material'\nimport { useMemo, type ReactNode } from 'react'\nimport HostActionsCard from './host-actions-card.component'\nimport HostWebhooksCard from './host-webhooks-card.component'\nimport HostWorkflowsCard from './host-workflows-card.component'\nimport OrgAutomationsCard from './org-automations-card.component'\nimport OrgSiteAutomationList from './org-site-automation-list.component'\nimport RunQuotaLine from './run-quota-line.component'\nimport SiteOrgAutomationsPanel from './site-org-automations-panel.component'\nimport type { WorkflowsConsoleSectionId } from './workflows-console-sections'\nimport {\n workflowsOrgMount,\n type WorkflowsOrgMount,\n} from './workflows-org-mount'\n\n/**\n * The body of one workflows section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT all three every render, and each card opens its Firestore\n * listens on mount — which is the entire cost this page exists to stop paying.\n * Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: WorkflowsConsoleSectionId,\n hostId: string,\n org: Partial<AglynOrgBilling> | undefined,\n): ReactNode {\n switch (section) {\n case 'workflows':\n return (\n /*\n * `N runs this month · M included` (AGL-2171), which the run-history\n * mockup puts opposite the heading. The quota silently stops\n * automations running once it is reached, and the only place it was\n * reported was the Billing page.\n */\n <Stack spacing={1}>\n <RunQuotaLine hostId={hostId} org={org} counter=\"workflowRuns\" />\n <HostWorkflowsCard hostId={hostId} org={org} />\n </Stack>\n )\n case 'actions':\n return (\n <Stack spacing={1}>\n {/* `actionRunsPerMonth` had NO customer-facing surface at all\n before this — staff panel and usage-alerts only. */}\n <RunQuotaLine hostId={hostId} org={org} counter=\"actionRuns\" />\n <HostActionsCard hostId={hostId} org={org} />\n {/* What the organization runs on this site beside the site's own\n actions, and the site's pause for each (AGL-3302). */}\n <SiteOrgAutomationsPanel hostId={hostId} />\n </Stack>\n )\n case 'webhooks':\n return <HostWebhooksCard hostId={hostId} org={org} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section (AGL-3302): the org automations,\n * and every site's own workflows, actions and webhooks listed with the site\n * each belongs to.\n *\n * The run allowance line heads the workflows and actions lists here too: the\n * allowance is the workspace's (AGL-3472), every site's runs spend it, and\n * this is the page that lists them all.\n */\nfunction orgSectionBody(\n section: WorkflowsConsoleSectionId,\n mount: WorkflowsOrgMount,\n org: Partial<AglynOrgBilling> | undefined,\n canEdit: boolean,\n): ReactNode {\n switch (section) {\n case 'automations':\n return <OrgAutomationsCard mount={mount} org={org} canEdit={canEdit} />\n case 'workflows':\n case 'actions':\n return (\n <Stack spacing={1}>\n <RunQuotaLine\n hostId={null}\n orgId={mount.orgId}\n org={org}\n counter={section === 'workflows' ? 'workflowRuns' : 'actionRuns'}\n />\n <OrgSiteAutomationList mount={mount} kind={section} canRead />\n </Stack>\n )\n case 'webhooks':\n // A webhook holds its site's secret: the rules admit a site's admins\n // and editors, which every org-wide member but a viewer is.\n return (\n <OrgSiteAutomationList mount={mount} kind=\"webhooks\" canRead={canEdit} />\n )\n default:\n return null\n }\n}\n\n/**\n * Automation page (AGL-101/148/149 → AGL-395): the automation surface —\n * workflow builder, event-triggered actions, and webhooks — owned by the\n * workflows plugin and rendered by the shell's generic plugin route. Each card\n * runs its own entitlement check (workflows / actions / webhooks are distinct\n * plan flags), so the shell's resolved `org` doc flows into all three.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `workflows-console-read-cost.spec.tsx` was\n * written BEFORE the conversion precisely to hold that line, and reports the\n * same counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag.\n *\n * The section ids are unchanged by the Automation rename: they are the\n * addresses people bookmark and the docs link to. A workflow belongs to the\n * site and an ACTION belongs to the site too; an interaction belongs to the\n * document that holds it and is edited on the element, not here.\n */\nexport function WorkflowsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, permissions, section, sections, basePath } =\n props\n /*\n * Mounted with no site: the organization's hub (AGL-3302). Memoised so the\n * cards below, which key their listeners on the mount's org, see one object.\n */\n const mount = useMemo(\n () => (hostId == null ? workflowsOrgMount(orgMount, basePath) : null),\n [hostId, orgMount, basePath],\n )\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n */\n if (!section || !sections?.length || !basePath) return null\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!mount) return null\n return (\n <HubSections sections={sections}>\n {orgSectionBody(\n section as WorkflowsConsoleSectionId,\n mount,\n org,\n // An org viewer's `editHosts` is false; every other org role's is\n // true. The server routes decide — this keeps a viewer from being\n // offered controls that are about to refuse them.\n permissions?.editHosts !== false,\n )}\n </HubSections>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(section as WorkflowsConsoleSectionId, hostId, org)}\n </HubSections>\n )\n}\nWorkflowsConsolePage.displayName = 'WorkflowsConsolePage'\n\nexport default WorkflowsConsolePage\n"],"names":["HubSections","Stack","useMemo","HostActionsCard","HostWebhooksCard","HostWorkflowsCard","OrgAutomationsCard","OrgSiteAutomationList","RunQuotaLine","SiteOrgAutomationsPanel","workflowsOrgMount","sectionBody","section","hostId","org","spacing","counter","orgSectionBody","mount","canEdit","orgId","kind","canRead","WorkflowsConsolePage","props","orgMount","permissions","sections","basePath","length","editHosts","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,WAAW,QAAQ,wBAAuB;AACnD,SAASC,KAAK,QAAQ,gBAAe;AACrC,SAASC,OAAO,QAAwB,QAAO;AAC/C,OAAOC,qBAAqB,mCAA+B;AAC3D,OAAOC,sBAAsB,oCAAgC;AAC7D,OAAOC,uBAAuB,qCAAiC;AAC/D,OAAOC,wBAAwB,sCAAkC;AACjE,OAAOC,2BAA2B,0CAAsC;AACxE,OAAOC,kBAAkB,gCAA4B;AACrD,OAAOC,6BAA6B,4CAAwC;AAE5E,SACEC,iBAAiB,QAEZ,2BAAuB;AAE9B;;;;;;;;CAQC,GACD,SAASC,YACPC,OAAkC,EAClCC,MAAc,EACdC,GAAyC;IAEzC,OAAQF;QACN,KAAK;YACH,OACE;;;;;SAKC,iBACD,MAACX;gBAAMc,SAAS;;kCACd,KAACP;wBAAaK,QAAQA;wBAAQC,KAAKA;wBAAKE,SAAQ;;kCAChD,KAACX;wBAAkBQ,QAAQA;wBAAQC,KAAKA;;;;QAG9C,KAAK;YACH,qBACE,MAACb;gBAAMc,SAAS;;kCAGd,KAACP;wBAAaK,QAAQA;wBAAQC,KAAKA;wBAAKE,SAAQ;;kCAChD,KAACb;wBAAgBU,QAAQA;wBAAQC,KAAKA;;kCAGtC,KAACL;wBAAwBI,QAAQA;;;;QAGvC,KAAK;YACH,qBAAO,KAACT;gBAAiBS,QAAQA;gBAAQC,KAAKA;;QAChD;YACE,OAAO;IACX;AACF;AAEA;;;;;;;;CAQC,GACD,SAASG,eACPL,OAAkC,EAClCM,KAAwB,EACxBJ,GAAyC,EACzCK,OAAgB;IAEhB,OAAQP;QACN,KAAK;YACH,qBAAO,KAACN;gBAAmBY,OAAOA;gBAAOJ,KAAKA;gBAAKK,SAASA;;QAC9D,KAAK;QACL,KAAK;YACH,qBACE,MAAClB;gBAAMc,SAAS;;kCACd,KAACP;wBACCK,QAAQ;wBACRO,OAAOF,MAAME,KAAK;wBAClBN,KAAKA;wBACLE,SAASJ,YAAY,cAAc,iBAAiB;;kCAEtD,KAACL;wBAAsBW,OAAOA;wBAAOG,MAAMT;wBAASU,OAAO;;;;QAGjE,KAAK;YACH,qEAAqE;YACrE,4DAA4D;YAC5D,qBACE,KAACf;gBAAsBW,OAAOA;gBAAOG,MAAK;gBAAWC,SAASH;;QAElE;YACE,OAAO;IACX;AACF;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASI,qBAAqBC,KAA6B;IAChE,MAAM,EAAEX,MAAM,EAAEY,QAAQ,EAAEX,GAAG,EAAEY,WAAW,EAAEd,OAAO,EAAEe,QAAQ,EAAEC,QAAQ,EAAE,GACvEJ;IACF;;;GAGC,GACD,MAAMN,QAAQhB,QACZ,IAAOW,UAAU,OAAOH,kBAAkBe,UAAUG,YAAY,MAChE;QAACf;QAAQY;QAAUG;KAAS;IAG9B;;;;;GAKC,GACD,IAAI,CAAChB,WAAW,EAACe,4BAAAA,SAAUE,MAAM,KAAI,CAACD,UAAU,OAAO;IAEvD,IAAIf,UAAU,MAAM;QAClB,sEAAsE;QACtE,IAAI,CAACK,OAAO,OAAO;QACnB,qBACE,KAAClB;YAAY2B,UAAUA;sBACpBV,eACCL,SACAM,OACAJ,KACA,kEAAkE;YAClE,kEAAkE;YAClE,kDAAkD;YAClDY,CAAAA,+BAAAA,YAAaI,SAAS,MAAK;;IAInC;IAEA,qBACE,KAAC9B;QAAY2B,UAAUA;kBACpBhB,YAAYC,SAAsCC,QAAQC;;AAGjE;AACAS,qBAAqBQ,WAAW,GAAG;AAEnC,eAAeR,qBAAoB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/workflows/src/lib/components/workflows-console-page.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport type { AglynOrgBilling, ConsolePluginPageProps } from '@aglyn/aglyn'\nimport { HubSections } from '@aglyn/shared-ui-next'\nimport { Stack } from '@mui/material'\nimport { useMemo, type ReactNode } from 'react'\nimport HostActionsCard from './host-actions-card.component'\nimport HostWebhooksCard from './host-webhooks-card.component'\nimport HostWorkflowsCard from './host-workflows-card.component'\nimport OrgAutomationsCard from './org-automations-card.component'\nimport OrgSiteAutomationList from './org-site-automation-list.component'\nimport RunQuotaLine from './run-quota-line.component'\nimport SiteOrgAutomationsPanel from './site-org-automations-panel.component'\nimport type { WorkflowsConsoleSectionId } from './workflows-console-sections'\nimport {\n workflowsOrgMount,\n type WorkflowsOrgMount,\n} from './workflows-org-mount'\n\n/**\n * The body of one workflows section, built only when that section is the one\n * being read (AGL-2501).\n *\n * A function rather than a map of nodes on purpose: a `Record<id, ReactNode>`\n * would CONSTRUCT all three every render, and each card opens its Firestore\n * listens on mount — which is the entire cost this page exists to stop paying.\n * Only the returned branch is ever built.\n */\nfunction sectionBody(\n section: WorkflowsConsoleSectionId,\n hostId: string,\n org: Partial<AglynOrgBilling> | undefined,\n /** The Actions section's address, where a drafted action is opened. */\n actionsHref?: string,\n): ReactNode {\n switch (section) {\n case 'workflows':\n return (\n /*\n * `N runs this month · M included` (AGL-2171), which the run-history\n * mockup puts opposite the heading. The quota silently stops\n * automations running once it is reached, and the only place it was\n * reported was the Billing page.\n */\n <Stack spacing={1}>\n <RunQuotaLine hostId={hostId} org={org} counter=\"workflowRuns\" />\n <HostWorkflowsCard\n hostId={hostId}\n org={org}\n actionsHref={actionsHref}\n />\n </Stack>\n )\n case 'actions':\n return (\n <Stack spacing={1}>\n {/* `actionRunsPerMonth` had NO customer-facing surface at all\n before this — staff panel and usage-alerts only. */}\n <RunQuotaLine hostId={hostId} org={org} counter=\"actionRuns\" />\n <HostActionsCard hostId={hostId} org={org} />\n {/* What the organization runs on this site beside the site's own\n actions, and the site's pause for each (AGL-3302). */}\n <SiteOrgAutomationsPanel hostId={hostId} />\n </Stack>\n )\n case 'webhooks':\n return <HostWebhooksCard hostId={hostId} org={org} />\n default:\n return null\n }\n}\n\n/**\n * The body of one ORGANIZATION-level section (AGL-3302): the org automations,\n * and every site's own workflows, actions and webhooks listed with the site\n * each belongs to.\n *\n * The run allowance line heads the workflows and actions lists here too: the\n * allowance is the workspace's (AGL-3472), every site's runs spend it, and\n * this is the page that lists them all.\n */\nfunction orgSectionBody(\n section: WorkflowsConsoleSectionId,\n mount: WorkflowsOrgMount,\n org: Partial<AglynOrgBilling> | undefined,\n canEdit: boolean,\n): ReactNode {\n switch (section) {\n case 'automations':\n return <OrgAutomationsCard mount={mount} org={org} canEdit={canEdit} />\n case 'workflows':\n case 'actions':\n return (\n <Stack spacing={1}>\n <RunQuotaLine\n hostId={null}\n orgId={mount.orgId}\n org={org}\n counter={section === 'workflows' ? 'workflowRuns' : 'actionRuns'}\n />\n <OrgSiteAutomationList mount={mount} kind={section} canRead />\n </Stack>\n )\n case 'webhooks':\n // A webhook holds its site's secret: the rules admit a site's admins\n // and editors, which every org-wide member but a viewer is.\n return (\n <OrgSiteAutomationList mount={mount} kind=\"webhooks\" canRead={canEdit} />\n )\n default:\n return null\n }\n}\n\n/**\n * Automation page (AGL-101/148/149 → AGL-395): the automation surface —\n * workflow builder, event-triggered actions, and webhooks — owned by the\n * workflows plugin and rendered by the shell's generic plugin route. Each card\n * runs its own entitlement check (workflows / actions / webhooks are distinct\n * plan flags), so the shell's resolved `org` doc flows into all three.\n *\n * Sections are ROUTES (AGL-2501). `HubTabs lazy` already mounted one panel, so\n * this is not a read saving — `workflows-console-read-cost.spec.tsx` was\n * written BEFORE the conversion precisely to hold that line, and reports the\n * same counts after. What routing adds is that the URL names the section: it is\n * linkable, the back button walks sections, the breadcrumb says where you are,\n * and \"mount only what is open\" is structural rather than a `lazy` flag.\n *\n * The section ids are unchanged by the Automation rename: they are the\n * addresses people bookmark and the docs link to. A workflow belongs to the\n * site and an ACTION belongs to the site too; an interaction belongs to the\n * document that holds it and is edited on the element, not here.\n */\nexport function WorkflowsConsolePage(props: ConsolePluginPageProps) {\n const { hostId, orgMount, org, permissions, section, sections, basePath } =\n props\n /*\n * Mounted with no site: the organization's hub (AGL-3302). Memoised so the\n * cards below, which key their listeners on the mount's org, see one object.\n */\n const mount = useMemo(\n () => (hostId == null ? workflowsOrgMount(orgMount, basePath) : null),\n [hostId, orgMount, basePath],\n )\n\n /*\n * Nothing until the URL names a section. The shell redirects a bare hub URL\n * to the landing section and holds a spinner while it does, so this state is\n * transient — and rendering a default section here instead would pay for its\n * listens on a URL that is already being replaced.\n */\n if (!section || !sections?.length || !basePath) return null\n\n if (hostId == null) {\n // No site and no org to stand in for it: nothing this page can scope.\n if (!mount) return null\n return (\n <HubSections sections={sections}>\n {orgSectionBody(\n section as WorkflowsConsoleSectionId,\n mount,\n org,\n // An org viewer's `editHosts` is false; every other org role's is\n // true. The server routes decide — this keeps a viewer from being\n // offered controls that are about to refuse them.\n permissions?.editHosts !== false,\n )}\n </HubSections>\n )\n }\n\n return (\n <HubSections sections={sections}>\n {sectionBody(\n section as WorkflowsConsoleSectionId,\n hostId,\n org,\n sections.find((one) => one.id === 'actions')?.href,\n )}\n </HubSections>\n )\n}\nWorkflowsConsolePage.displayName = 'WorkflowsConsolePage'\n\nexport default WorkflowsConsolePage\n"],"names":["HubSections","Stack","useMemo","HostActionsCard","HostWebhooksCard","HostWorkflowsCard","OrgAutomationsCard","OrgSiteAutomationList","RunQuotaLine","SiteOrgAutomationsPanel","workflowsOrgMount","sectionBody","section","hostId","org","actionsHref","spacing","counter","orgSectionBody","mount","canEdit","orgId","kind","canRead","WorkflowsConsolePage","props","sections","orgMount","permissions","basePath","length","editHosts","find","one","id","href","displayName"],"mappings":"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"}
@@ -34,7 +34,6 @@ import { FieldValue } from "firebase-admin/firestore";
34
34
  import { runSummaryFields } from "../model/run-history.js";
35
35
  import { ACTION_MAX_EVENT_DEPTH, runWorkflow, WORKFLOW_MAX_STEPS } from "../model/workflows.js";
36
36
  import { describeStepOutcome } from "../model/step-outcomes.js";
37
- import { WEBHOOK_URL_PATTERN } from "../model/webhooks.js";
38
37
  import { eventRunSuspension } from "./site-suspension.js";
39
38
  import { triggeredDocsForEvent } from "./triggered-docs.js";
40
39
  import { resolveStepEmailMerge, stepEmailMergeContext } from "./email-merge.js";
@@ -297,7 +296,7 @@ export function makeWorkflowContextLoader(hostRef) {
297
296
  // Id-first lookup (AGL-261); the name query is the legacy path.
298
297
  const hookDoc = ((_step_webhookId = step.webhookId) == null ? void 0 : _step_webhookId.trim()) ? await hostRef.collection('webhooks').doc(step.webhookId.trim()).get() : (await hostRef.collection('webhooks').where('name', '==', (_ref3 = (_step_webhookName = step.webhookName) == null ? void 0 : _step_webhookName.trim()) != null ? _ref3 : '').limit(1).get()).docs[0];
299
298
  const hook = (hookDoc == null ? void 0 : hookDoc.exists) ? hookDoc.data() : undefined;
300
- if (!hook || hookDoc.get('deletedAt') || hook.enabled === false || hook.direction !== 'outbound' || !hook.url || !WEBHOOK_URL_PATTERN.test(hook.url)) {
299
+ if (!hook || hookDoc.get('deletedAt') || hook.enabled === false || hook.direction !== 'outbound' || !hook.url) {
301
300
  return failed(`unknown webhook "${step.webhookName || step.webhookId}"`);
302
301
  }
303
302
  const body = JSON.stringify({
@@ -308,11 +307,23 @@ export function makeWorkflowContextLoader(hostRef) {
308
307
  const signature = hook.secret ? createHmac('sha256', hook.secret).update(body).digest('hex') : '';
309
308
  // Two quick retries — serverless-friendly; longer retry queues
310
309
  // are a follow-up.
310
+ //
311
+ // The hook's URL is tenant-typed and the step runs on any visitor's
312
+ // form submission, so it goes out through the configured-URL fetch
313
+ // (AGL-3363), never a bare `fetch`: https only, the name resolved to
314
+ // PUBLIC addresses with the socket pinned to the one checked, and no
315
+ // redirect followed — a 3xx is a failed delivery. That is what refuses
316
+ // `[::1]`, `[::ffff:127.0.0.1]`, a decimal `2130706433`, a name that
317
+ // resolves inward, and a public host answering `302` to the metadata
318
+ // endpoint, none of which a pattern over the URL text can see.
319
+ //
320
+ // Loaded here, not at the top: see `webhook-delivery.ts`.
321
+ const { describeConfiguredUrlRefusal, fetchConfiguredPublicUrl } = await import("./webhook-delivery.js");
311
322
  let delivered = false;
312
323
  let lastStatus;
313
324
  for(let attempt = 0; attempt < 3 && !delivered; attempt += 1){
314
325
  try {
315
- const response = await fetch(hook.url, {
326
+ const result = await fetchConfiguredPublicUrl(hook.url, {
316
327
  method: 'POST',
317
328
  headers: _extends({
318
329
  'Content-Type': 'application/json'
@@ -322,8 +333,13 @@ export function makeWorkflowContextLoader(hostRef) {
322
333
  body,
323
334
  signal: AbortSignal.timeout(5000)
324
335
  });
325
- lastStatus = response.status;
326
- delivered = response.ok;
336
+ if ('refusal' in result) {
337
+ // A refusal is the URL's, not the network's: retrying it would
338
+ // only re-resolve the same answer.
339
+ return failed(`webhook "${step.webhookName || step.webhookId}" refused: ${describeConfiguredUrlRefusal(result.refusal)}`);
340
+ }
341
+ lastStatus = result.status;
342
+ delivered = result.status >= 200 && result.status < 300;
327
343
  } catch (unused) {
328
344
  // Retry below.
329
345
  }