@aglyn/plugins-workflows 1.0.0-beta.143

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 (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/package.json +53 -0
  4. package/src/index.d.ts +18 -0
  5. package/src/index.js +19 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/automation-step-fields.component.d.ts +74 -0
  8. package/src/lib/components/automation-step-fields.component.js +1210 -0
  9. package/src/lib/components/automation-step-fields.component.js.map +1 -0
  10. package/src/lib/components/host-actions-card.component.d.ts +15 -0
  11. package/src/lib/components/host-actions-card.component.js +1229 -0
  12. package/src/lib/components/host-actions-card.component.js.map +1 -0
  13. package/src/lib/components/host-activity-card.component.d.ts +84 -0
  14. package/src/lib/components/host-activity-card.component.js +280 -0
  15. package/src/lib/components/host-activity-card.component.js.map +1 -0
  16. package/src/lib/components/host-run-history-card.component.d.ts +40 -0
  17. package/src/lib/components/host-run-history-card.component.js +284 -0
  18. package/src/lib/components/host-run-history-card.component.js.map +1 -0
  19. package/src/lib/components/host-webhooks-card.component.d.ts +15 -0
  20. package/src/lib/components/host-webhooks-card.component.js +421 -0
  21. package/src/lib/components/host-webhooks-card.component.js.map +1 -0
  22. package/src/lib/components/host-workflows-card.component.d.ts +18 -0
  23. package/src/lib/components/host-workflows-card.component.js +952 -0
  24. package/src/lib/components/host-workflows-card.component.js.map +1 -0
  25. package/src/lib/components/run-quota-line.component.d.ts +27 -0
  26. package/src/lib/components/run-quota-line.component.js +72 -0
  27. package/src/lib/components/run-quota-line.component.js.map +1 -0
  28. package/src/lib/components/use-automation-step-pickers.d.ts +27 -0
  29. package/src/lib/components/use-automation-step-pickers.js +205 -0
  30. package/src/lib/components/use-automation-step-pickers.js.map +1 -0
  31. package/src/lib/components/workflow-zones.d.ts +36 -0
  32. package/src/lib/components/workflow-zones.js +19 -0
  33. package/src/lib/components/workflow-zones.js.map +1 -0
  34. package/src/lib/components/workflows-console-page.d.ts +25 -0
  35. package/src/lib/components/workflows-console-page.js +112 -0
  36. package/src/lib/components/workflows-console-page.js.map +1 -0
  37. package/src/lib/components/workflows-console-sections.d.ts +34 -0
  38. package/src/lib/components/workflows-console-sections.js +50 -0
  39. package/src/lib/components/workflows-console-sections.js.map +1 -0
  40. package/src/lib/constants/bundle-common.d.ts +8 -0
  41. package/src/lib/constants/bundle-common.js +9 -0
  42. package/src/lib/constants/bundle-common.js.map +1 -0
  43. package/src/lib/declarations.server.d.ts +41 -0
  44. package/src/lib/declarations.server.js +54 -0
  45. package/src/lib/declarations.server.js.map +1 -0
  46. package/src/lib/engine/crm-action-steps.d.ts +129 -0
  47. package/src/lib/engine/crm-action-steps.js +432 -0
  48. package/src/lib/engine/crm-action-steps.js.map +1 -0
  49. package/src/lib/engine/flow-enrollments.d.ts +264 -0
  50. package/src/lib/engine/flow-enrollments.js +386 -0
  51. package/src/lib/engine/flow-enrollments.js.map +1 -0
  52. package/src/lib/engine/run-event-actions.d.ts +232 -0
  53. package/src/lib/engine/run-event-actions.js +1392 -0
  54. package/src/lib/engine/run-event-actions.js.map +1 -0
  55. package/src/lib/engine/run-event-automations.d.ts +28 -0
  56. package/src/lib/engine/run-event-automations.js +37 -0
  57. package/src/lib/engine/run-event-automations.js.map +1 -0
  58. package/src/lib/engine/run-event-workflows.d.ts +75 -0
  59. package/src/lib/engine/run-event-workflows.js +311 -0
  60. package/src/lib/engine/run-event-workflows.js.map +1 -0
  61. package/src/lib/engine/workflow-steps.d.ts +107 -0
  62. package/src/lib/engine/workflow-steps.js +124 -0
  63. package/src/lib/engine/workflow-steps.js.map +1 -0
  64. package/src/lib/plugin.d.ts +33 -0
  65. package/src/lib/plugin.js +94 -0
  66. package/src/lib/plugin.js.map +1 -0
  67. package/src/lib/server-automation-drafts.d.ts +90 -0
  68. package/src/lib/server-automation-drafts.js +429 -0
  69. package/src/lib/server-automation-drafts.js.map +1 -0
  70. package/src/lib/server-console.d.ts +27 -0
  71. package/src/lib/server-console.js +35 -0
  72. package/src/lib/server-console.js.map +1 -0
  73. package/src/lib/server.d.ts +29 -0
  74. package/src/lib/server.js +298 -0
  75. package/src/lib/server.js.map +1 -0
@@ -0,0 +1,1229 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { ACTION_MAX_CONDITIONS, ACTION_MAX_STEPS, checkEntitlement, createResourceUid, CRM_ACTION_RECIPES, ELEMENT_SCOPED_SITE_EVENTS, HOST_ACTION_STEP_LABELS, HOST_EVENT_TYPES, hostActionDocument, hostActionRecipeId, hostEventLabel, hostEventPayloadHint, isSiteEventType, normalizeTriggerConditions, pluginDocsHelp, SITE_EVENT_TYPES, validateHostAction } from "@aglyn/aglyn";
20
+ import { automationPlaceholders, describeAutomationPlaceholder } from "@aglyn/aglyn/app-utils/automation-placeholders";
21
+ import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
22
+ import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
23
+ import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
24
+ import { TABLE_PAGE_SIZE_DEFAULT } from "@aglyn/shared-ui-jsx/const/table-pagination";
25
+ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
+ import { Timestamp } from "@aglyn/shared-util-timestamp";
27
+ import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, ListItemText, Menu, MenuItem, Stack, Switch, TextField, Typography } from "@mui/material";
28
+ import { collection, doc, setDoc, updateDoc } from "firebase/firestore";
29
+ import { useCallback, useMemo, useRef, useState } from "react";
30
+ import { ceilingedWindow, collectionCeiling, useFirestore, useFirestoreCollection, useHostResourceApi, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
31
+ import { AutomationStepFields, defaultStep, placeholderState } from "./automation-step-fields.component.js";
32
+ import HostRunHistoryCard from "./host-run-history-card.component.js";
33
+ import { EDITOR_OPTION_CEILING, useAutomationStepPickers } from "./use-automation-step-pickers.js";
34
+ const CUSTOM_EVENT_VALUE = '__custom__';
35
+ /**
36
+ * How many action documents the card reads.
37
+ *
38
+ * A CEILING, not a page size — see the query, which explains why this one
39
+ * collection cannot be sliced by the server without making the count beneath
40
+ * the list a per-page number.
41
+ *
42
+ * `ACTIONS_MAX_PER_HOST` is 500, so the ceiling really can bite and the probe
43
+ * beside it is what says when it has.
44
+ */ const ACTION_CEILING = 100;
45
+ const EMPTY_CONDITION_ROW = {
46
+ op: '',
47
+ field: '',
48
+ value: ''
49
+ };
50
+ /** Every Actions step, as the builder's "Do" picker offers it. */ const ACTION_STEP_KINDS = Object.entries(HOST_ACTION_STEP_LABELS).map(([value, label])=>({
51
+ value,
52
+ label
53
+ }));
54
+ /**
55
+ * A stored action — or one a recipe just built — as the editor holds it.
56
+ *
57
+ * One conversion for both doors, because they must agree: an action that
58
+ * round-trips through Edit and one a recipe hands over are the same shape
59
+ * to the editor, and a field the Edit path hydrated that the recipe path
60
+ * forgot would be a recipe whose conditions vanished on open. Site events
61
+ * are first-class (AGL-256/266): their selector/threshold/path config is
62
+ * kept through an edit. Structured conditions (AGL-557; chained AGL-565)
63
+ * become rows; a legacy single-condition doc normalizes to one row.
64
+ */ function draftFromAction(action, id) {
65
+ var _ref, _action_name, _ref1, _ref2, _ref3, _action_steps, _ref4;
66
+ var _action_trigger, _action_trigger1, _action_trigger2, _action_trigger3, _action_trigger4, _action_trigger5, _action_trigger6, _action_trigger7, _action_trigger8, _action_trigger9, _action_trigger10, _action_trigger11, _action_trigger12;
67
+ const builtIn = HOST_EVENT_TYPES.includes((_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event) || isSiteEventType(String((_ref = (_action_trigger1 = action.trigger) == null ? void 0 : _action_trigger1.event) != null ? _ref : ''));
68
+ const rows = normalizeTriggerConditions(action.trigger).map((condition)=>{
69
+ var _condition_op, _condition_field, _condition_value;
70
+ return {
71
+ op: (_condition_op = condition.op) != null ? _condition_op : '',
72
+ field: (_condition_field = condition.field) != null ? _condition_field : '',
73
+ value: (_condition_value = condition.value) != null ? _condition_value : ''
74
+ };
75
+ });
76
+ return {
77
+ id,
78
+ name: (_action_name = action.name) != null ? _action_name : '',
79
+ trigger: _extends({
80
+ event: builtIn ? action.trigger.event : CUSTOM_EVENT_VALUE,
81
+ filter: (_ref1 = (_action_trigger2 = action.trigger) == null ? void 0 : _action_trigger2.filter) != null ? _ref1 : '',
82
+ selector: (_ref2 = (_action_trigger3 = action.trigger) == null ? void 0 : _action_trigger3.selector) != null ? _ref2 : '',
83
+ threshold: (_action_trigger4 = action.trigger) == null ? void 0 : _action_trigger4.threshold,
84
+ pathPattern: (_ref3 = (_action_trigger5 = action.trigger) == null ? void 0 : _action_trigger5.pathPattern) != null ? _ref3 : '',
85
+ oncePerVisitor: ((_action_trigger6 = action.trigger) == null ? void 0 : _action_trigger6.oncePerVisitor) === true,
86
+ oncePerSession: ((_action_trigger7 = action.trigger) == null ? void 0 : _action_trigger7.oncePerSession) === true,
87
+ everyTime: ((_action_trigger8 = action.trigger) == null ? void 0 : _action_trigger8.everyTime) === true
88
+ }, Number((_action_trigger9 = action.trigger) == null ? void 0 : _action_trigger9.cooldownMinutes) >= 1 ? {
89
+ cooldownMinutes: Number((_action_trigger10 = action.trigger) == null ? void 0 : _action_trigger10.cooldownMinutes)
90
+ } : {}),
91
+ steps: (_action_steps = action.steps) != null ? _action_steps : [],
92
+ enabled: action.enabled !== false,
93
+ customEvent: builtIn ? '' : (_ref4 = (_action_trigger11 = action.trigger) == null ? void 0 : _action_trigger11.event) != null ? _ref4 : '',
94
+ conditionRows: rows.length ? rows : [
95
+ EMPTY_CONDITION_ROW
96
+ ],
97
+ conditionCombinator: ((_action_trigger12 = action.trigger) == null ? void 0 : _action_trigger12.combinator) === 'or' ? 'or' : 'and',
98
+ // A recipe stamps what it builds, and a stored action carries what it
99
+ // was saved with — one reader for both doors, unknown kept unknown.
100
+ recipe: hostActionRecipeId(action)
101
+ };
102
+ }
103
+ /** A trigger bound to one element, which makes the row an interaction. */ const LEAF_SELECTOR = /^\[data-aglyn="leaf:.+"\]$/;
104
+ /**
105
+ * Actions builder (AGL-148): HubSpot-style enrollment — trigger event →
106
+ * optional filter → ordered steps (run workflow, site alert, custom
107
+ * event, dataset append). Runs server-side via runEventActions; paid
108
+ * feature (`actions` flag, Pro+, `actionRunsPerMonth` metered).
109
+ */ export function HostActionsCard(props) {
110
+ var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _hostEventPayloadHint, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12, _ref13, _ref14, _ref15, _ref16, _ref17, _ref18, _ref19, _ref20, _ref21, _runsFor_name;
111
+ var _CRM_ACTION_RECIPES_find;
112
+ const { hostId, org } = props;
113
+ // Org-shared data root (AGL-237). Null until the org lookup settles
114
+ // (AGL-1061), and for a host with no owning org — the pre-migration host
115
+ // path is gone (AGL-1050), so the dataset and list pickers stay empty
116
+ // instead of offering rows from a path nothing else reads.
117
+ const firestore = useFirestore();
118
+ // Action creation is server-owned since AGL-2266 (the cap); every other
119
+ // action write on this surface stays client-direct.
120
+ const createResource = useHostResourceApi();
121
+ const { enqueueSnackbar } = useSnackbar();
122
+ const { confirm } = useConfirmationContext();
123
+ /**
124
+ * The shell's zone renderer, for what other plugins add to automations
125
+ * (AGL-2919); `null` outside the console shell, where there is no workspace
126
+ * to gate on.
127
+ */ const ExtensionZone = useConsoleWidgetSlot();
128
+ const { data: actionDocs, status: actionsStatus, /**
129
+ * The action rows this card's editor is seeded from are unconfirmed by
130
+ * the server (AGL-1358). Editing an action copies the whole stored row
131
+ * into `draft` — name, trigger and every step — and writes all of it
132
+ * back, so `merge: true` protects nothing and a cached seed reverts a
133
+ * colleague's newer version of the steps nobody here touched.
134
+ *
135
+ * The same shape the interaction builder guards (AGL-1066); this is its
136
+ * card twin, and it was the one that never got the guard.
137
+ */ fromCache: actionsFromCache } = useFirestoreCollection(/*
138
+ * ORDERED AND CEILINGED, deliberately not paged by the query (AGL-2501) —
139
+ * the same decision as the workflows card beside it.
140
+ *
141
+ * `limit(100)` alone is answered in DOCUMENT-ID order, so the window was a
142
+ * pseudo-random hundred that the `localeCompare` below arranged
143
+ * alphabetically, and nothing said the list was bounded.
144
+ *
145
+ * `collectionCeiling` does not change WHICH hundred — document-id order is
146
+ * what the bare cap already returned. What it changes is that the order is
147
+ * NAMED, so the obvious next edit is caught: ordering on `name` would HIDE
148
+ * every action written without one rather than mis-sorting the list, and
149
+ * `/api/hosts/resources` validates no field for presence while
150
+ * `IMPORTABLE_FIELDS.actions` copies one only if the export carried it.
151
+ *
152
+ * The QUERY is not paged because this collection holds TWO audiences. A
153
+ * row whose trigger names a leaf selector is an element interaction, which
154
+ * belongs to its document and is reported below as a count rather than
155
+ * listed; the rest are the site's actions. Both partitions are derived
156
+ * from the rows in hand, so a server page would make "3 interactions are
157
+ * set up on their own elements" mean "3 on this page" — a count that is a
158
+ * window length, which is the defect this sweep keeps finding rather than
159
+ * a shape it should add.
160
+ */ ()=>collectionCeiling(collection(firestore, 'hosts', hostId, 'actions'), ACTION_CEILING), [
161
+ firestore,
162
+ hostId
163
+ ], {
164
+ idField: '$id'
165
+ });
166
+ const { rows: readActions, truncated: actionsTruncated } = ceilingedWindow(actionDocs, ACTION_CEILING);
167
+ const [draft, setDraft] = useState(null);
168
+ /**
169
+ * The editor has been opened at least once in this session.
170
+ *
171
+ * A LATCH rather than `draft` itself, because the pickers below key their
172
+ * listeners on it. Tracking the dialog would tear those subscriptions down
173
+ * on Cancel and pay for them again on the next Edit, so a merchant working
174
+ * through ten actions would buy the same six windows ten times — worse than
175
+ * the mount-time read this replaces. Latched, a reader who never edits pays
176
+ * nothing and a reader who edits pays once.
177
+ */ const [editorOpened, setEditorOpened] = useState(false);
178
+ if (draft && !editorOpened) setEditorOpened(true);
179
+ /*
180
+ * The step editor's six option lists, read on the same latch as the
181
+ * Workflows builder's — one hook, because the two editors offer one set of
182
+ * steps and a second copy would be a second read cost to keep in agreement.
183
+ */ const { pickers: stepPickers, truncated: truncatedPickers } = useAutomationStepPickers(hostId, editorOpened);
184
+ const liveActions = readActions.filter((action)=>!action.deletedAt);
185
+ /*
186
+ * Opens a listed action in the editor, for a widget in the `hostAutomations`
187
+ * zone — one that drafted an action and offers to open it. Read through a
188
+ * ref so the callback keeps one identity while the list changes beneath it,
189
+ * and answers `false` for an id the list has not read yet.
190
+ */ const listedRef = useRef(liveActions);
191
+ listedRef.current = liveActions;
192
+ const openAction = useCallback((actionId)=>{
193
+ const action = listedRef.current.find((row)=>row.$id === actionId);
194
+ if (!action) return false;
195
+ setDraft(draftFromAction(action, action.$id));
196
+ return true;
197
+ }, []);
198
+ /**
199
+ * Element interactions are not listed here.
200
+ *
201
+ * An ACTION is something the SITE does — an order was placed, a form was
202
+ * submitted. An INTERACTION is something an ELEMENT does, and it belongs to
203
+ * the document that holds the element: it publishes and rolls back with it,
204
+ * travels with it, and is deleted with it. This page is the site's
205
+ * automation list, and a nav menu's hover timing sitting in it beside "when
206
+ * an order is placed" was the tell that the two had been conflated.
207
+ *
208
+ * These rows are the ones written BEFORE the move, still bound by selector.
209
+ * They keep running and stay editable — on their element, in the besigner,
210
+ * which is now the only place they appear. Counted rather than hidden
211
+ * outright, so the page can say where they went instead of appearing to
212
+ * have lost them.
213
+ */ const elementScoped = liveActions.filter((action)=>{
214
+ var _ref;
215
+ var _action_trigger;
216
+ return LEAF_SELECTOR.test(String((_ref = action == null ? void 0 : (_action_trigger = action.trigger) == null ? void 0 : _action_trigger.selector) != null ? _ref : ''));
217
+ });
218
+ // Sorting is safe here in a way it is not on a paged list: these rows are
219
+ // the whole collection below the ceiling, not a slice of one.
220
+ const actions = liveActions.filter((action)=>!elementScoped.includes(action)).sort((a, b)=>{
221
+ var _a_name, _b_name;
222
+ return String((_a_name = a.name) != null ? _a_name : '').localeCompare(String((_b_name = b.name) != null ? _b_name : ''));
223
+ });
224
+ // The page is a SLICE: the rows are already in hand, and both the element
225
+ // interaction count and the alphabetical order need all of them.
226
+ const [page, setPage] = useState(0);
227
+ const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
228
+ const visibleActions = useMemo(()=>actions.slice(page * pageSize, page * pageSize + pageSize), [
229
+ actions,
230
+ page,
231
+ pageSize
232
+ ]);
233
+ const patch = useCallback((updater)=>setDraft((previous)=>previous ? updater(previous) : previous), []);
234
+ /** The saved action the editor has open, as the `automationEditor` zone names it. */ const draftId = (_ref = draft == null ? void 0 : draft.id) != null ? _ref : null;
235
+ const draftName = (_ref1 = draft == null ? void 0 : draft.name) != null ? _ref1 : '';
236
+ const editorTarget = useMemo(()=>draftId ? {
237
+ type: 'action',
238
+ id: draftId,
239
+ name: draftName
240
+ } : null, // The name the editor opened with: the zone reads the automation as it is
241
+ // stored, so a rename being typed does not make it a different one.
242
+ // eslint-disable-next-line react-hooks/exhaustive-deps
243
+ [
244
+ draftId
245
+ ]);
246
+ /**
247
+ * Whether this workspace may open the editor at all. One gate for the
248
+ * blank action and the recipes: a recipe is an action, so the plan that
249
+ * carries one carries the other, and the refusal reads the same.
250
+ */ const actionsEntitled = useCallback(()=>{
251
+ if (checkEntitlement(org, 'actions')) return true;
252
+ enqueueSnackbar('The actions builder requires a Pro plan — see Billing to upgrade', {
253
+ variant: 'warning',
254
+ persist: false
255
+ });
256
+ return false;
257
+ }, [
258
+ org,
259
+ enqueueSnackbar
260
+ ]);
261
+ const handleAdd = useCallback(()=>{
262
+ if (!actionsEntitled()) return;
263
+ setDraft({
264
+ id: null,
265
+ name: '',
266
+ trigger: {
267
+ event: 'formSubmission',
268
+ filter: ''
269
+ },
270
+ steps: [
271
+ defaultStep('siteAlert')
272
+ ],
273
+ enabled: true,
274
+ customEvent: '',
275
+ conditionRows: [
276
+ EMPTY_CONDITION_ROW
277
+ ],
278
+ conditionCombinator: 'and',
279
+ recipe: null
280
+ });
281
+ }, [
282
+ actionsEntitled
283
+ ]);
284
+ /*
285
+ * THE RECIPES (AGL-2626).
286
+ *
287
+ * A recipe opens the editor prefilled and writes nothing: the draft it
288
+ * builds is the one Save would persist, and until then it is state in
289
+ * this card and nowhere else. Three recipes open at once; "Tag by form"
290
+ * needs a form first, and the form is this site's — `hosts/{hostId}/forms`
291
+ * — so the picker is the one host-scoped piece of the feature, held here
292
+ * rather than in the catalog.
293
+ */ const [recipesAnchor, setRecipesAnchor] = useState(null);
294
+ const [formPickFor, setFormPickFor] = useState(null);
295
+ const [pickedFormId, setPickedFormId] = useState('');
296
+ /**
297
+ * The form picker has been opened at least once — the `editorOpened`
298
+ * latch's twin, and for the same reason: the forms window is read for
299
+ * this picker only, and a reader who never opens it pays nothing.
300
+ */ const [formPickerOpened, setFormPickerOpened] = useState(false);
301
+ if (formPickFor && !formPickerOpened) setFormPickerOpened(true);
302
+ const { data: formRead } = useFirestoreCollection(()=>formPickerOpened ? collectionCeiling(collection(firestore, 'hosts', hostId, 'forms'), EDITOR_OPTION_CEILING) : null, [
303
+ firestore,
304
+ hostId,
305
+ formPickerOpened
306
+ ], {
307
+ idField: '$id'
308
+ });
309
+ const { rows: formDocs, truncated: formsTruncated } = ceilingedWindow(formRead, EDITOR_OPTION_CEILING);
310
+ // An archived form collects nothing, so a recipe keyed on it would never
311
+ // fire; the name falls back to the id the way every form list's does.
312
+ const formOptions = (formDocs != null ? formDocs : []).filter((form)=>!form.archivedAt).map((form)=>{
313
+ var _form_displayName;
314
+ return {
315
+ id: form.$id,
316
+ name: String((_form_displayName = form.displayName) != null ? _form_displayName : '').trim() || form.$id
317
+ };
318
+ }).sort((a, b)=>a.name.localeCompare(b.name));
319
+ const handleRecipe = useCallback((recipe)=>{
320
+ setRecipesAnchor(null);
321
+ if (!actionsEntitled()) return;
322
+ if (recipe.needs === 'form') {
323
+ setPickedFormId('');
324
+ setFormPickFor(recipe);
325
+ return;
326
+ }
327
+ setDraft(draftFromAction(recipe.build(), null));
328
+ }, [
329
+ actionsEntitled
330
+ ]);
331
+ const handleFormPicked = useCallback(()=>{
332
+ const recipe = formPickFor;
333
+ const form = formOptions.find((option)=>option.id === pickedFormId);
334
+ if (!recipe || !form) return;
335
+ setFormPickFor(null);
336
+ setDraft(draftFromAction(recipe.build({
337
+ form
338
+ }), null));
339
+ }, [
340
+ formPickFor,
341
+ formOptions,
342
+ pickedFormId
343
+ ]);
344
+ // Run log + test runs (AGL-266).
345
+ const [runsFor, setRunsFor] = useState(null);
346
+ const handleTestRun = useCallback(async (action)=>{
347
+ try {
348
+ var _action_trigger;
349
+ const response = await fetch('/api/events/dispatch', {
350
+ method: 'POST',
351
+ headers: {
352
+ 'Content-Type': 'application/json'
353
+ },
354
+ body: JSON.stringify({
355
+ hostId,
356
+ actionId: action.$id,
357
+ event: (_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event,
358
+ payload: {
359
+ path: '/console-test',
360
+ test: 'true'
361
+ }
362
+ })
363
+ });
364
+ const payload = await response.json().catch(()=>({}));
365
+ if (!response.ok) {
366
+ var _ref;
367
+ return void enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Test run failed', {
368
+ variant: 'warning',
369
+ persist: false
370
+ });
371
+ }
372
+ const alerts = Array.isArray(payload == null ? void 0 : payload.alerts) ? payload.alerts : [];
373
+ enqueueSnackbar(alerts.length ? `Test ran — first alert: ${alerts[0].message}` : 'Test ran — server steps executed (see Runs)', {
374
+ variant: 'success',
375
+ persist: false
376
+ });
377
+ } catch (error) {
378
+ console.error(error);
379
+ enqueueSnackbar('Test run failed', {
380
+ variant: 'error'
381
+ });
382
+ }
383
+ }, [
384
+ hostId,
385
+ enqueueSnackbar
386
+ ]);
387
+ const handleSave = useCallback(async ()=>{
388
+ var _draft_trigger_filter, _draft_trigger_selector, _draft_trigger_pathPattern;
389
+ if (!draft) return;
390
+ const isCustom = draft.trigger.event === CUSTOM_EVENT_VALUE;
391
+ const candidate = _extends({
392
+ name: draft.name.trim().slice(0, 60),
393
+ trigger: _extends({
394
+ event: isCustom ? draft.customEvent.trim() : draft.trigger.event
395
+ }, ((_draft_trigger_filter = draft.trigger.filter) == null ? void 0 : _draft_trigger_filter.trim()) ? {
396
+ filter: draft.trigger.filter.trim()
397
+ } : {}, ((_draft_trigger_selector = draft.trigger.selector) == null ? void 0 : _draft_trigger_selector.trim()) ? {
398
+ selector: draft.trigger.selector.trim()
399
+ } : {}, Number(draft.trigger.threshold) > 0 ? {
400
+ threshold: Number(draft.trigger.threshold)
401
+ } : {}, ((_draft_trigger_pathPattern = draft.trigger.pathPattern) == null ? void 0 : _draft_trigger_pathPattern.trim()) ? {
402
+ pathPattern: draft.trigger.pathPattern.trim()
403
+ } : {}, draft.trigger.oncePerVisitor === true ? {
404
+ oncePerVisitor: true
405
+ } : {}, draft.trigger.oncePerSession === true ? {
406
+ oncePerSession: true
407
+ } : {}, Number(draft.trigger.cooldownMinutes) >= 1 ? {
408
+ cooldownMinutes: Number(draft.trigger.cooldownMinutes)
409
+ } : {}, draft.trigger.everyTime === true ? {
410
+ everyTime: true
411
+ } : {}, draft.conditionRows.some((row)=>row.op) ? {
412
+ conditions: draft.conditionRows.filter((row)=>row.op).map((row)=>_extends({
413
+ field: row.field.trim(),
414
+ op: row.op
415
+ }, row.op !== 'notEmpty' ? {
416
+ value: row.value.trim()
417
+ } : {})),
418
+ combinator: draft.conditionCombinator
419
+ } : {}),
420
+ steps: draft.steps,
421
+ enabled: draft.enabled !== false
422
+ }, draft.recipe !== undefined ? {
423
+ recipe: draft.recipe
424
+ } : {});
425
+ const problem = validateHostAction(candidate);
426
+ if (problem) {
427
+ return void enqueueSnackbar(problem, {
428
+ variant: 'warning',
429
+ persist: false
430
+ });
431
+ }
432
+ try {
433
+ var _draft_id;
434
+ const id = (_draft_id = draft.id) != null ? _draft_id : createResourceUid();
435
+ /**
436
+ * Refuse an EDIT whose seed the server never confirmed (AGL-1358).
437
+ *
438
+ * The payload is the whole action. `merge: true` is here so the
439
+ * frequency caps and conditions can be nulled explicitly, not to
440
+ * protect untouched fields — there are none, they are all in the
441
+ * payload. So a cached seed does not lose one edit, it reverts the
442
+ * trigger and every step to whatever the cache last held, on an
443
+ * automation that is running on a live site.
444
+ *
445
+ * Only the edit path. A NEW action is built from `EMPTY_*` defaults at
446
+ * a fresh uid and can overwrite nothing; the first snapshot of any
447
+ * listener is `fromCache: true`, so guarding a create would refuse a
448
+ * save that was never unsafe.
449
+ *
450
+ * The guard WRAPS the write — the interaction builder's early return
451
+ * is the shape you can keep while losing the protection.
452
+ */ /**
453
+ * The action DOCUMENT is created by the server (AGL-2266).
454
+ *
455
+ * `hosts/{hostId}/actions` was in none of the host catch-all's exclusion
456
+ * lists, so any editor could create one client-direct on any plan and
457
+ * nothing counted them. /api/hosts/resources now owns the create and
458
+ * holds `ACTIONS_MAX_PER_HOST` from a server read.
459
+ *
460
+ * Only the create moves, and it writes a SHELL. The merge-set below is
461
+ * untouched and still carries the whole candidate — trigger, steps,
462
+ * conditions, the explicit nulls AGL-274/557 depend on — so no field can
463
+ * be lost to the route's allow-list, and for an existing action this
464
+ * path does not run at all.
465
+ */ if (!draft.id) {
466
+ try {
467
+ var _ref, _candidate_name;
468
+ await createResource({
469
+ hostId,
470
+ resource: 'action',
471
+ id,
472
+ data: {
473
+ name: (_ref = (_candidate_name = candidate.name) != null ? _candidate_name : draft.name) != null ? _ref : 'Untitled action'
474
+ }
475
+ });
476
+ } catch (error) {
477
+ var _ref1;
478
+ return void enqueueSnackbar((_ref1 = error == null ? void 0 : error.message) != null ? _ref1 : 'Could not create the interaction', {
479
+ variant: 'error'
480
+ });
481
+ }
482
+ }
483
+ const verdict = await writeGuardedBySeed({
484
+ subject: 'action',
485
+ unreadable: Boolean(draft.id) && actionsStatus === 'error',
486
+ fromCache: Boolean(draft.id) && actionsFromCache
487
+ }, async ()=>{
488
+ await setDoc(doc(firestore, 'hosts', hostId, 'actions', id), _extends({}, hostActionDocument(candidate), {
489
+ updatedAt: Timestamp.now()
490
+ }, draft.id ? {} : {
491
+ createdAt: Timestamp.now()
492
+ }), {
493
+ merge: true
494
+ });
495
+ });
496
+ // A refusal keeps the editor open with everything that was typed.
497
+ if (!verdict.ok) {
498
+ return void enqueueSnackbar(verdict.message, {
499
+ variant: 'warning',
500
+ persist: false
501
+ });
502
+ }
503
+ setDraft(null);
504
+ enqueueSnackbar('Action saved', {
505
+ variant: 'success',
506
+ persist: false
507
+ });
508
+ } catch (error) {
509
+ console.error(error);
510
+ enqueueSnackbar('An error has occurred', {
511
+ variant: 'error',
512
+ allowDuplicate: true
513
+ });
514
+ }
515
+ }, [
516
+ draft,
517
+ firestore,
518
+ createResource,
519
+ hostId,
520
+ enqueueSnackbar,
521
+ actionsFromCache,
522
+ actionsStatus
523
+ ]);
524
+ const handleDelete = useCallback((action)=>async ()=>{
525
+ const confirmed = await confirm({
526
+ title: 'Delete this action?',
527
+ description: `"${action.name}" stops running on its trigger.`,
528
+ confirmationText: 'Delete',
529
+ confirmationButtonProps: {
530
+ color: 'error'
531
+ }
532
+ }).then(()=>true).catch(()=>false);
533
+ if (!confirmed) return;
534
+ await updateDoc(doc(firestore, 'hosts', hostId, 'actions', action.$id), {
535
+ deletedAt: Timestamp.now()
536
+ });
537
+ }, [
538
+ confirm,
539
+ firestore,
540
+ hostId
541
+ ]);
542
+ const handleToggle = useCallback((action)=>async (event)=>{
543
+ const enabled = event.target.checked;
544
+ /*
545
+ * A placeholder is a value nobody has supplied yet. Switched on, the
546
+ * step holding it fails every run and a condition holding one never
547
+ * matches, so turning such an automation on is confirmed, naming what
548
+ * is still missing.
549
+ */ const missing = enabled ? automationPlaceholders(action) : [];
550
+ if (missing.length) {
551
+ const confirmed = await confirm({
552
+ title: 'Switch on with placeholders?',
553
+ description: `"${action.name}" still has ${missing.length === 1 ? 'a placeholder' : `${missing.length} placeholders`} ` + `to fill in: ${missing.slice(0, 3).map(describeAutomationPlaceholder).join('; ')}` + `${missing.length > 3 ? '; …' : ''}. Open it with Edit to fill ` + `${missing.length === 1 ? 'it' : 'them'} in first.`,
554
+ confirmationText: 'Switch on anyway'
555
+ }).then(()=>true).catch(()=>false);
556
+ if (!confirmed) return;
557
+ }
558
+ await updateDoc(doc(firestore, 'hosts', hostId, 'actions', action.$id), {
559
+ enabled
560
+ });
561
+ }, [
562
+ confirm,
563
+ firestore,
564
+ hostId
565
+ ]);
566
+ return /*#__PURE__*/ _jsxs(CardDisplay, {
567
+ header: 'Actions',
568
+ help: pluginDocsHelp('actionsBuilder', {
569
+ anchor: '#create-an-action'
570
+ }),
571
+ contentGutterX: true,
572
+ contentGutterY: true,
573
+ children: [
574
+ /*#__PURE__*/ _jsxs(Stack, {
575
+ spacing: 1,
576
+ children: [
577
+ /*#__PURE__*/ _jsx(Typography, {
578
+ variant: "body2",
579
+ color: "text.secondary",
580
+ children: 'When a site event fires, run automations in order — trigger a ' + 'workflow, show the visitor an alert, chain a custom event, or ' + 'write to a dataset. Pro plans and up.'
581
+ }),
582
+ visibleActions.map((action)=>{
583
+ var _action_steps, _ref;
584
+ var _action_trigger, _action_trigger1;
585
+ const missing = automationPlaceholders(action).length;
586
+ return /*#__PURE__*/ _jsxs(Stack, {
587
+ direction: "row",
588
+ spacing: 1,
589
+ sx: {
590
+ alignItems: 'center'
591
+ },
592
+ children: [
593
+ /*#__PURE__*/ _jsx(Switch, {
594
+ size: "small",
595
+ checked: action.enabled !== false,
596
+ onChange: handleToggle(action)
597
+ }),
598
+ /*#__PURE__*/ _jsxs(Stack, {
599
+ sx: {
600
+ flex: 1,
601
+ minWidth: 0
602
+ },
603
+ children: [
604
+ /*#__PURE__*/ _jsx(Typography, {
605
+ variant: "body2",
606
+ noWrap: true,
607
+ children: action.name
608
+ }),
609
+ /*#__PURE__*/ _jsx(Typography, {
610
+ variant: "caption",
611
+ color: "text.secondary",
612
+ noWrap: true,
613
+ children: `on ${hostEventLabel((_action_trigger = action.trigger) == null ? void 0 : _action_trigger.event)}` + ` · ${((_action_steps = action.steps) != null ? _action_steps : []).map((step)=>{
614
+ var _HOST_ACTION_STEP_LABELS_step_type;
615
+ return (_HOST_ACTION_STEP_LABELS_step_type = HOST_ACTION_STEP_LABELS[step.type]) != null ? _HOST_ACTION_STEP_LABELS_step_type : step.type;
616
+ }).join(' → ')}`
617
+ }),
618
+ missing ? /*#__PURE__*/ _jsx(Typography, {
619
+ variant: "caption",
620
+ color: "warning.main",
621
+ noWrap: true,
622
+ children: missing === 1 ? '1 placeholder to fill in' : `${missing} placeholders to fill in`
623
+ }) : null
624
+ ]
625
+ }),
626
+ /*#__PURE__*/ _jsx(Button, {
627
+ size: "small",
628
+ onClick: ()=>setDraft(draftFromAction(action, action.$id)),
629
+ children: 'Edit'
630
+ }),
631
+ isSiteEventType(String((_ref = (_action_trigger1 = action.trigger) == null ? void 0 : _action_trigger1.event) != null ? _ref : '')) ? // Test run (AGL-266): server steps only, via the dispatch API.
632
+ /*#__PURE__*/ _jsx(Button, {
633
+ size: "small",
634
+ onClick: ()=>void handleTestRun(action),
635
+ children: 'Test'
636
+ }) : null,
637
+ /*#__PURE__*/ _jsx(Button, {
638
+ size: "small",
639
+ onClick: ()=>setRunsFor(action),
640
+ children: 'Runs'
641
+ }),
642
+ /*#__PURE__*/ _jsx(Button, {
643
+ size: "small",
644
+ color: "error",
645
+ onClick: handleDelete(action),
646
+ children: 'Delete'
647
+ })
648
+ ]
649
+ }, action.$id);
650
+ }),
651
+ actions.length === 0 ? null : /*#__PURE__*/ _jsx(ListPagination, {
652
+ page: page,
653
+ pageSize: pageSize,
654
+ rowCount: visibleActions.length,
655
+ // The actions the card HOLDS, which is what the reader is paging
656
+ // through — not the collection, whose other half is the element
657
+ // interactions counted below.
658
+ count: actions.length,
659
+ onPageChange: setPage,
660
+ onPageSizeChange: setPageSize
661
+ }),
662
+ actionsTruncated ? /*#__PURE__*/ _jsx(Alert, {
663
+ severity: "info",
664
+ children: `Showing the first ${ACTION_CEILING} rows of this site’s ` + 'automations, ordered by id. There are more — both the list ' + 'above and the interaction count below describe only what was ' + 'read.'
665
+ }) : null,
666
+ /*#__PURE__*/ _jsxs(Stack, {
667
+ direction: "row",
668
+ spacing: 1,
669
+ sx: {
670
+ alignSelf: 'flex-start'
671
+ },
672
+ children: [
673
+ /*#__PURE__*/ _jsx(Button, {
674
+ size: "small",
675
+ color: "primary",
676
+ onClick: handleAdd,
677
+ children: 'Add action'
678
+ }),
679
+ /*#__PURE__*/ _jsx(Button, {
680
+ size: "small",
681
+ color: "primary",
682
+ "aria-haspopup": "menu",
683
+ "aria-controls": recipesAnchor ? 'host-action-recipes' : undefined,
684
+ "aria-expanded": recipesAnchor ? true : undefined,
685
+ onClick: (event)=>setRecipesAnchor(event.currentTarget),
686
+ children: 'Recipes'
687
+ }),
688
+ ExtensionZone ? /*#__PURE__*/ _jsx(ExtensionZone, {
689
+ slot: "hostAutomations",
690
+ hostId: hostId,
691
+ orgId: org == null ? void 0 : org.$id,
692
+ openAction: openAction
693
+ }) : null
694
+ ]
695
+ }),
696
+ /*#__PURE__*/ _jsx(Menu, {
697
+ id: "host-action-recipes",
698
+ anchorEl: recipesAnchor,
699
+ open: Boolean(recipesAnchor),
700
+ onClose: ()=>setRecipesAnchor(null),
701
+ children: CRM_ACTION_RECIPES.map((recipe)=>/*#__PURE__*/ _jsx(MenuItem, {
702
+ onClick: ()=>handleRecipe(recipe),
703
+ children: /*#__PURE__*/ _jsx(ListItemText, {
704
+ primary: recipe.title,
705
+ secondary: recipe.description,
706
+ slotProps: {
707
+ secondary: {
708
+ sx: {
709
+ whiteSpace: 'normal',
710
+ maxWidth: 360
711
+ }
712
+ }
713
+ }
714
+ })
715
+ }, recipe.id))
716
+ }),
717
+ elementScoped.length ? /*#__PURE__*/ _jsxs(Typography, {
718
+ variant: "caption",
719
+ color: "text.secondary",
720
+ children: [
721
+ elementScoped.length === 1 ? '1 interaction is set up on its own element — ' : `${elementScoped.length} interactions are set up on their own elements — `,
722
+ 'open the element in the besigner to edit it. Interactions ' + 'belong to the page they are on, so they publish and roll back ' + 'with it.'
723
+ ]
724
+ }) : null
725
+ ]
726
+ }),
727
+ /*#__PURE__*/ _jsxs(Dialog, {
728
+ open: Boolean(draft),
729
+ onClose: ()=>setDraft(null),
730
+ maxWidth: "sm",
731
+ fullWidth: true,
732
+ children: [
733
+ /*#__PURE__*/ _jsx(DialogTitle, {
734
+ children: (draft == null ? void 0 : draft.id) ? 'Edit action' : 'Add action'
735
+ }),
736
+ /*#__PURE__*/ _jsxs(DialogContent, {
737
+ sx: {
738
+ display: 'flex',
739
+ flexDirection: 'column',
740
+ gap: 1.5,
741
+ pt: 1
742
+ },
743
+ children: [
744
+ ExtensionZone && editorTarget ? /*#__PURE__*/ _jsx(ExtensionZone, {
745
+ slot: "automationEditor",
746
+ hostId: hostId,
747
+ orgId: org == null ? void 0 : org.$id,
748
+ target: editorTarget
749
+ }) : null,
750
+ (draft == null ? void 0 : draft.recipe) ? /*#__PURE__*/ _jsx(Typography, {
751
+ variant: "body2",
752
+ color: "text.secondary",
753
+ children: `Started from the “${(_ref2 = (_CRM_ACTION_RECIPES_find = CRM_ACTION_RECIPES.find((recipe)=>recipe.id === draft.recipe)) == null ? void 0 : _CRM_ACTION_RECIPES_find.title) != null ? _ref2 : 'recipe'}” recipe — change anything, then save.`
754
+ }) : null,
755
+ truncatedPickers.length > 0 ? /*#__PURE__*/ _jsx(Alert, {
756
+ severity: "info",
757
+ sx: {
758
+ mt: 1
759
+ },
760
+ children: `Offering the first ${EDITOR_OPTION_CEILING} rows, ordered by ` + `id, for: ${truncatedPickers.join(', ')}. This site has ` + 'more, so a step target may not be listed below.'
761
+ }) : null,
762
+ /*#__PURE__*/ _jsx(TextField, {
763
+ label: "Name",
764
+ value: (_ref3 = draft == null ? void 0 : draft.name) != null ? _ref3 : '',
765
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
766
+ name: event.target.value
767
+ })),
768
+ size: "small",
769
+ autoFocus: true,
770
+ sx: {
771
+ mt: 1
772
+ }
773
+ }),
774
+ /*#__PURE__*/ _jsxs(Stack, {
775
+ direction: "row",
776
+ spacing: 1,
777
+ children: [
778
+ /*#__PURE__*/ _jsxs(TextField, {
779
+ select: true,
780
+ label: "Trigger event",
781
+ value: (_ref4 = draft == null ? void 0 : draft.trigger.event) != null ? _ref4 : '',
782
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
783
+ trigger: _extends({}, previous.trigger, {
784
+ event: event.target.value
785
+ })
786
+ })),
787
+ size: "small",
788
+ sx: {
789
+ minWidth: 180
790
+ },
791
+ children: [
792
+ HOST_EVENT_TYPES.map((eventType)=>/*#__PURE__*/ _jsx(MenuItem, {
793
+ value: eventType,
794
+ children: hostEventLabel(eventType)
795
+ }, eventType)),
796
+ SITE_EVENT_TYPES.map((eventType)=>/*#__PURE__*/ _jsx(MenuItem, {
797
+ value: eventType,
798
+ children: `${eventType} (on page)`
799
+ }, eventType)),
800
+ /*#__PURE__*/ _jsx(MenuItem, {
801
+ value: CUSTOM_EVENT_VALUE,
802
+ children: 'Custom event…'
803
+ })
804
+ ]
805
+ }),
806
+ (draft == null ? void 0 : draft.trigger.event) === CUSTOM_EVENT_VALUE ? /*#__PURE__*/ _jsx(TextField, {
807
+ label: "Custom event name",
808
+ value: (_ref5 = draft == null ? void 0 : draft.customEvent) != null ? _ref5 : '',
809
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
810
+ customEvent: event.target.value
811
+ })),
812
+ size: "small",
813
+ sx: {
814
+ flex: 1
815
+ }
816
+ }) : /*#__PURE__*/ _jsx(TextField, {
817
+ label: "Filter (optional)",
818
+ placeholder: 'path == "/pricing"',
819
+ // What the expression — and the conditions below — can name
820
+ // for this event; nothing for an event whose payload is not
821
+ // written down, rather than a guess.
822
+ helperText: (_hostEventPayloadHint = hostEventPayloadHint(draft == null ? void 0 : draft.trigger.event)) != null ? _hostEventPayloadHint : undefined,
823
+ value: (_ref6 = draft == null ? void 0 : draft.trigger.filter) != null ? _ref6 : '',
824
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
825
+ trigger: _extends({}, previous.trigger, {
826
+ filter: event.target.value
827
+ })
828
+ })),
829
+ size: "small",
830
+ sx: {
831
+ flex: 1
832
+ }
833
+ })
834
+ ]
835
+ }),
836
+ ((_ref7 = draft == null ? void 0 : draft.conditionRows) != null ? _ref7 : []).map((row, index)=>{
837
+ var _ref, _ref1;
838
+ return /*#__PURE__*/ _jsxs(Stack, {
839
+ direction: "row",
840
+ spacing: 1,
841
+ sx: {
842
+ alignItems: 'center'
843
+ },
844
+ children: [
845
+ index > 0 ? /*#__PURE__*/ _jsx(Typography, {
846
+ variant: "caption",
847
+ color: "text.secondary",
848
+ children: (draft == null ? void 0 : draft.conditionCombinator) === 'or' ? 'or' : 'and'
849
+ }) : null,
850
+ /*#__PURE__*/ _jsxs(TextField, {
851
+ select: true,
852
+ label: index === 0 ? 'Only run when' : 'Condition',
853
+ value: row.op,
854
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
855
+ conditionRows: previous.conditionRows.map((previousRow, index2)=>index2 === index ? _extends({}, previousRow, {
856
+ op: event.target.value
857
+ }) : previousRow)
858
+ })),
859
+ size: "small",
860
+ sx: {
861
+ minWidth: 180
862
+ },
863
+ children: [
864
+ ((_ref = draft == null ? void 0 : draft.conditionRows.length) != null ? _ref : 0) === 1 ? /*#__PURE__*/ _jsx(MenuItem, {
865
+ value: "",
866
+ children: 'Always (no condition)'
867
+ }) : null,
868
+ /*#__PURE__*/ _jsx(MenuItem, {
869
+ value: "notEmpty",
870
+ children: 'A field is not empty'
871
+ }),
872
+ /*#__PURE__*/ _jsx(MenuItem, {
873
+ value: "equals",
874
+ children: 'A field equals…'
875
+ }),
876
+ /*#__PURE__*/ _jsx(MenuItem, {
877
+ value: "contains",
878
+ children: 'A field contains…'
879
+ })
880
+ ]
881
+ }),
882
+ row.op ? /*#__PURE__*/ _jsx(TextField, {
883
+ label: "Field",
884
+ placeholder: "subscribe",
885
+ value: row.field,
886
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
887
+ conditionRows: previous.conditionRows.map((previousRow, index2)=>index2 === index ? _extends({}, previousRow, {
888
+ field: event.target.value
889
+ }) : previousRow)
890
+ })),
891
+ size: "small",
892
+ sx: {
893
+ flex: 1
894
+ }
895
+ }) : null,
896
+ row.op === 'equals' || row.op === 'contains' ? /*#__PURE__*/ _jsx(TextField, _extends({
897
+ label: "Value",
898
+ placeholder: "Yes"
899
+ }, placeholderState(row.value, 'Replace the placeholder with the value to match'), {
900
+ value: row.value,
901
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
902
+ conditionRows: previous.conditionRows.map((previousRow, index2)=>index2 === index ? _extends({}, previousRow, {
903
+ value: event.target.value
904
+ }) : previousRow)
905
+ })),
906
+ size: "small",
907
+ sx: {
908
+ flex: 1
909
+ }
910
+ })) : null,
911
+ ((_ref1 = draft == null ? void 0 : draft.conditionRows.length) != null ? _ref1 : 0) > 1 ? /*#__PURE__*/ _jsx(IconButton, {
912
+ size: "small",
913
+ "aria-label": "remove condition",
914
+ onClick: ()=>patch((previous)=>_extends({}, previous, {
915
+ conditionRows: previous.conditionRows.length > 1 ? previous.conditionRows.filter((_, index2)=>index2 !== index) : [
916
+ EMPTY_CONDITION_ROW
917
+ ]
918
+ })),
919
+ children: '×'
920
+ }) : null
921
+ ]
922
+ }, index);
923
+ }),
924
+ (draft == null ? void 0 : draft.conditionRows.every((row)=>row.op)) ? /*#__PURE__*/ _jsxs(Stack, {
925
+ direction: "row",
926
+ spacing: 1,
927
+ sx: {
928
+ alignItems: 'center'
929
+ },
930
+ children: [
931
+ /*#__PURE__*/ _jsx(Button, {
932
+ size: "small",
933
+ disabled: ((_ref8 = draft == null ? void 0 : draft.conditionRows.length) != null ? _ref8 : 0) >= ACTION_MAX_CONDITIONS,
934
+ onClick: ()=>patch((previous)=>_extends({}, previous, {
935
+ conditionRows: [
936
+ ...previous.conditionRows,
937
+ // New rows start on the simplest operator so the
938
+ // field input is immediately visible.
939
+ {
940
+ op: 'notEmpty',
941
+ field: '',
942
+ value: ''
943
+ }
944
+ ]
945
+ })),
946
+ children: 'Add condition'
947
+ }),
948
+ ((_ref9 = draft == null ? void 0 : draft.conditionRows.length) != null ? _ref9 : 0) >= 2 ? // AND/OR combinator (AGL-565); applies to every row.
949
+ /*#__PURE__*/ _jsxs(TextField, {
950
+ select: true,
951
+ label: "Match",
952
+ value: (_ref10 = draft == null ? void 0 : draft.conditionCombinator) != null ? _ref10 : 'and',
953
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
954
+ conditionCombinator: event.target.value
955
+ })),
956
+ size: "small",
957
+ sx: {
958
+ minWidth: 220
959
+ },
960
+ children: [
961
+ /*#__PURE__*/ _jsx(MenuItem, {
962
+ value: "and",
963
+ children: 'All conditions match (AND)'
964
+ }),
965
+ /*#__PURE__*/ _jsx(MenuItem, {
966
+ value: "or",
967
+ children: 'Any condition matches (OR)'
968
+ })
969
+ ]
970
+ }) : null
971
+ ]
972
+ }) : null,
973
+ isSiteEventType((_ref11 = draft == null ? void 0 : draft.trigger.event) != null ? _ref11 : '') ? // Site-event config (AGL-256): what/where the trigger watches.
974
+ /*#__PURE__*/ _jsxs(Stack, {
975
+ direction: "row",
976
+ spacing: 1,
977
+ children: [
978
+ ELEMENT_SCOPED_SITE_EVENTS.includes((_ref12 = draft == null ? void 0 : draft.trigger.event) != null ? _ref12 : '') ? /*#__PURE__*/ _jsx(TextField, {
979
+ label: "CSS selector",
980
+ placeholder: "#pricing-table",
981
+ value: (_ref13 = draft == null ? void 0 : draft.trigger.selector) != null ? _ref13 : '',
982
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
983
+ trigger: _extends({}, previous.trigger, {
984
+ selector: event.target.value
985
+ })
986
+ })),
987
+ size: "small",
988
+ sx: {
989
+ flex: 1
990
+ }
991
+ }) : null,
992
+ [
993
+ 'scrollDepth',
994
+ 'timeOnPage'
995
+ ].includes((_ref14 = draft == null ? void 0 : draft.trigger.event) != null ? _ref14 : '') ? /*#__PURE__*/ _jsx(TextField, {
996
+ type: "number",
997
+ label: (draft == null ? void 0 : draft.trigger.event) === 'scrollDepth' ? 'Scroll %' : 'Seconds',
998
+ value: (_ref15 = draft == null ? void 0 : draft.trigger.threshold) != null ? _ref15 : '',
999
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
1000
+ trigger: _extends({}, previous.trigger, {
1001
+ threshold: Number(event.target.value)
1002
+ })
1003
+ })),
1004
+ size: "small",
1005
+ sx: {
1006
+ width: 120
1007
+ }
1008
+ }) : null,
1009
+ /*#__PURE__*/ _jsx(TextField, {
1010
+ label: "Only on pages (optional)",
1011
+ placeholder: "/pricing or /blog/*",
1012
+ value: (_ref16 = draft == null ? void 0 : draft.trigger.pathPattern) != null ? _ref16 : '',
1013
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
1014
+ trigger: _extends({}, previous.trigger, {
1015
+ pathPattern: event.target.value
1016
+ })
1017
+ })),
1018
+ size: "small",
1019
+ sx: {
1020
+ flex: 1
1021
+ }
1022
+ }),
1023
+ /*#__PURE__*/ _jsxs(TextField, {
1024
+ select: true,
1025
+ label: "Frequency",
1026
+ size: "small",
1027
+ sx: {
1028
+ minWidth: 170
1029
+ },
1030
+ value: (draft == null ? void 0 : draft.trigger.oncePerVisitor) ? 'visitor' : (draft == null ? void 0 : draft.trigger.oncePerSession) ? 'session' : Number(draft == null ? void 0 : draft.trigger.cooldownMinutes) >= 1 ? 'cooldown' : (draft == null ? void 0 : draft.trigger.everyTime) ? 'every' : '',
1031
+ onChange: (event)=>{
1032
+ const mode = event.target.value;
1033
+ patch((previous)=>_extends({}, previous, {
1034
+ trigger: _extends({}, previous.trigger, {
1035
+ // Every occurrence (AGL-562): repeatable UI
1036
+ // choreography (menu/drawer toggles).
1037
+ everyTime: mode === 'every',
1038
+ oncePerVisitor: mode === 'visitor',
1039
+ oncePerSession: mode === 'session',
1040
+ cooldownMinutes: mode === 'cooldown' ? Number(previous.trigger.cooldownMinutes) >= 1 ? Number(previous.trigger.cooldownMinutes) : 60 : undefined
1041
+ })
1042
+ }));
1043
+ },
1044
+ children: [
1045
+ /*#__PURE__*/ _jsx(MenuItem, {
1046
+ value: "",
1047
+ children: 'Every matching pageview'
1048
+ }),
1049
+ /*#__PURE__*/ _jsx(MenuItem, {
1050
+ value: "every",
1051
+ children: 'Every occurrence (repeatable)'
1052
+ }),
1053
+ /*#__PURE__*/ _jsx(MenuItem, {
1054
+ value: "session",
1055
+ children: 'Once per session'
1056
+ }),
1057
+ /*#__PURE__*/ _jsx(MenuItem, {
1058
+ value: "visitor",
1059
+ children: 'Once per visitor'
1060
+ }),
1061
+ /*#__PURE__*/ _jsx(MenuItem, {
1062
+ value: "cooldown",
1063
+ children: 'With a cooldown'
1064
+ })
1065
+ ]
1066
+ }),
1067
+ Number(draft == null ? void 0 : draft.trigger.cooldownMinutes) >= 1 && !(draft == null ? void 0 : draft.trigger.oncePerVisitor) && !(draft == null ? void 0 : draft.trigger.oncePerSession) ? /*#__PURE__*/ _jsx(TextField, {
1068
+ type: "number",
1069
+ label: "Cooldown (minutes)",
1070
+ size: "small",
1071
+ sx: {
1072
+ width: 150
1073
+ },
1074
+ value: (_ref17 = draft == null ? void 0 : draft.trigger.cooldownMinutes) != null ? _ref17 : 60,
1075
+ onChange: (event)=>patch((previous)=>_extends({}, previous, {
1076
+ trigger: _extends({}, previous.trigger, {
1077
+ cooldownMinutes: Number(event.target.value)
1078
+ })
1079
+ }))
1080
+ }) : null
1081
+ ]
1082
+ }) : null,
1083
+ /*#__PURE__*/ _jsx(Typography, {
1084
+ variant: "overline",
1085
+ color: "text.secondary",
1086
+ children: 'Steps (run in order)'
1087
+ }),
1088
+ ((_ref18 = draft == null ? void 0 : draft.steps) != null ? _ref18 : []).map((step, index)=>/*#__PURE__*/ _jsx(AutomationStepFields, {
1089
+ step: step,
1090
+ index: index,
1091
+ kind: step.type,
1092
+ kinds: ACTION_STEP_KINDS,
1093
+ stepForKind: (value)=>defaultStep(value),
1094
+ pickers: stepPickers,
1095
+ onSteps: (update)=>patch((previous)=>_extends({}, previous, {
1096
+ steps: update(previous.steps)
1097
+ }))
1098
+ }, index)),
1099
+ /*#__PURE__*/ _jsx(Button, {
1100
+ size: "small",
1101
+ sx: {
1102
+ alignSelf: 'flex-start'
1103
+ },
1104
+ disabled: ((_ref19 = draft == null ? void 0 : draft.steps.length) != null ? _ref19 : 0) >= ACTION_MAX_STEPS,
1105
+ onClick: ()=>patch((previous)=>_extends({}, previous, {
1106
+ steps: [
1107
+ ...previous.steps,
1108
+ defaultStep('siteAlert')
1109
+ ]
1110
+ })),
1111
+ children: 'Add step'
1112
+ })
1113
+ ]
1114
+ }),
1115
+ /*#__PURE__*/ _jsxs(DialogActions, {
1116
+ children: [
1117
+ /*#__PURE__*/ _jsx(Button, {
1118
+ onClick: ()=>setDraft(null),
1119
+ children: 'Cancel'
1120
+ }),
1121
+ /*#__PURE__*/ _jsx(Button, {
1122
+ variant: "contained",
1123
+ color: "primary",
1124
+ disabled: !(draft == null ? void 0 : draft.name.trim()),
1125
+ onClick: handleSave,
1126
+ children: 'Save action'
1127
+ })
1128
+ ]
1129
+ })
1130
+ ]
1131
+ }),
1132
+ /*#__PURE__*/ _jsxs(Dialog, {
1133
+ open: Boolean(formPickFor),
1134
+ onClose: ()=>setFormPickFor(null),
1135
+ maxWidth: "xs",
1136
+ fullWidth: true,
1137
+ children: [
1138
+ /*#__PURE__*/ _jsx(DialogTitle, {
1139
+ children: (_ref20 = formPickFor == null ? void 0 : formPickFor.title) != null ? _ref20 : ''
1140
+ }),
1141
+ /*#__PURE__*/ _jsxs(DialogContent, {
1142
+ sx: {
1143
+ display: 'flex',
1144
+ flexDirection: 'column',
1145
+ gap: 1.5,
1146
+ pt: 1
1147
+ },
1148
+ children: [
1149
+ /*#__PURE__*/ _jsx(Typography, {
1150
+ variant: "body2",
1151
+ color: "text.secondary",
1152
+ children: 'Pick the form whose new contacts get the tag. The action opens ' + 'ready to edit; nothing is saved until you save it.'
1153
+ }),
1154
+ formsTruncated ? /*#__PURE__*/ _jsx(Alert, {
1155
+ severity: "info",
1156
+ children: `Offering the first ${EDITOR_OPTION_CEILING} forms, ordered by ` + 'id. This site has more, so the form you want may not be listed.'
1157
+ }) : null,
1158
+ formOptions.length ? /*#__PURE__*/ _jsx(TextField, {
1159
+ select: true,
1160
+ label: "Form",
1161
+ value: pickedFormId,
1162
+ onChange: (event)=>setPickedFormId(event.target.value),
1163
+ size: "small",
1164
+ sx: {
1165
+ mt: 1
1166
+ },
1167
+ children: formOptions.map((option)=>/*#__PURE__*/ _jsx(MenuItem, {
1168
+ value: option.id,
1169
+ children: option.name
1170
+ }, option.id))
1171
+ }) : /*#__PURE__*/ _jsx(Typography, {
1172
+ variant: "body2",
1173
+ color: "text.secondary",
1174
+ children: 'This site has no forms yet. Add one in the besigner, then come back.'
1175
+ })
1176
+ ]
1177
+ }),
1178
+ /*#__PURE__*/ _jsxs(DialogActions, {
1179
+ children: [
1180
+ /*#__PURE__*/ _jsx(Button, {
1181
+ color: "inherit",
1182
+ onClick: ()=>setFormPickFor(null),
1183
+ children: 'Cancel'
1184
+ }),
1185
+ /*#__PURE__*/ _jsx(Button, {
1186
+ variant: "contained",
1187
+ disabled: !pickedFormId,
1188
+ onClick: handleFormPicked,
1189
+ children: 'Use recipe'
1190
+ })
1191
+ ]
1192
+ })
1193
+ ]
1194
+ }),
1195
+ /*#__PURE__*/ _jsxs(Dialog, {
1196
+ open: Boolean(runsFor),
1197
+ onClose: ()=>setRunsFor(null),
1198
+ maxWidth: "sm",
1199
+ fullWidth: true,
1200
+ children: [
1201
+ /*#__PURE__*/ _jsx(DialogTitle, {
1202
+ children: `Runs — ${(_ref21 = runsFor == null ? void 0 : runsFor.name) != null ? _ref21 : ''}`
1203
+ }),
1204
+ /*#__PURE__*/ _jsx(DialogContent, {
1205
+ children: runsFor ? /*#__PURE__*/ _jsx(HostRunHistoryCard, {
1206
+ hostId: hostId,
1207
+ orgId: org == null ? void 0 : org.$id,
1208
+ targetId: runsFor.$id,
1209
+ targetType: "action",
1210
+ targetName: (_runsFor_name = runsFor.name) != null ? _runsFor_name : '',
1211
+ header: "Recent runs"
1212
+ }) : null
1213
+ }),
1214
+ /*#__PURE__*/ _jsx(DialogActions, {
1215
+ children: /*#__PURE__*/ _jsx(Button, {
1216
+ color: "inherit",
1217
+ onClick: ()=>setRunsFor(null),
1218
+ children: 'Close'
1219
+ })
1220
+ })
1221
+ ]
1222
+ })
1223
+ ]
1224
+ });
1225
+ }
1226
+ HostActionsCard.displayName = 'HostActionsCard';
1227
+ export default HostActionsCard;
1228
+
1229
+ //# sourceMappingURL=host-actions-card.component.js.map