@burdenoff/fluidgrids-fe-sdk 2026.1115.4 → 2026.1115.5

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.
@@ -6,7 +6,8 @@ import { A as useFlowStore } from "./node-operation-selection-DCJ_DDHr.js";
6
6
  import { $ as resolveLoadOptionsSpec, B as coerceNumber, Dt as useSaveStatusStore, G as paramsFromOperation, L as useRunGuard, Q as optionListValues, U as nodeTitle, W as optionText, X as isStringOptionList, Z as optionListText, cn as structuredParameterType, dn as isParamVisible, et as ConditionTreeBuilder, fn as useFluidGridsAnalytics, it as parseMappingValue, nt as casesFromRules, on as formatParameterValue, ot as boundFieldsOf, pn as openRunQuotaUpgradePrompt, sn as parseParameterValue, st as missingRequiredParameters, t as FluidGridsContext, tt as SwitchRulesEditor, z as asRecord } from "./FluidGridsProvider-CaOI_H63.js";
7
7
  import { n as useBuilderReadOnly } from "./read-only-context-Dt9N9SIG.js";
8
8
  import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
9
- import { L as triggerSchedule, T as pauseSchedule, t as backfillSchedule, u as describeSchedule, z as unpauseSchedule } from "./api-o5UUDbr8.js";
9
+ import { m as usePhaseCFlags } from "./feature-flags-B_b8PJ6Z.js";
10
+ import { L as triggerSchedule, T as pauseSchedule, t as backfillSchedule, u as describeSchedule, x as fetchSearchAttributes, z as unpauseSchedule } from "./api-o5UUDbr8.js";
10
11
  import { on as PreviewScheduleDocument, t as useAgentDefinitionList, vt as useRunFromNodeEligibilityQuery } from "./useAgentDefinitions-tsGlUO4v.js";
11
12
  import { r as describeRule, s as isAgentNodeType } from "./agent-form-D0uLvzx4.js";
12
13
  import { C as RegenerateEventRegistrationTokenDocument, S as StopWebhookTestListenerDocument, T as ListenForFormTestSubmissionDocument, _ as webhookDeliveriesPath, b as WorkflowCallInputsDocument, n as PopoverContent$1, r as PopoverTrigger$1, t as Popover$1, v as RunOnSection, w as ListenForWebhookTestEventDocument, x as WebhookTestListenerDocument, y as ConnectionSelector } from "./popover-BtbVqTzP.js";
@@ -14,7 +15,7 @@ import { a as collectExtensions } from "./quota-Cdt8yz7N.js";
14
15
  import { BO as ScheduleType, QI as WorkflowScheduleOverlapPolicy } from "./graphql-types-BjCOnRUQ.js";
15
16
  import { t as useGlobalStoreClient } from "./useGlobalStoreClient-ZLYG_Xls.js";
16
17
  import { a as operatorSpec, c as ruleFromRow, i as normaliseAssertRules, n as describeAssertRuleProblem, o as readAssertRulesSetting, r as emptyAssertRuleRow, s as rowsFromRules, t as ASSERT_OPERATORS } from "./assert-rules-BA2caUAZ.js";
17
- import { i as NativeSelect, o as StatusPill, t as FieldError, u as useResource } from "./shared-mbyvDPde.js";
18
+ import { i as NativeSelect, l as parseJsonObjectInput, o as StatusPill, t as FieldError, u as useResource } from "./shared-mbyvDPde.js";
18
19
  import { r as normalizeCodeLanguage, t as CodeEditorField } from "./code-editor-field-Cv907s3W.js";
19
20
  import { useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
20
21
  import { Link } from "react-router";
@@ -2486,7 +2487,7 @@ function defaultScheduleRule() {
2486
2487
  function isScheduleRuleField(value) {
2487
2488
  return typeof value === "string" && SCHEDULE_RULE_FIELDS.some((field) => field === value);
2488
2489
  }
2489
- function isRecord$4(value) {
2490
+ function isRecord$5(value) {
2490
2491
  return typeof value === "object" && value !== null && !Array.isArray(value);
2491
2492
  }
2492
2493
  function toNumber(value) {
@@ -2585,10 +2586,10 @@ function rulesFromLegacyState(state) {
2585
2586
  * array and the legacy fields, and always yields at least one rule.
2586
2587
  */
2587
2588
  function normalizeScheduleState(state) {
2588
- const source = isRecord$4(state) ? state : {};
2589
+ const source = isRecord$5(state) ? state : {};
2589
2590
  let rules;
2590
2591
  if (Array.isArray(source.rules)) {
2591
- rules = source.rules.filter(isRecord$4).map(normalizeScheduleRule);
2592
+ rules = source.rules.filter(isRecord$5).map(normalizeScheduleRule);
2592
2593
  if (rules.length === 0) rules = [defaultScheduleRule()];
2593
2594
  } else rules = rulesFromLegacyState(source);
2594
2595
  const timezone = toTrimmedString(source.timezone) || "UTC";
@@ -2603,10 +2604,10 @@ function normalizeScheduleState(state) {
2603
2604
  }
2604
2605
  /** Whether the saved state predates `rules[]` (so the editor is showing a conversion). */
2605
2606
  function isLegacyScheduleState(state) {
2606
- if (!isRecord$4(state) || Array.isArray(state.rules)) return false;
2607
+ if (!isRecord$5(state) || Array.isArray(state.rules)) return false;
2607
2608
  return state.mode !== void 0 || toTrimmedString(state.cronExpression) !== "" || toNumber(state.interval) !== void 0;
2608
2609
  }
2609
- function inRange(value, min, max) {
2610
+ function inRange$1(value, min, max) {
2610
2611
  return value !== void 0 && Number.isInteger(value) && value >= min && value <= max;
2611
2612
  }
2612
2613
  /** Humanised text for a cron expression, or null when cronstrue rejects it. */
@@ -2629,30 +2630,30 @@ function validateRule(rule) {
2629
2630
  const issues = [];
2630
2631
  switch (rule.field) {
2631
2632
  case "seconds":
2632
- if (!inRange(rule.secondsInterval, 1, 59)) issues.push("secondsRange");
2633
+ if (!inRange$1(rule.secondsInterval, 1, 59)) issues.push("secondsRange");
2633
2634
  break;
2634
2635
  case "minutes":
2635
- if (!inRange(rule.minutesInterval, 1, 59)) issues.push("minutesRange");
2636
+ if (!inRange$1(rule.minutesInterval, 1, 59)) issues.push("minutesRange");
2636
2637
  break;
2637
2638
  case "hours":
2638
- if (!inRange(rule.hoursInterval, 1, 23)) issues.push("hoursRange");
2639
+ if (!inRange$1(rule.hoursInterval, 1, 23)) issues.push("hoursRange");
2639
2640
  break;
2640
2641
  case "days":
2641
- if (!inRange(rule.daysInterval, 1, 31)) issues.push("daysRange");
2642
+ if (!inRange$1(rule.daysInterval, 1, 31)) issues.push("daysRange");
2642
2643
  break;
2643
2644
  case "weeks": {
2644
2645
  if (!isPositiveInteger(rule.weeksInterval)) issues.push("weeksMin");
2645
2646
  const days = rule.triggerAtDay;
2646
2647
  if (days === void 0 || days.length === 0) issues.push("weekdaysRequired");
2647
2648
  else {
2648
- if (!days.every((day) => inRange(day, 0, 6))) issues.push("weekdaysRange");
2649
+ if (!days.every((day) => inRange$1(day, 0, 6))) issues.push("weekdaysRange");
2649
2650
  if (new Set(days).size !== days.length) issues.push("weekdaysRepeat");
2650
2651
  }
2651
2652
  break;
2652
2653
  }
2653
2654
  case "months":
2654
2655
  if (!isPositiveInteger(rule.monthsInterval)) issues.push("monthsMin");
2655
- if (rule.triggerAtDayOfMonth !== void 0 && !inRange(rule.triggerAtDayOfMonth, 1, 31)) issues.push("dayOfMonthRange");
2656
+ if (rule.triggerAtDayOfMonth !== void 0 && !inRange$1(rule.triggerAtDayOfMonth, 1, 31)) issues.push("dayOfMonthRange");
2656
2657
  break;
2657
2658
  case "cronExpression": {
2658
2659
  const text = (rule.expression ?? "").trim();
@@ -2662,8 +2663,8 @@ function validateRule(rule) {
2662
2663
  return describeCron(text) === null ? ["cronInvalid"] : [];
2663
2664
  }
2664
2665
  }
2665
- if (rule.triggerAtHour !== void 0 && !inRange(rule.triggerAtHour, 0, 23)) issues.push("hourRange");
2666
- if (rule.triggerAtMinute !== void 0 && !inRange(rule.triggerAtMinute, 0, 59)) issues.push("minuteRange");
2666
+ if (rule.triggerAtHour !== void 0 && !inRange$1(rule.triggerAtHour, 0, 23)) issues.push("hourRange");
2667
+ if (rule.triggerAtMinute !== void 0 && !inRange$1(rule.triggerAtMinute, 0, 59)) issues.push("minuteRange");
2667
2668
  return issues;
2668
2669
  }
2669
2670
  /**
@@ -3296,7 +3297,7 @@ const CHOICE_TYPES = /* @__PURE__ */ new Set([
3296
3297
  const MULTI_CHOICE_TYPES = /* @__PURE__ */ new Set(["multiselect", "checkboxes"]);
3297
3298
  /** Types the service renders without a placeholder. */
3298
3299
  const NO_PLACEHOLDER_TYPES = /* @__PURE__ */ new Set(["checkbox", "hidden"]);
3299
- function isRecord$3(value) {
3300
+ function isRecord$4(value) {
3300
3301
  return typeof value === "object" && value !== null && !Array.isArray(value);
3301
3302
  }
3302
3303
  /**
@@ -3337,7 +3338,7 @@ function readFormFieldsSetting(value) {
3337
3338
  return null;
3338
3339
  }
3339
3340
  }
3340
- if (!Array.isArray(parsed) || !parsed.every(isRecord$3)) return null;
3341
+ if (!Array.isArray(parsed) || !parsed.every(isRecord$4)) return null;
3341
3342
  return parsed.map((field) => ({ ...field }));
3342
3343
  }
3343
3344
  /** The text the JSON view shows for a setting. */
@@ -3418,7 +3419,7 @@ function formFieldOptions(field) {
3418
3419
  if (!Array.isArray(field.options)) return [];
3419
3420
  return field.options.map((option) => {
3420
3421
  if (typeof option === "string") return option;
3421
- if (isRecord$3(option)) return String(option.value ?? option.label ?? "");
3422
+ if (isRecord$4(option)) return String(option.value ?? option.label ?? "");
3422
3423
  return option === void 0 || option === null ? "" : String(option);
3423
3424
  });
3424
3425
  }
@@ -3872,7 +3873,7 @@ const TOOL_APPROVAL_RULES = [
3872
3873
  function isRule(value) {
3873
3874
  return typeof value === "string" && TOOL_APPROVAL_RULES.includes(value);
3874
3875
  }
3875
- function isRecord$2(value) {
3876
+ function isRecord$3(value) {
3876
3877
  return typeof value === "object" && value !== null && !Array.isArray(value);
3877
3878
  }
3878
3879
  function parsed(value) {
@@ -3886,11 +3887,11 @@ function parsed(value) {
3886
3887
  /** The rows of a stored setting; rows with an unknown rule read as `ask`. */
3887
3888
  function readToolApprovalRows(value) {
3888
3889
  const raw = parsed(value);
3889
- if (Array.isArray(raw)) return raw.filter(isRecord$2).map((row) => ({
3890
+ if (Array.isArray(raw)) return raw.filter(isRecord$3).map((row) => ({
3890
3891
  toolName: typeof row.toolName === "string" ? row.toolName : "",
3891
3892
  rule: isRule(row.rule) ? row.rule : "ask"
3892
3893
  }));
3893
- if (isRecord$2(raw)) return Object.entries(raw).map(([toolName, rule]) => ({
3894
+ if (isRecord$3(raw)) return Object.entries(raw).map(([toolName, rule]) => ({
3894
3895
  toolName,
3895
3896
  rule: isRule(rule) ? rule : "ask"
3896
3897
  }));
@@ -3907,9 +3908,9 @@ function connectedToolNames(agentId, nodes, edges) {
3907
3908
  for (const edge of edges) {
3908
3909
  if (edge.source !== agentId || edge.sourceHandle !== "Tool") continue;
3909
3910
  const node = edge.target ? byId.get(edge.target) : void 0;
3910
- const args = isRecord$2(node?.data) ? node.data.args : void 0;
3911
- const state = isRecord$2(args) ? args.state : void 0;
3912
- const name = isRecord$2(state) ? state.toolName : void 0;
3911
+ const args = isRecord$3(node?.data) ? node.data.args : void 0;
3912
+ const state = isRecord$3(args) ? args.state : void 0;
3913
+ const name = isRecord$3(state) ? state.toolName : void 0;
3913
3914
  if (typeof name === "string" && name.trim()) names.add(name.trim());
3914
3915
  }
3915
3916
  return [...names];
@@ -4472,17 +4473,17 @@ const PLATFORM_EVENT_SOURCES_QUERY = gql`
4472
4473
  /** The list hardly changes; one read per few minutes is plenty. */
4473
4474
  const CACHE_TTL_MS$1 = 18e4;
4474
4475
  const caches$1 = /* @__PURE__ */ new WeakMap();
4475
- function isRecord$1(value) {
4476
+ function isRecord$2(value) {
4476
4477
  return typeof value === "object" && value !== null && !Array.isArray(value);
4477
4478
  }
4478
4479
  /** The well-formed sources of a `platformEventSources` response; anything else is dropped. */
4479
4480
  function readPlatformEventSources(data) {
4480
- const list = isRecord$1(data) ? data.platformEventSources : void 0;
4481
+ const list = isRecord$2(data) ? data.platformEventSources : void 0;
4481
4482
  if (!Array.isArray(list)) return [];
4482
4483
  const sources = [];
4483
4484
  for (const item of list) {
4484
- if (!isRecord$1(item) || typeof item.module !== "string") continue;
4485
- const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap((type) => isRecord$1(type) && typeof type.name === "string" ? [{
4485
+ if (!isRecord$2(item) || typeof item.module !== "string") continue;
4486
+ const recordTypes = Array.isArray(item.recordTypes) ? item.recordTypes.flatMap((type) => isRecord$2(type) && typeof type.name === "string" ? [{
4486
4487
  name: type.name,
4487
4488
  actions: Array.isArray(type.actions) ? type.actions.filter((action) => typeof action === "string") : []
4488
4489
  }] : []) : [];
@@ -4749,16 +4750,16 @@ const MY_TIME_TRIGGERS_QUERY = gql`
4749
4750
  */
4750
4751
  const CACHE_TTL_MS = 3e4;
4751
4752
  const caches = /* @__PURE__ */ new WeakMap();
4752
- function isRecord(value) {
4753
+ function isRecord$1(value) {
4753
4754
  return typeof value === "object" && value !== null && !Array.isArray(value);
4754
4755
  }
4755
4756
  /** The well-formed triggers of a `timeCampusMyTimeTriggers` response, sorted by name. */
4756
4757
  function readTimeTriggers(data) {
4757
- const list = isRecord(data) ? data.timeCampusMyTimeTriggers : void 0;
4758
+ const list = isRecord$1(data) ? data.timeCampusMyTimeTriggers : void 0;
4758
4759
  if (!Array.isArray(list)) return [];
4759
4760
  const triggers = [];
4760
4761
  for (const item of list) {
4761
- if (!isRecord(item) || typeof item.id !== "string" || !item.id) continue;
4762
+ if (!isRecord$1(item) || typeof item.id !== "string" || !item.id) continue;
4762
4763
  triggers.push({
4763
4764
  id: item.id,
4764
4765
  name: typeof item.name === "string" && item.name.trim() ? item.name.trim() : item.id,
@@ -8333,15 +8334,998 @@ function NodeExecutionActions({ targetNodeId, nodes, edges, logs, workflowKey, w
8333
8334
  });
8334
8335
  }
8335
8336
 
8337
+ // src/builder/components/run-control/schedule-spec.ts
8338
+ const SCHEDULE_SPEC_KINDS = [
8339
+ "cron",
8340
+ "calendar",
8341
+ "interval"
8342
+ ];
8343
+ /** Fields a spec may carry beside its one kind (SDK SCHEDULE_SPEC_FIELDS). */
8344
+ const SCHEDULE_SPEC_FIELDS = [
8345
+ "excludeCalendars",
8346
+ "startAt",
8347
+ "endAt",
8348
+ "timezone",
8349
+ "jitter"
8350
+ ];
8351
+ const CALENDAR_FIELDS = [
8352
+ {
8353
+ name: "second",
8354
+ min: 0,
8355
+ max: 59,
8356
+ defaultsToZero: true
8357
+ },
8358
+ {
8359
+ name: "minute",
8360
+ min: 0,
8361
+ max: 59,
8362
+ defaultsToZero: true
8363
+ },
8364
+ {
8365
+ name: "hour",
8366
+ min: 0,
8367
+ max: 23,
8368
+ defaultsToZero: true
8369
+ },
8370
+ {
8371
+ name: "dayOfMonth",
8372
+ min: 1,
8373
+ max: 31,
8374
+ defaultsToZero: false
8375
+ },
8376
+ {
8377
+ name: "month",
8378
+ min: 1,
8379
+ max: 12,
8380
+ defaultsToZero: false
8381
+ },
8382
+ {
8383
+ name: "dayOfWeek",
8384
+ min: 0,
8385
+ max: 6,
8386
+ defaultsToZero: false
8387
+ },
8388
+ {
8389
+ name: "year",
8390
+ min: 1970,
8391
+ max: 9999,
8392
+ defaultsToZero: false
8393
+ }
8394
+ ];
8395
+ /** The longest jitter the service accepts (one week, like the catch-up window). */
8396
+ const MAX_JITTER_SECONDS = 604800;
8397
+ /** The cron a new cron spec starts with (every hour, on the hour). */
8398
+ const DEFAULT_SPEC_CRON = "0 * * * *";
8399
+ function isRecord(value) {
8400
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8401
+ }
8402
+ /** The spec's kind, or null when the trigger has no usable spec. */
8403
+ function specKindOf(spec) {
8404
+ if (!isRecord(spec)) return null;
8405
+ const kinds = SCHEDULE_SPEC_KINDS.filter((kind) => spec[kind] !== void 0);
8406
+ return kinds.length === 1 ? kinds[0] ?? null : null;
8407
+ }
8408
+ /** The spec of a node state, as a record (an empty record when absent or not an object). */
8409
+ function specOf(state) {
8410
+ return isRecord(state.spec) ? state.spec : {};
8411
+ }
8412
+ /** A first entry for each kind, so a spec switched to it is valid at once. */
8413
+ function seedFor(kind) {
8414
+ if (kind === "cron") return DEFAULT_SPEC_CRON;
8415
+ if (kind === "calendar") return [{
8416
+ hour: [9],
8417
+ minute: [0]
8418
+ }];
8419
+ return [{ every: "1h" }];
8420
+ }
8421
+ /**
8422
+ * The spec switched to `kind`, keeping its window, timezone, jitter and
8423
+ * exclusions; `null` drops the spec (the trigger's rules apply again, and
8424
+ * the spec-only fields go with it, because they cannot exist without one).
8425
+ */
8426
+ function withSpecKind(spec, kind) {
8427
+ if (kind === null) return void 0;
8428
+ const current = isRecord(spec) ? spec : {};
8429
+ if (specKindOf(current) === kind) return current;
8430
+ const next = {};
8431
+ for (const field of SCHEDULE_SPEC_FIELDS) if (current[field] !== void 0) next[field] = current[field];
8432
+ next[kind] = seedFor(kind);
8433
+ return next;
8434
+ }
8435
+ /** The spec with one field set (undefined removes it). */
8436
+ function withSpecField(spec, field, value) {
8437
+ const next = { ...isRecord(spec) ? spec : {} };
8438
+ if (value === void 0) delete next[field];
8439
+ else next[field] = value;
8440
+ return next;
8441
+ }
8442
+ const SHORT_DURATION = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/;
8443
+ const ISO_DURATION = /^P(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
8444
+ const UNIT_MS = {
8445
+ ms: 1,
8446
+ s: 1e3,
8447
+ m: 6e4,
8448
+ h: 36e5,
8449
+ d: 864e5,
8450
+ w: 6048e5
8451
+ };
8452
+ /**
8453
+ * Milliseconds of a duration as the IR accepts it (the SDK's `durationMs`
8454
+ * with bare numbers in seconds): `90`, `'30s'`, `'15m'`, `'2h'`, `'1d'`,
8455
+ * `'1w'`, `'PT1H30M'`. Undefined when the value is not a duration.
8456
+ */
8457
+ function durationMsOf(value) {
8458
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value * 1e3 : void 0;
8459
+ if (typeof value !== "string") return void 0;
8460
+ const text = value.trim();
8461
+ const short = SHORT_DURATION.exec(text);
8462
+ if (short?.[1] !== void 0 && short[2] !== void 0) {
8463
+ const unit = UNIT_MS[short[2]];
8464
+ return unit === void 0 ? void 0 : Number(short[1]) * unit;
8465
+ }
8466
+ const iso = ISO_DURATION.exec(text);
8467
+ if (!iso || text === "P" || text === "PT") return void 0;
8468
+ const [, weeks, days, hours, minutes, seconds] = iso;
8469
+ if ([
8470
+ weeks,
8471
+ days,
8472
+ hours,
8473
+ minutes,
8474
+ seconds
8475
+ ].every((p) => p === void 0)) return void 0;
8476
+ return Number(weeks ?? 0) * 6048e5 + Number(days ?? 0) * 864e5 + Number(hours ?? 0) * 36e5 + Number(minutes ?? 0) * 6e4 + Number(seconds ?? 0) * 1e3;
8477
+ }
8478
+ /** The text a stored duration shows as (a number of seconds shows as `<n>s`). */
8479
+ function formatDuration(value) {
8480
+ if (typeof value === "number") return `${value}s`;
8481
+ return typeof value === "string" ? value : "";
8482
+ }
8483
+ /**
8484
+ * A duration typed in the panel. Empty is "no value"; digits alone are
8485
+ * seconds and stored as a number; anything else is kept as typed when the
8486
+ * IR reads it as a duration. `wholeSeconds` refuses fractions of a second
8487
+ * (interval entries run on the scheduler's one-second grid).
8488
+ */
8489
+ function parseDurationInput(text, options = {}) {
8490
+ const trimmed = text.trim();
8491
+ if (trimmed === "") return {
8492
+ ok: true,
8493
+ value: void 0
8494
+ };
8495
+ const value = /^\d+$/.test(trimmed) ? Number(trimmed) : trimmed;
8496
+ const ms = durationMsOf(value);
8497
+ if (ms === void 0) return {
8498
+ ok: false,
8499
+ error: "Enter a duration such as 30s, 15m, 2h, 1d or PT1H30M"
8500
+ };
8501
+ if (options.positive && ms <= 0) return {
8502
+ ok: false,
8503
+ error: "The duration must be longer than zero"
8504
+ };
8505
+ if (options.wholeSeconds && ms % 1e3 !== 0) return {
8506
+ ok: false,
8507
+ error: "Use whole seconds"
8508
+ };
8509
+ if (options.maxSeconds !== void 0 && ms > options.maxSeconds * 1e3) return {
8510
+ ok: false,
8511
+ error: `At most ${options.maxSeconds / 86400} days`
8512
+ };
8513
+ return {
8514
+ ok: true,
8515
+ value
8516
+ };
8517
+ }
8518
+ /** An interval entry's own problem (the service's `intervalEntryOf`), or null. */
8519
+ function intervalEntryProblem(entry) {
8520
+ if (!isRecord(entry)) return "Each interval entry must be an object";
8521
+ const every = durationMsOf(entry.every);
8522
+ if (every === void 0 || every < 1e3 || every % 1e3 !== 0) return "Every must be a whole number of seconds, at least 1s";
8523
+ const offset = entry.offset === void 0 ? 0 : durationMsOf(entry.offset);
8524
+ if (offset === void 0 || offset % 1e3 !== 0 || offset >= every) return "The offset must be a whole number of seconds below every";
8525
+ return null;
8526
+ }
8527
+ /** The text a stored calendar field shows as: `1,15`, `1-5`, `0-59/15`, `*`. */
8528
+ function formatCalendarField(value, field) {
8529
+ if (value === void 0) return "";
8530
+ return (Array.isArray(value) ? value : [value]).map((entry) => {
8531
+ if (typeof entry === "number") return String(entry);
8532
+ if (!isRecord(entry) || typeof entry.start !== "number") return "?";
8533
+ const start = entry.start;
8534
+ const end = typeof entry.end === "number" ? entry.end : start;
8535
+ const step = typeof entry.step === "number" ? entry.step : 1;
8536
+ if (start === field.min && end === field.max) return step === 1 ? "*" : `*/${step}`;
8537
+ const range = end === start ? String(start) : `${start}-${end}`;
8538
+ return step === 1 ? range : `${range}/${step}`;
8539
+ }).join(",");
8540
+ }
8541
+ function inRange(n, field) {
8542
+ return Number.isInteger(n) && n >= field.min && n <= field.max;
8543
+ }
8544
+ /**
8545
+ * A calendar field typed in the panel: comma-separated values `n`, ranges
8546
+ * `a-b`, steps `a-b/s`, `a/s` (to the field's end), `*` (every value) and
8547
+ * `*\/s`. Empty is "no value" (0 for second, minute and hour; any for the
8548
+ * others). `*` on a date field is the same as empty, so it is stored as
8549
+ * absent.
8550
+ */
8551
+ function parseCalendarField(text, field) {
8552
+ const trimmed = text.trim();
8553
+ if (trimmed === "") return {
8554
+ ok: true,
8555
+ value: void 0
8556
+ };
8557
+ const values = [];
8558
+ for (const raw of trimmed.split(",")) {
8559
+ const token = raw.trim();
8560
+ const match = /^(\*|\d+)(?:-(\d+))?(?:\/(\d+))?$/.exec(token);
8561
+ if (!match) return {
8562
+ ok: false,
8563
+ error: `"${token}" is not a value, a range (a-b) or a step (a-b/s)`
8564
+ };
8565
+ const [, first, last, stepText] = match;
8566
+ const step = stepText === void 0 ? 1 : Number(stepText);
8567
+ if (step < 1) return {
8568
+ ok: false,
8569
+ error: "A step must be at least 1"
8570
+ };
8571
+ if (first === "*") {
8572
+ if (last !== void 0) return {
8573
+ ok: false,
8574
+ error: `"${token}": * cannot start a range`
8575
+ };
8576
+ if (step === 1 && !field.defaultsToZero && trimmed === "*") return {
8577
+ ok: true,
8578
+ value: void 0
8579
+ };
8580
+ values.push(step === 1 ? {
8581
+ start: field.min,
8582
+ end: field.max
8583
+ } : {
8584
+ start: field.min,
8585
+ end: field.max,
8586
+ step
8587
+ });
8588
+ continue;
8589
+ }
8590
+ const start = Number(first);
8591
+ const end = last !== void 0 ? Number(last) : stepText !== void 0 ? field.max : start;
8592
+ if (!inRange(start, field) || !inRange(end, field)) return {
8593
+ ok: false,
8594
+ error: `Values must be between ${field.min} and ${field.max}`
8595
+ };
8596
+ if (end < start) return {
8597
+ ok: false,
8598
+ error: `"${token}": the range ends before it starts`
8599
+ };
8600
+ if (end === start && step === 1) values.push(start);
8601
+ else values.push(step === 1 ? {
8602
+ start,
8603
+ end
8604
+ } : {
8605
+ start,
8606
+ end,
8607
+ step
8608
+ });
8609
+ }
8610
+ return {
8611
+ ok: true,
8612
+ value: values
8613
+ };
8614
+ }
8615
+ /** A calendar entry with one field set (undefined removes it). */
8616
+ function withCalendarField(entry, field, value) {
8617
+ const next = { ...isRecord(entry) ? entry : {} };
8618
+ if (value === void 0 || value === "") delete next[field];
8619
+ else next[field] = value;
8620
+ return next;
8621
+ }
8622
+ /** A calendar entry's own problem (field values out of range), or null. */
8623
+ function calendarEntryProblem(entry) {
8624
+ if (!isRecord(entry)) return "Each calendar entry must be an object";
8625
+ for (const field of CALENDAR_FIELDS) {
8626
+ const value = entry[field.name];
8627
+ if (value === void 0) continue;
8628
+ const parsed = parseCalendarField(formatCalendarField(value, field), field);
8629
+ if (!parsed.ok) return `${field.name}: ${parsed.error}`;
8630
+ }
8631
+ return null;
8632
+ }
8633
+ /** Whether `zone` is an IANA time zone this browser knows. */
8634
+ function isTimeZone(zone) {
8635
+ if (zone.trim() === "") return false;
8636
+ try {
8637
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
8638
+ return true;
8639
+ } catch {
8640
+ return false;
8641
+ }
8642
+ }
8643
+ /** The `datetime-local` text of a stored ISO date-time (in the browser's zone). */
8644
+ function localInputOf(value) {
8645
+ if (typeof value !== "string") return "";
8646
+ const ms = Date.parse(value);
8647
+ if (Number.isNaN(ms)) return "";
8648
+ const date = new Date(ms);
8649
+ const pad = (n) => String(n).padStart(2, "0");
8650
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
8651
+ }
8652
+ /** The ISO date-time of a `datetime-local` text; undefined when empty or invalid. */
8653
+ function isoOfLocalInput(text) {
8654
+ if (text.trim() === "") return void 0;
8655
+ const ms = new Date(text).getTime();
8656
+ return Number.isNaN(ms) ? void 0 : new Date(ms).toISOString();
8657
+ }
8658
+ /** The spec's own problem with its window (`endAt` after `startAt`), or null. */
8659
+ function windowProblem(spec) {
8660
+ const start = typeof spec.startAt === "string" ? Date.parse(spec.startAt) : NaN;
8661
+ const end = typeof spec.endAt === "string" ? Date.parse(spec.endAt) : NaN;
8662
+ if (!Number.isNaN(start) && !Number.isNaN(end) && end <= start) return "The end must be after the start";
8663
+ return null;
8664
+ }
8665
+ /** The JSON text of a stored object field (`scheduleContext`, `scheduleSearchAttributes`). */
8666
+ function jsonTextOf(value) {
8667
+ return isRecord(value) ? JSON.stringify(value, null, 2) : "";
8668
+ }
8669
+ /** The names of a search attribute object that no definition declares. */
8670
+ function undefinedAttributeNames(attributes, definedNames) {
8671
+ if (!isRecord(attributes)) return [];
8672
+ const defined = new Set(definedNames);
8673
+ return Object.keys(attributes).filter((name) => !defined.has(name));
8674
+ }
8675
+
8676
+ // src/builder/components/run-control/schedule-spec-editor.tsx
8677
+ /**
8678
+ * The spec half of the Schedule Trigger's schedule panel (BOFF-13524, spec
8679
+ * code-first 11 §2.33 and §2.16): which fire times the schedule uses (the
8680
+ * trigger's own rules, or a cron, calendar or interval spec), the spec's
8681
+ * window, timezone, jitter and excluded calendars, and the schedule context
8682
+ * and search attributes every run it starts carries.
8683
+ *
8684
+ * Everything is written to the node's `args.state` (`spec`,
8685
+ * `scheduleContext`, `scheduleSearchAttributes`), which is what the service
8686
+ * reads when the workflow is saved. A field that is being typed and does not
8687
+ * parse yet is kept in the field and not written, with the reason under it,
8688
+ * so the node never holds a value the save would refuse for its shape.
8689
+ */
8690
+ /**
8691
+ * A text field whose typed text is kept while it does not parse. The parsed
8692
+ * value is written through `onValue` only when it parses; when the stored
8693
+ * value changes from elsewhere (undo, the canvas JSON), the text follows it.
8694
+ */
8695
+ function useDraft(formatted, parse, onValue) {
8696
+ const [text, setText] = useState(formatted);
8697
+ const [error, setError] = useState(null);
8698
+ const [followed, setFollowed] = useState(formatted);
8699
+ if (formatted !== followed) {
8700
+ setFollowed(formatted);
8701
+ setText(formatted);
8702
+ setError(null);
8703
+ }
8704
+ return {
8705
+ text,
8706
+ error,
8707
+ onChange: (next) => {
8708
+ setText(next);
8709
+ const parsed = parse(next);
8710
+ if (!parsed.ok) {
8711
+ setError(parsed.error);
8712
+ return;
8713
+ }
8714
+ setError(null);
8715
+ onValue(parsed.value);
8716
+ }
8717
+ };
8718
+ }
8719
+ function DraftInput({ id, label, formatted, parse, onValue, disabled, placeholder, testId, hint }) {
8720
+ const draft = useDraft(formatted, parse, onValue);
8721
+ return /* @__PURE__ */ jsxs("div", {
8722
+ className: "space-y-1",
8723
+ children: [
8724
+ /* @__PURE__ */ jsx(Label, {
8725
+ htmlFor: id,
8726
+ className: "text-xs",
8727
+ children: label
8728
+ }),
8729
+ /* @__PURE__ */ jsx(Input, {
8730
+ id,
8731
+ value: draft.text,
8732
+ disabled,
8733
+ placeholder,
8734
+ onChange: (event) => draft.onChange(event.target.value),
8735
+ "aria-invalid": draft.error ? true : void 0,
8736
+ "data-testid": testId
8737
+ }),
8738
+ hint && !draft.error && /* @__PURE__ */ jsx("p", {
8739
+ className: "text-xs text-muted-foreground",
8740
+ children: hint
8741
+ }),
8742
+ /* @__PURE__ */ jsx(FieldError, {
8743
+ message: draft.error,
8744
+ testId: `${testId}-error`
8745
+ })
8746
+ ]
8747
+ });
8748
+ }
8749
+ function DraftJson({ id, label, value, onValue, disabled, placeholder, testId }) {
8750
+ const draft = useDraft(jsonTextOf(value), (text) => {
8751
+ if (text.trim() === "") return {
8752
+ ok: true,
8753
+ value: void 0
8754
+ };
8755
+ const parsed = parseJsonObjectInput(text);
8756
+ return parsed.ok ? {
8757
+ ok: true,
8758
+ value: parsed.value
8759
+ } : parsed;
8760
+ }, onValue);
8761
+ return /* @__PURE__ */ jsxs("div", {
8762
+ className: "space-y-1.5",
8763
+ children: [
8764
+ /* @__PURE__ */ jsx(Label, {
8765
+ htmlFor: id,
8766
+ children: label
8767
+ }),
8768
+ /* @__PURE__ */ jsx(Textarea, {
8769
+ id,
8770
+ value: draft.text,
8771
+ disabled,
8772
+ rows: 4,
8773
+ placeholder,
8774
+ className: "font-mono text-xs",
8775
+ onChange: (event) => draft.onChange(event.target.value),
8776
+ "aria-invalid": draft.error ? true : void 0,
8777
+ "data-testid": testId
8778
+ }),
8779
+ /* @__PURE__ */ jsx(FieldError, {
8780
+ message: draft.error,
8781
+ testId: `${testId}-error`
8782
+ })
8783
+ ]
8784
+ });
8785
+ }
8786
+ const FIELD_LABELS = {
8787
+ second: ["fluidgrids.builder.schedule.spec.second", "Second"],
8788
+ minute: ["fluidgrids.builder.schedule.spec.minute", "Minute"],
8789
+ hour: ["fluidgrids.builder.schedule.spec.hour", "Hour"],
8790
+ dayOfMonth: ["fluidgrids.builder.schedule.spec.dayOfMonth", "Day of month"],
8791
+ month: ["fluidgrids.builder.schedule.spec.month", "Month"],
8792
+ dayOfWeek: ["fluidgrids.builder.schedule.spec.dayOfWeek", "Day of week"],
8793
+ year: ["fluidgrids.builder.schedule.spec.year", "Year"]
8794
+ };
8795
+ /** One calendar entry (a spec entry or an excluded calendar). */
8796
+ function CalendarEntryEditor({ entry, index, testId, onChange, onRemove, disabled }) {
8797
+ const tr = useTr();
8798
+ const problem = calendarEntryProblem(entry);
8799
+ const comment = isRecord(entry) && typeof entry.comment === "string" ? entry.comment : "";
8800
+ return /* @__PURE__ */ jsxs("div", {
8801
+ className: "space-y-2 rounded-md border border-border p-3",
8802
+ "data-testid": `${testId}-${index}`,
8803
+ children: [
8804
+ /* @__PURE__ */ jsx("div", {
8805
+ className: "grid grid-cols-2 gap-2 sm:grid-cols-4",
8806
+ children: CALENDAR_FIELDS.map((field) => {
8807
+ const [key, fallback] = FIELD_LABELS[field.name];
8808
+ return /* @__PURE__ */ jsx(DraftInput, {
8809
+ id: `${testId}-${index}-${field.name}`,
8810
+ label: tr(key, fallback),
8811
+ formatted: formatCalendarField(isRecord(entry) ? entry[field.name] : void 0, field),
8812
+ parse: (text) => parseCalendarField(text, field),
8813
+ onValue: (value) => onChange(withCalendarField(entry, field.name, value)),
8814
+ disabled,
8815
+ placeholder: field.defaultsToZero ? "0" : "*",
8816
+ testId: `${testId}-${index}-${field.name}`
8817
+ }, field.name);
8818
+ })
8819
+ }),
8820
+ /* @__PURE__ */ jsx(Input, {
8821
+ value: comment,
8822
+ disabled,
8823
+ maxLength: 200,
8824
+ placeholder: tr("fluidgrids.builder.schedule.spec.comment", "Comment (optional)"),
8825
+ onChange: (event) => onChange(withCalendarField(entry, "comment", event.target.value)),
8826
+ "data-testid": `${testId}-${index}-comment`
8827
+ }),
8828
+ /* @__PURE__ */ jsx(FieldError, { message: problem }),
8829
+ onRemove && !disabled && /* @__PURE__ */ jsxs(Button$1, {
8830
+ size: "sm",
8831
+ variant: "ghost",
8832
+ onClick: onRemove,
8833
+ "data-testid": `${testId}-${index}-remove`,
8834
+ children: [/* @__PURE__ */ jsx(Icon, {
8835
+ icon: "mdi:delete-outline",
8836
+ className: "mr-1",
8837
+ width: 14
8838
+ }), tr("fluidgrids.builder.schedule.spec.remove", "Remove")]
8839
+ })
8840
+ ]
8841
+ });
8842
+ }
8843
+ function listOf(value) {
8844
+ return Array.isArray(value) ? value : [];
8845
+ }
8846
+ function replaced(list, index, entry) {
8847
+ return list.map((current, at) => at === index ? entry : current);
8848
+ }
8849
+ function removed(list, index) {
8850
+ return list.filter((_, at) => at !== index);
8851
+ }
8852
+ let lastEntryKey = 0;
8853
+ function freshEntryKeys(count) {
8854
+ return Array.from({ length: count }, () => {
8855
+ lastEntryKey += 1;
8856
+ return `entry-${lastEntryKey}`;
8857
+ });
8858
+ }
8859
+ /**
8860
+ * Stable React keys for a list of plain entries, which carry no ids of
8861
+ * their own. Adding and removing through the returned helpers keeps every
8862
+ * other row's key, so a row's half-typed text stays with that row. A change
8863
+ * of length from elsewhere (undo, the canvas JSON) starts a fresh set of
8864
+ * keys, which remounts the rows with the stored values.
8865
+ */
8866
+ function useEntryKeys(length) {
8867
+ const [keys, setKeys] = useState(() => freshEntryKeys(length));
8868
+ let current = keys;
8869
+ if (keys.length !== length) {
8870
+ current = freshEntryKeys(length);
8871
+ setKeys(current);
8872
+ }
8873
+ return {
8874
+ keys: current,
8875
+ add: () => setKeys([...current, ...freshEntryKeys(1)]),
8876
+ remove: (index) => setKeys(current.filter((_, at) => at !== index))
8877
+ };
8878
+ }
8879
+ /** A list of calendar entries: the spec's calendar or its exclusions. */
8880
+ function CalendarEntryList({ entries, minimum, newEntry, onEntries, disabled, entryTestId, addTestId, addLabel }) {
8881
+ const rows = useEntryKeys(entries.length);
8882
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [entries.map((entry, index) => /* @__PURE__ */ jsx(CalendarEntryEditor, {
8883
+ entry,
8884
+ index,
8885
+ testId: entryTestId,
8886
+ disabled,
8887
+ onChange: (next) => onEntries(replaced(entries, index, next)),
8888
+ onRemove: entries.length > minimum ? () => {
8889
+ rows.remove(index);
8890
+ onEntries(removed(entries, index));
8891
+ } : null
8892
+ }, rows.keys[index])), !disabled && /* @__PURE__ */ jsxs(Button$1, {
8893
+ size: "sm",
8894
+ variant: "outline",
8895
+ onClick: () => {
8896
+ rows.add();
8897
+ onEntries([...entries, newEntry]);
8898
+ },
8899
+ "data-testid": addTestId,
8900
+ children: [/* @__PURE__ */ jsx(Icon, {
8901
+ icon: "mdi:plus",
8902
+ className: "mr-1",
8903
+ width: 14
8904
+ }), addLabel]
8905
+ })] });
8906
+ }
8907
+ /** One interval entry: its length and optional offset. */
8908
+ function IntervalEntryEditor({ entry, index, onChange, onRemove, disabled }) {
8909
+ const tr = useTr();
8910
+ const record = isRecord(entry) ? entry : {};
8911
+ const testId = `schedule-spec-interval-${index}`;
8912
+ return /* @__PURE__ */ jsxs("div", {
8913
+ className: "space-y-2 rounded-md border border-border p-3",
8914
+ "data-testid": `schedule-spec-interval-entry-${index}`,
8915
+ children: [
8916
+ /* @__PURE__ */ jsxs("div", {
8917
+ className: "grid gap-2 sm:grid-cols-2",
8918
+ children: [/* @__PURE__ */ jsx(DraftInput, {
8919
+ id: `${testId}-every`,
8920
+ label: tr("fluidgrids.builder.schedule.spec.every", "Every"),
8921
+ formatted: formatDuration(record.every),
8922
+ parse: (text) => text.trim() === "" ? {
8923
+ ok: false,
8924
+ error: "Every is required"
8925
+ } : parseDurationInput(text, {
8926
+ wholeSeconds: true,
8927
+ positive: true
8928
+ }),
8929
+ onValue: (value) => onChange({
8930
+ ...record,
8931
+ every: value
8932
+ }),
8933
+ disabled,
8934
+ placeholder: "15m",
8935
+ testId: `${testId}-every`
8936
+ }), /* @__PURE__ */ jsx(DraftInput, {
8937
+ id: `${testId}-offset`,
8938
+ label: tr("fluidgrids.builder.schedule.spec.offset", "Offset (optional)"),
8939
+ formatted: formatDuration(record.offset),
8940
+ parse: (text) => parseDurationInput(text, { wholeSeconds: true }),
8941
+ onValue: (value) => {
8942
+ const next = { ...record };
8943
+ if (value === void 0) delete next.offset;
8944
+ else next.offset = value;
8945
+ onChange(next);
8946
+ },
8947
+ disabled,
8948
+ placeholder: "0s",
8949
+ testId: `${testId}-offset`
8950
+ })]
8951
+ }),
8952
+ /* @__PURE__ */ jsx(FieldError, { message: intervalEntryProblem(entry) }),
8953
+ onRemove && !disabled && /* @__PURE__ */ jsxs(Button$1, {
8954
+ size: "sm",
8955
+ variant: "ghost",
8956
+ onClick: onRemove,
8957
+ "data-testid": `${testId}-remove`,
8958
+ children: [/* @__PURE__ */ jsx(Icon, {
8959
+ icon: "mdi:delete-outline",
8960
+ className: "mr-1",
8961
+ width: 14
8962
+ }), tr("fluidgrids.builder.schedule.spec.remove", "Remove")]
8963
+ })
8964
+ ]
8965
+ });
8966
+ }
8967
+ function IntervalList({ entries, onEntries, disabled }) {
8968
+ const tr = useTr();
8969
+ const rows = useEntryKeys(entries.length);
8970
+ return /* @__PURE__ */ jsxs("div", {
8971
+ className: "space-y-2",
8972
+ "data-testid": "schedule-spec-interval",
8973
+ children: [
8974
+ /* @__PURE__ */ jsx("p", {
8975
+ className: "text-xs text-muted-foreground",
8976
+ children: tr("fluidgrids.builder.schedule.spec.intervalHint", "An interval fires at every multiple of its length since 1970-01-01 UTC, moved by the offset.")
8977
+ }),
8978
+ entries.map((entry, index) => /* @__PURE__ */ jsx(IntervalEntryEditor, {
8979
+ entry,
8980
+ index,
8981
+ disabled,
8982
+ onChange: (next) => onEntries(replaced(entries, index, next)),
8983
+ onRemove: entries.length > 1 ? () => {
8984
+ rows.remove(index);
8985
+ onEntries(removed(entries, index));
8986
+ } : null
8987
+ }, rows.keys[index])),
8988
+ !disabled && /* @__PURE__ */ jsxs(Button$1, {
8989
+ size: "sm",
8990
+ variant: "outline",
8991
+ onClick: () => {
8992
+ rows.add();
8993
+ onEntries([...entries, { every: "1h" }]);
8994
+ },
8995
+ "data-testid": "schedule-spec-interval-add",
8996
+ children: [/* @__PURE__ */ jsx(Icon, {
8997
+ icon: "mdi:plus",
8998
+ className: "mr-1",
8999
+ width: 14
9000
+ }), tr("fluidgrids.builder.schedule.spec.addInterval", "Add an interval")]
9001
+ })
9002
+ ]
9003
+ });
9004
+ }
9005
+ function parseCron(text) {
9006
+ const trimmed = text.trim();
9007
+ const fields = trimmed.split(/\s+/);
9008
+ return trimmed !== "" && fields.length >= 5 && fields.length <= 6 ? {
9009
+ ok: true,
9010
+ value: trimmed
9011
+ } : {
9012
+ ok: false,
9013
+ error: "Enter a cron expression with 5 or 6 fields"
9014
+ };
9015
+ }
9016
+ function parseTimeZone(text) {
9017
+ const zone = text.trim();
9018
+ if (zone === "") return {
9019
+ ok: true,
9020
+ value: void 0
9021
+ };
9022
+ return isTimeZone(zone) ? {
9023
+ ok: true,
9024
+ value: zone
9025
+ } : {
9026
+ ok: false,
9027
+ error: "Enter an IANA time zone, such as Asia/Kolkata"
9028
+ };
9029
+ }
9030
+ /** The choice between the trigger's rules and a cron, calendar or interval. */
9031
+ function SpecKindField({ storedSpec, kind, disabled, onSpec }) {
9032
+ const tr = useTr();
9033
+ return /* @__PURE__ */ jsxs("div", {
9034
+ className: "space-y-1.5",
9035
+ children: [
9036
+ /* @__PURE__ */ jsx(Label, {
9037
+ htmlFor: "schedule-spec-kind",
9038
+ children: tr("fluidgrids.builder.schedule.spec.kind", "When the schedule fires")
9039
+ }),
9040
+ /* @__PURE__ */ jsxs(NativeSelect, {
9041
+ id: "schedule-spec-kind",
9042
+ value: kind ?? "",
9043
+ disabled,
9044
+ onChange: (value) => onSpec(withSpecKind(storedSpec, SCHEDULE_SPEC_KINDS.find((candidate) => candidate === value) ?? null)),
9045
+ testId: "schedule-spec-kind",
9046
+ children: [
9047
+ /* @__PURE__ */ jsx("option", {
9048
+ value: "",
9049
+ children: tr("fluidgrids.builder.schedule.spec.rules", "The trigger's rules (Parameters tab)")
9050
+ }),
9051
+ /* @__PURE__ */ jsx("option", {
9052
+ value: "cron",
9053
+ children: tr("fluidgrids.builder.schedule.spec.cron", "Cron expression")
9054
+ }),
9055
+ /* @__PURE__ */ jsx("option", {
9056
+ value: "calendar",
9057
+ children: tr("fluidgrids.builder.schedule.spec.calendar", "Calendar entries")
9058
+ }),
9059
+ /* @__PURE__ */ jsx("option", {
9060
+ value: "interval",
9061
+ children: tr("fluidgrids.builder.schedule.spec.interval", "Intervals")
9062
+ })
9063
+ ]
9064
+ }),
9065
+ /* @__PURE__ */ jsx("p", {
9066
+ className: "text-xs text-muted-foreground",
9067
+ children: kind ? tr("fluidgrids.builder.schedule.spec.replacesRules", "These fire times replace the trigger's rules. Choose the trigger's rules again to drop them, with the window, jitter and excluded calendars.") : tr("fluidgrids.builder.schedule.spec.rulesHint", "A start and end, jitter and excluded calendars need fire times defined here.")
9068
+ }),
9069
+ storedSpec !== void 0 && !kind && /* @__PURE__ */ jsx(FieldError, {
9070
+ message: tr("fluidgrids.builder.schedule.spec.invalid", "The stored spec needs exactly one of cron, calendar or interval. Choose one to replace it."),
9071
+ testId: "schedule-spec-invalid"
9072
+ })
9073
+ ]
9074
+ });
9075
+ }
9076
+ /** The fire times themselves, for the chosen kind. */
9077
+ function SpecFireTimes({ kind, spec, setField, disabled }) {
9078
+ const tr = useTr();
9079
+ if (kind === "cron") return /* @__PURE__ */ jsx(DraftInput, {
9080
+ id: "schedule-spec-cron",
9081
+ label: tr("fluidgrids.builder.schedule.spec.cronLabel", "Cron (5 or 6 fields)"),
9082
+ formatted: typeof spec.cron === "string" ? spec.cron : "",
9083
+ parse: parseCron,
9084
+ onValue: (value) => setField("cron", value),
9085
+ disabled,
9086
+ placeholder: "0 9 * * 1-5",
9087
+ testId: "schedule-spec-cron"
9088
+ });
9089
+ if (kind === "interval") return /* @__PURE__ */ jsx(IntervalList, {
9090
+ entries: listOf(spec.interval),
9091
+ onEntries: (next) => setField("interval", next),
9092
+ disabled
9093
+ });
9094
+ return /* @__PURE__ */ jsxs("div", {
9095
+ className: "space-y-2",
9096
+ "data-testid": "schedule-spec-calendar",
9097
+ children: [/* @__PURE__ */ jsx("p", {
9098
+ className: "text-xs text-muted-foreground",
9099
+ children: tr("fluidgrids.builder.schedule.spec.calendarHint", "Each field takes values, ranges and steps (1,15 or 1-5 or 0-59/15). Empty second, minute and hour mean 0; empty date fields mean any. Day of week 0 is Sunday. Every field of an entry must match.")
9100
+ }), /* @__PURE__ */ jsx(CalendarEntryList, {
9101
+ entries: listOf(spec.calendar),
9102
+ minimum: 1,
9103
+ newEntry: {
9104
+ hour: [9],
9105
+ minute: [0]
9106
+ },
9107
+ onEntries: (next) => setField("calendar", next),
9108
+ disabled,
9109
+ entryTestId: "schedule-spec-calendar-entry",
9110
+ addTestId: "schedule-spec-calendar-add",
9111
+ addLabel: tr("fluidgrids.builder.schedule.spec.addCalendar", "Add a calendar entry")
9112
+ })]
9113
+ });
9114
+ }
9115
+ const WHOLE_DAY_EXCLUSION = {
9116
+ second: [{
9117
+ start: 0,
9118
+ end: 59
9119
+ }],
9120
+ minute: [{
9121
+ start: 0,
9122
+ end: 59
9123
+ }],
9124
+ hour: [{
9125
+ start: 0,
9126
+ end: 23
9127
+ }],
9128
+ dayOfWeek: [0, 6]
9129
+ };
9130
+ function DateTimeField({ id, label, value, onValue, disabled }) {
9131
+ return /* @__PURE__ */ jsxs("div", {
9132
+ className: "space-y-1",
9133
+ children: [/* @__PURE__ */ jsx(Label, {
9134
+ htmlFor: id,
9135
+ className: "text-xs",
9136
+ children: label
9137
+ }), /* @__PURE__ */ jsx(Input, {
9138
+ id,
9139
+ type: "datetime-local",
9140
+ disabled,
9141
+ value: localInputOf(value),
9142
+ onChange: (event) => onValue(isoOfLocalInput(event.target.value)),
9143
+ "data-testid": id
9144
+ })]
9145
+ });
9146
+ }
9147
+ /** The window, time zone, jitter and excluded calendars of a spec. */
9148
+ function SpecOptions({ kind, spec, setField, disabled }) {
9149
+ const tr = useTr();
9150
+ return /* @__PURE__ */ jsxs("div", {
9151
+ className: "space-y-3",
9152
+ "data-testid": "schedule-spec-options",
9153
+ children: [
9154
+ /* @__PURE__ */ jsxs("div", {
9155
+ className: "grid gap-3 sm:grid-cols-2",
9156
+ children: [/* @__PURE__ */ jsx(DateTimeField, {
9157
+ id: "schedule-spec-start",
9158
+ label: tr("fluidgrids.builder.schedule.spec.startAt", "Start (optional)"),
9159
+ value: spec.startAt,
9160
+ onValue: (value) => setField("startAt", value),
9161
+ disabled
9162
+ }), /* @__PURE__ */ jsx(DateTimeField, {
9163
+ id: "schedule-spec-end",
9164
+ label: tr("fluidgrids.builder.schedule.spec.endAt", "End (optional)"),
9165
+ value: spec.endAt,
9166
+ onValue: (value) => setField("endAt", value),
9167
+ disabled
9168
+ })]
9169
+ }),
9170
+ /* @__PURE__ */ jsx(FieldError, {
9171
+ message: windowProblem(spec),
9172
+ testId: "schedule-spec-window-error"
9173
+ }),
9174
+ /* @__PURE__ */ jsxs("div", {
9175
+ className: "grid gap-3 sm:grid-cols-2",
9176
+ children: [/* @__PURE__ */ jsx(DraftInput, {
9177
+ id: "schedule-spec-timezone",
9178
+ label: tr("fluidgrids.builder.schedule.spec.timezone", "Time zone (optional)"),
9179
+ formatted: typeof spec.timezone === "string" ? spec.timezone : "",
9180
+ parse: parseTimeZone,
9181
+ onValue: (value) => setField("timezone", value),
9182
+ disabled,
9183
+ placeholder: "Asia/Kolkata",
9184
+ hint: kind === "interval" ? tr("fluidgrids.builder.schedule.spec.timezoneInterval", "Intervals always run on UTC; the time zone applies to excluded calendars.") : tr("fluidgrids.builder.schedule.spec.timezoneHint", "Empty uses the trigger's time zone."),
9185
+ testId: "schedule-spec-timezone"
9186
+ }), /* @__PURE__ */ jsx(DraftInput, {
9187
+ id: "schedule-spec-jitter",
9188
+ label: tr("fluidgrids.builder.schedule.spec.jitter", "Jitter (optional)"),
9189
+ formatted: formatDuration(spec.jitter),
9190
+ parse: (text) => parseDurationInput(text, { maxSeconds: MAX_JITTER_SECONDS }),
9191
+ onValue: (value) => setField("jitter", value),
9192
+ disabled,
9193
+ placeholder: "30s",
9194
+ hint: tr("fluidgrids.builder.schedule.spec.jitterHint", "Each fire waits a random time up to this long."),
9195
+ testId: "schedule-spec-jitter"
9196
+ })]
9197
+ }),
9198
+ /* @__PURE__ */ jsxs("div", {
9199
+ className: "space-y-2",
9200
+ "data-testid": "schedule-spec-excludes",
9201
+ children: [
9202
+ /* @__PURE__ */ jsx("p", {
9203
+ className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
9204
+ children: tr("fluidgrids.builder.schedule.spec.excludes", "Excluded calendars")
9205
+ }),
9206
+ /* @__PURE__ */ jsx("p", {
9207
+ className: "text-xs text-muted-foreground",
9208
+ children: tr("fluidgrids.builder.schedule.spec.excludesHint", "A fire time that matches any of these is skipped (recorded as excluded). Empty second, minute and hour mean 0, so leave them out only for a whole-day exclusion with 0-59 and 0-23.")
9209
+ }),
9210
+ /* @__PURE__ */ jsx(CalendarEntryList, {
9211
+ entries: listOf(spec.excludeCalendars),
9212
+ minimum: 0,
9213
+ newEntry: WHOLE_DAY_EXCLUSION,
9214
+ onEntries: (next) => setField("excludeCalendars", next.length > 0 ? next : void 0),
9215
+ disabled,
9216
+ entryTestId: "schedule-spec-exclude",
9217
+ addTestId: "schedule-spec-exclude-add",
9218
+ addLabel: tr("fluidgrids.builder.schedule.spec.addExclude", "Add an excluded calendar")
9219
+ })
9220
+ ]
9221
+ })
9222
+ ]
9223
+ });
9224
+ }
9225
+ /** What the search attribute definitions say about the typed names. */
9226
+ function SearchAttributeNotes({ value, searchAttributesOn }) {
9227
+ const tr = useTr();
9228
+ const definitions = useResource(searchAttributesOn ? "schedule:search-attribute-definitions" : null, searchAttributesOn, fetchSearchAttributes);
9229
+ if (!searchAttributesOn) return /* @__PURE__ */ jsx("p", {
9230
+ className: "text-xs text-muted-foreground",
9231
+ "data-testid": "schedule-search-attributes-off",
9232
+ children: tr("fluidgrids.builder.schedule.spec.searchAttributesOff", "Search attributes are applied to runs only while search attributes are enabled for this workspace.")
9233
+ });
9234
+ if (!definitions.data) return null;
9235
+ const names = definitions.data.map((definition) => definition.name);
9236
+ const unknownNames = undefinedAttributeNames(value, names);
9237
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", {
9238
+ className: "text-xs text-muted-foreground",
9239
+ "data-testid": "schedule-search-attributes-defined",
9240
+ children: names.length > 0 ? tr("fluidgrids.builder.schedule.spec.definedAttributes", "Defined: {{names}}", { names: names.join(", ") }) : tr("fluidgrids.builder.schedule.spec.noDefinedAttributes", "No search attributes are defined yet (Settings, Run control).")
9241
+ }), unknownNames.length > 0 && /* @__PURE__ */ jsx(FieldError, {
9242
+ message: tr("fluidgrids.builder.schedule.spec.undefinedAttributes", "Not defined, so left out when a run starts: {{names}}", { names: unknownNames.join(", ") }),
9243
+ testId: "schedule-search-attributes-undefined"
9244
+ })] });
9245
+ }
9246
+ function ScheduleSpecEditor({ state, writeField, canUpdate, searchAttributesOn }) {
9247
+ const tr = useTr();
9248
+ const spec = specOf(state);
9249
+ const kind = specKindOf(state.spec);
9250
+ const disabled = !canUpdate;
9251
+ const writeSpec = (next) => writeField("spec", next);
9252
+ const setField = (field, value) => writeSpec(withSpecField(spec, field, value));
9253
+ return /* @__PURE__ */ jsxs("div", {
9254
+ className: "space-y-5",
9255
+ "data-testid": "schedule-spec",
9256
+ children: [/* @__PURE__ */ jsxs("section", {
9257
+ className: "space-y-3",
9258
+ children: [
9259
+ /* @__PURE__ */ jsx("h4", {
9260
+ className: "text-sm font-semibold text-foreground",
9261
+ children: tr("fluidgrids.builder.schedule.spec.title", "Fire times")
9262
+ }),
9263
+ /* @__PURE__ */ jsx(SpecKindField, {
9264
+ storedSpec: state.spec,
9265
+ kind,
9266
+ disabled,
9267
+ onSpec: writeSpec
9268
+ }),
9269
+ kind && /* @__PURE__ */ jsx(SpecFireTimes, {
9270
+ kind,
9271
+ spec,
9272
+ setField,
9273
+ disabled
9274
+ }),
9275
+ kind && /* @__PURE__ */ jsx(SpecOptions, {
9276
+ kind,
9277
+ spec,
9278
+ setField,
9279
+ disabled
9280
+ })
9281
+ ]
9282
+ }), /* @__PURE__ */ jsxs("section", {
9283
+ className: "space-y-3",
9284
+ "data-testid": "schedule-run-fields",
9285
+ children: [
9286
+ /* @__PURE__ */ jsx("h4", {
9287
+ className: "text-sm font-semibold text-foreground",
9288
+ children: tr("fluidgrids.builder.schedule.spec.runFields", "Every run the schedule starts")
9289
+ }),
9290
+ /* @__PURE__ */ jsx(DraftJson, {
9291
+ id: "schedule-context",
9292
+ label: tr("fluidgrids.builder.schedule.spec.context", "Schedule context (JSON object, the run reads it as context.scheduleContext)"),
9293
+ value: state.scheduleContext,
9294
+ onValue: (value) => writeField("scheduleContext", value),
9295
+ disabled,
9296
+ placeholder: "{\"region\": \"eu\"}",
9297
+ testId: "schedule-context"
9298
+ }),
9299
+ /* @__PURE__ */ jsx(DraftJson, {
9300
+ id: "schedule-search-attributes",
9301
+ label: tr("fluidgrids.builder.schedule.spec.searchAttributes", "Search attributes (JSON object of defined attributes)"),
9302
+ value: state.scheduleSearchAttributes,
9303
+ onValue: (value) => writeField("scheduleSearchAttributes", value),
9304
+ disabled,
9305
+ placeholder: "{\"CustomerTier\": \"gold\"}",
9306
+ testId: "schedule-search-attributes"
9307
+ }),
9308
+ /* @__PURE__ */ jsx(SearchAttributeNotes, {
9309
+ value: state.scheduleSearchAttributes,
9310
+ searchAttributesOn
9311
+ })
9312
+ ]
9313
+ })]
9314
+ });
9315
+ }
9316
+
8336
9317
  // src/builder/components/run-control/schedule-panel.tsx
8337
9318
  /**
8338
9319
  * The schedule panel of a Schedule Trigger node (BOFF-9906, spec code-first
8339
9320
  * 11 §2.16 and §2.33), shown in the node's detail view while
8340
- * fluidgrids.schedule-policies is on. It edits the policy fields the service
8341
- * reads from the node's state when the workflow is saved (overlap policy,
8342
- * catch-up window, pause on failure, limited actions) and, for a saved
8343
- * workflow, describes the live schedule: its state, next fire times and
8344
- * recent ticks, with Trigger now, Backfill and Pause with a note.
9321
+ * fluidgrids.schedule-policies is on. It edits every schedule field the
9322
+ * service reads from the node's state when the workflow is saved: the policy
9323
+ * (overlap policy, catch-up window, pause on failure, limited actions), the
9324
+ * spec (cron, calendar or interval fire times, start and end, time zone,
9325
+ * jitter, excluded calendars) and the schedule context and search
9326
+ * attributes (BOFF-13524, in ScheduleSpecEditor). For a saved workflow it
9327
+ * also describes the live schedule: its state, next fire times and recent
9328
+ * ticks, with Trigger now, Backfill and Pause with a note.
8345
9329
  *
8346
9330
  * A schedule saved without a policy keeps the legacy behaviour (every fire
8347
9331
  * starts a run); SKIP is only the default the panel suggests for a new
@@ -8377,6 +9361,7 @@ function secondsOf(value) {
8377
9361
  }
8378
9362
  function SchedulePanel({ state, writeField, workflowKey, triggerNodeId, canExecute, canUpdate }) {
8379
9363
  const tr = useTr();
9364
+ const { searchAttributes: searchAttributesOn } = usePhaseCFlags();
8380
9365
  const policy = overlapPolicyOf(state.overlapPolicy);
8381
9366
  const catchup = secondsOf(state.catchupWindow);
8382
9367
  const limited = typeof state.limitedActions === "number" ? state.limitedActions : null;
@@ -8404,306 +9389,315 @@ function SchedulePanel({ state, writeField, workflowKey, triggerNodeId, canExecu
8404
9389
  return /* @__PURE__ */ jsxs("div", {
8405
9390
  className: "space-y-5 p-4",
8406
9391
  "data-testid": "schedule-panel",
8407
- children: [/* @__PURE__ */ jsxs("section", {
8408
- className: "space-y-3",
8409
- children: [
8410
- /* @__PURE__ */ jsx("h4", {
8411
- className: "text-sm font-semibold text-foreground",
8412
- children: tr("fluidgrids.builder.schedule.policy", "Policy")
8413
- }),
8414
- /* @__PURE__ */ jsxs("div", {
8415
- className: "space-y-1.5",
8416
- children: [
8417
- /* @__PURE__ */ jsx(Label, {
8418
- htmlFor: "schedule-overlap",
8419
- children: tr("fluidgrids.builder.schedule.overlap", "When the previous run is still open")
8420
- }),
8421
- /* @__PURE__ */ jsxs(NativeSelect, {
8422
- id: "schedule-overlap",
8423
- value: policy ?? "",
8424
- disabled: !canUpdate,
8425
- onChange: (value) => writeField("overlapPolicy", overlapPolicyOf(value) ?? void 0),
8426
- testId: "schedule-overlap",
8427
- children: [
8428
- /* @__PURE__ */ jsx("option", {
8429
- value: "",
8430
- children: tr("fluidgrids.builder.schedule.overlapLegacy", "Start every run (no policy)")
8431
- }),
8432
- /* @__PURE__ */ jsx("option", {
8433
- value: "SKIP",
8434
- children: tr("fluidgrids.builder.schedule.skip", "Skip the fire")
8435
- }),
8436
- /* @__PURE__ */ jsx("option", {
8437
- value: "BUFFER_ONE",
8438
- children: tr("fluidgrids.builder.schedule.bufferOne", "Run once after it ends")
8439
- }),
8440
- /* @__PURE__ */ jsx("option", {
8441
- value: "BUFFER_ALL",
8442
- children: tr("fluidgrids.builder.schedule.bufferAll", "Run every missed fire after it ends")
8443
- }),
8444
- /* @__PURE__ */ jsx("option", {
8445
- value: "CANCEL_OTHER",
8446
- children: tr("fluidgrids.builder.schedule.cancelOther", "Cancel it and start")
8447
- }),
8448
- /* @__PURE__ */ jsx("option", {
8449
- value: "TERMINATE_OTHER",
8450
- children: tr("fluidgrids.builder.schedule.terminateOther", "Terminate it and start")
8451
- }),
8452
- /* @__PURE__ */ jsx("option", {
8453
- value: "ALLOW_ALL",
8454
- children: tr("fluidgrids.builder.schedule.allowAll", "Start anyway")
8455
- })
8456
- ]
8457
- }),
8458
- !policy && canUpdate && /* @__PURE__ */ jsx(Button$1, {
8459
- size: "sm",
8460
- variant: "link",
8461
- className: "h-auto p-0 text-xs",
8462
- onClick: () => writeField("overlapPolicy", "SKIP"),
8463
- "data-testid": "schedule-use-policy",
8464
- children: tr("fluidgrids.builder.schedule.usePolicy", "Use a policy (skip while a run is open)")
8465
- })
8466
- ]
8467
- }),
8468
- /* @__PURE__ */ jsxs("div", {
8469
- className: "grid gap-3 sm:grid-cols-2",
8470
- children: [/* @__PURE__ */ jsxs("div", {
9392
+ children: [
9393
+ /* @__PURE__ */ jsxs("section", {
9394
+ className: "space-y-3",
9395
+ children: [
9396
+ /* @__PURE__ */ jsx("h4", {
9397
+ className: "text-sm font-semibold text-foreground",
9398
+ children: tr("fluidgrids.builder.schedule.policy", "Policy")
9399
+ }),
9400
+ /* @__PURE__ */ jsxs("div", {
8471
9401
  className: "space-y-1.5",
8472
- children: [/* @__PURE__ */ jsx(Label, {
8473
- htmlFor: "schedule-catchup",
8474
- children: tr("fluidgrids.builder.schedule.catchup", "Catch-up window (seconds)")
8475
- }), /* @__PURE__ */ jsx(Input, {
8476
- id: "schedule-catchup",
8477
- inputMode: "numeric",
8478
- disabled: !canUpdate,
8479
- value: catchup === null ? "" : String(catchup),
8480
- onChange: (event) => {
8481
- const value = event.target.value.trim();
8482
- writeField("catchupWindow", value === "" ? void 0 : /^\d+$/.test(value) ? Math.min(Number(value), 604800) : state.catchupWindow);
8483
- },
8484
- "data-testid": "schedule-catchup"
9402
+ children: [
9403
+ /* @__PURE__ */ jsx(Label, {
9404
+ htmlFor: "schedule-overlap",
9405
+ children: tr("fluidgrids.builder.schedule.overlap", "When the previous run is still open")
9406
+ }),
9407
+ /* @__PURE__ */ jsxs(NativeSelect, {
9408
+ id: "schedule-overlap",
9409
+ value: policy ?? "",
9410
+ disabled: !canUpdate,
9411
+ onChange: (value) => writeField("overlapPolicy", overlapPolicyOf(value) ?? void 0),
9412
+ testId: "schedule-overlap",
9413
+ children: [
9414
+ /* @__PURE__ */ jsx("option", {
9415
+ value: "",
9416
+ children: tr("fluidgrids.builder.schedule.overlapLegacy", "Start every run (no policy)")
9417
+ }),
9418
+ /* @__PURE__ */ jsx("option", {
9419
+ value: "SKIP",
9420
+ children: tr("fluidgrids.builder.schedule.skip", "Skip the fire")
9421
+ }),
9422
+ /* @__PURE__ */ jsx("option", {
9423
+ value: "BUFFER_ONE",
9424
+ children: tr("fluidgrids.builder.schedule.bufferOne", "Run once after it ends")
9425
+ }),
9426
+ /* @__PURE__ */ jsx("option", {
9427
+ value: "BUFFER_ALL",
9428
+ children: tr("fluidgrids.builder.schedule.bufferAll", "Run every missed fire after it ends")
9429
+ }),
9430
+ /* @__PURE__ */ jsx("option", {
9431
+ value: "CANCEL_OTHER",
9432
+ children: tr("fluidgrids.builder.schedule.cancelOther", "Cancel it and start")
9433
+ }),
9434
+ /* @__PURE__ */ jsx("option", {
9435
+ value: "TERMINATE_OTHER",
9436
+ children: tr("fluidgrids.builder.schedule.terminateOther", "Terminate it and start")
9437
+ }),
9438
+ /* @__PURE__ */ jsx("option", {
9439
+ value: "ALLOW_ALL",
9440
+ children: tr("fluidgrids.builder.schedule.allowAll", "Start anyway")
9441
+ })
9442
+ ]
9443
+ }),
9444
+ !policy && canUpdate && /* @__PURE__ */ jsx(Button$1, {
9445
+ size: "sm",
9446
+ variant: "link",
9447
+ className: "h-auto p-0 text-xs",
9448
+ onClick: () => writeField("overlapPolicy", "SKIP"),
9449
+ "data-testid": "schedule-use-policy",
9450
+ children: tr("fluidgrids.builder.schedule.usePolicy", "Use a policy (skip while a run is open)")
9451
+ })
9452
+ ]
9453
+ }),
9454
+ /* @__PURE__ */ jsxs("div", {
9455
+ className: "grid gap-3 sm:grid-cols-2",
9456
+ children: [/* @__PURE__ */ jsxs("div", {
9457
+ className: "space-y-1.5",
9458
+ children: [/* @__PURE__ */ jsx(Label, {
9459
+ htmlFor: "schedule-catchup",
9460
+ children: tr("fluidgrids.builder.schedule.catchup", "Catch-up window (seconds)")
9461
+ }), /* @__PURE__ */ jsx(Input, {
9462
+ id: "schedule-catchup",
9463
+ inputMode: "numeric",
9464
+ disabled: !canUpdate,
9465
+ value: catchup === null ? "" : String(catchup),
9466
+ onChange: (event) => {
9467
+ const value = event.target.value.trim();
9468
+ writeField("catchupWindow", value === "" ? void 0 : /^\d+$/.test(value) ? Math.min(Number(value), 604800) : state.catchupWindow);
9469
+ },
9470
+ "data-testid": "schedule-catchup"
9471
+ })]
9472
+ }), /* @__PURE__ */ jsxs("div", {
9473
+ className: "space-y-1.5",
9474
+ children: [/* @__PURE__ */ jsx(Label, {
9475
+ htmlFor: "schedule-limited",
9476
+ children: tr("fluidgrids.builder.schedule.limited", "Stop after this many runs (optional)")
9477
+ }), /* @__PURE__ */ jsx(Input, {
9478
+ id: "schedule-limited",
9479
+ inputMode: "numeric",
9480
+ disabled: !canUpdate,
9481
+ value: limited === null ? "" : String(limited),
9482
+ onChange: (event) => {
9483
+ const value = event.target.value.trim();
9484
+ writeField("limitedActions", value === "" ? void 0 : /^\d+$/.test(value) ? Number(value) : state.limitedActions);
9485
+ },
9486
+ "data-testid": "schedule-limited"
9487
+ })]
8485
9488
  })]
8486
- }), /* @__PURE__ */ jsxs("div", {
8487
- className: "space-y-1.5",
8488
- children: [/* @__PURE__ */ jsx(Label, {
8489
- htmlFor: "schedule-limited",
8490
- children: tr("fluidgrids.builder.schedule.limited", "Stop after this many runs (optional)")
8491
- }), /* @__PURE__ */ jsx(Input, {
8492
- id: "schedule-limited",
8493
- inputMode: "numeric",
9489
+ }),
9490
+ /* @__PURE__ */ jsxs("label", {
9491
+ className: "flex items-center justify-between gap-3 text-sm text-foreground",
9492
+ children: [tr("fluidgrids.builder.schedule.pauseOnFailure", "Pause the schedule when a run it started fails"), /* @__PURE__ */ jsx(Switch, {
9493
+ checked: state.pauseOnFailure === true,
8494
9494
  disabled: !canUpdate,
8495
- value: limited === null ? "" : String(limited),
8496
- onChange: (event) => {
8497
- const value = event.target.value.trim();
8498
- writeField("limitedActions", value === "" ? void 0 : /^\d+$/.test(value) ? Number(value) : state.limitedActions);
8499
- },
8500
- "data-testid": "schedule-limited"
9495
+ onCheckedChange: (checked) => writeField("pauseOnFailure", checked ? true : void 0),
9496
+ "data-testid": "schedule-pause-on-failure"
8501
9497
  })]
8502
- })]
8503
- }),
8504
- /* @__PURE__ */ jsxs("label", {
8505
- className: "flex items-center justify-between gap-3 text-sm text-foreground",
8506
- children: [tr("fluidgrids.builder.schedule.pauseOnFailure", "Pause the schedule when a run it started fails"), /* @__PURE__ */ jsx(Switch, {
8507
- checked: state.pauseOnFailure === true,
8508
- disabled: !canUpdate,
8509
- onCheckedChange: (checked) => writeField("pauseOnFailure", checked ? true : void 0),
8510
- "data-testid": "schedule-pause-on-failure"
8511
- })]
8512
- }),
8513
- /* @__PURE__ */ jsx("p", {
8514
- className: "text-xs text-muted-foreground",
8515
- children: tr("fluidgrids.builder.schedule.saveHint", "Policy changes apply when the workflow is saved and activated.")
8516
- })
8517
- ]
8518
- }), !workflowKey ? /* @__PURE__ */ jsx("p", {
8519
- className: "text-sm text-muted-foreground",
8520
- children: tr("fluidgrids.builder.schedule.unsaved", "Save the workflow to see and control its schedule.")
8521
- }) : /* @__PURE__ */ jsxs("section", {
8522
- className: "space-y-3",
8523
- "data-testid": "schedule-live",
8524
- children: [
8525
- /* @__PURE__ */ jsxs("div", {
8526
- className: "flex flex-wrap items-center gap-2",
8527
- children: [
8528
- /* @__PURE__ */ jsx("h4", {
8529
- className: "text-sm font-semibold text-foreground",
8530
- children: tr("fluidgrids.builder.schedule.live", "Schedule")
8531
- }),
8532
- live && /* @__PURE__ */ jsx(StatusPill, {
8533
- tone: live.state.paused ? "warning" : "success",
8534
- testId: "schedule-state",
8535
- children: live.state.paused ? tr("fluidgrids.builder.schedule.paused", "Paused") : tr("fluidgrids.builder.schedule.active", "Active")
8536
- }),
8537
- /* @__PURE__ */ jsx(Button$1, {
8538
- size: "sm",
8539
- variant: "ghost",
8540
- className: "ml-auto",
8541
- onClick: () => void describe.refetch(),
8542
- children: /* @__PURE__ */ jsx(Icon, {
8543
- icon: "mdi:refresh",
8544
- width: 14
9498
+ }),
9499
+ /* @__PURE__ */ jsx("p", {
9500
+ className: "text-xs text-muted-foreground",
9501
+ children: tr("fluidgrids.builder.schedule.saveHint", "Policy changes apply when the workflow is saved and activated.")
9502
+ })
9503
+ ]
9504
+ }),
9505
+ /* @__PURE__ */ jsx(ScheduleSpecEditor, {
9506
+ state,
9507
+ writeField,
9508
+ canUpdate,
9509
+ searchAttributesOn
9510
+ }),
9511
+ !workflowKey ? /* @__PURE__ */ jsx("p", {
9512
+ className: "text-sm text-muted-foreground",
9513
+ children: tr("fluidgrids.builder.schedule.unsaved", "Save the workflow to see and control its schedule.")
9514
+ }) : /* @__PURE__ */ jsxs("section", {
9515
+ className: "space-y-3",
9516
+ "data-testid": "schedule-live",
9517
+ children: [
9518
+ /* @__PURE__ */ jsxs("div", {
9519
+ className: "flex flex-wrap items-center gap-2",
9520
+ children: [
9521
+ /* @__PURE__ */ jsx("h4", {
9522
+ className: "text-sm font-semibold text-foreground",
9523
+ children: tr("fluidgrids.builder.schedule.live", "Schedule")
9524
+ }),
9525
+ live && /* @__PURE__ */ jsx(StatusPill, {
9526
+ tone: live.state.paused ? "warning" : "success",
9527
+ testId: "schedule-state",
9528
+ children: live.state.paused ? tr("fluidgrids.builder.schedule.paused", "Paused") : tr("fluidgrids.builder.schedule.active", "Active")
9529
+ }),
9530
+ /* @__PURE__ */ jsx(Button$1, {
9531
+ size: "sm",
9532
+ variant: "ghost",
9533
+ className: "ml-auto",
9534
+ onClick: () => void describe.refetch(),
9535
+ children: /* @__PURE__ */ jsx(Icon, {
9536
+ icon: "mdi:refresh",
9537
+ width: 14
9538
+ })
8545
9539
  })
8546
- })
8547
- ]
8548
- }),
8549
- /* @__PURE__ */ jsx(FieldError, {
8550
- message: describe.error,
8551
- testId: "schedule-describe-error"
8552
- }),
8553
- live?.state.paused && live.state.note && /* @__PURE__ */ jsx("p", {
8554
- className: "text-sm text-foreground",
8555
- "data-testid": "schedule-note",
8556
- children: live.state.note
8557
- }),
8558
- live && /* @__PURE__ */ jsxs("div", {
8559
- className: "grid gap-3 sm:grid-cols-2",
8560
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
8561
- className: "mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
8562
- children: tr("fluidgrids.builder.schedule.next", "Next fire times")
8563
- }), /* @__PURE__ */ jsxs("ul", {
8564
- className: "space-y-0.5 font-mono text-xs text-foreground",
8565
- "data-testid": "schedule-next",
8566
- children: [live.nextFireTimes.slice(0, 5).map((time) => /* @__PURE__ */ jsx("li", { children: new Date(time).toLocaleString() }, time)), live.nextFireTimes.length === 0 && /* @__PURE__ */ jsx("li", { children: "—" })]
8567
- })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
8568
- className: "mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
8569
- children: tr("fluidgrids.builder.schedule.recent", "Recent actions")
8570
- }), /* @__PURE__ */ jsxs("ul", {
8571
- className: "space-y-0.5 text-xs text-foreground",
8572
- "data-testid": "schedule-recent",
8573
- children: [live.recentActions.slice(0, 5).map((tick) => /* @__PURE__ */ jsxs("li", { children: [
8574
- /* @__PURE__ */ jsx("span", {
8575
- className: "font-mono",
8576
- children: new Date(tick.dueAt).toLocaleString()
9540
+ ]
9541
+ }),
9542
+ /* @__PURE__ */ jsx(FieldError, {
9543
+ message: describe.error,
9544
+ testId: "schedule-describe-error"
9545
+ }),
9546
+ live?.state.paused && live.state.note && /* @__PURE__ */ jsx("p", {
9547
+ className: "text-sm text-foreground",
9548
+ "data-testid": "schedule-note",
9549
+ children: live.state.note
9550
+ }),
9551
+ live && /* @__PURE__ */ jsxs("div", {
9552
+ className: "grid gap-3 sm:grid-cols-2",
9553
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
9554
+ className: "mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
9555
+ children: tr("fluidgrids.builder.schedule.next", "Next fire times")
9556
+ }), /* @__PURE__ */ jsxs("ul", {
9557
+ className: "space-y-0.5 font-mono text-xs text-foreground",
9558
+ "data-testid": "schedule-next",
9559
+ children: [live.nextFireTimes.slice(0, 5).map((time) => /* @__PURE__ */ jsx("li", { children: new Date(time).toLocaleString() }, time)), live.nextFireTimes.length === 0 && /* @__PURE__ */ jsx("li", { children: "—" })]
9560
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
9561
+ className: "mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
9562
+ children: tr("fluidgrids.builder.schedule.recent", "Recent actions")
9563
+ }), /* @__PURE__ */ jsxs("ul", {
9564
+ className: "space-y-0.5 text-xs text-foreground",
9565
+ "data-testid": "schedule-recent",
9566
+ children: [live.recentActions.slice(0, 5).map((tick) => /* @__PURE__ */ jsxs("li", { children: [
9567
+ /* @__PURE__ */ jsx("span", {
9568
+ className: "font-mono",
9569
+ children: new Date(tick.dueAt).toLocaleString()
9570
+ }),
9571
+ " ",
9572
+ "· ",
9573
+ tick.kind,
9574
+ " · ",
9575
+ tick.outcome
9576
+ ] }, tick.id)), live.recentActions.length === 0 && /* @__PURE__ */ jsx("li", { children: "—" })]
9577
+ })] })]
9578
+ }),
9579
+ /* @__PURE__ */ jsxs("div", {
9580
+ className: "flex flex-wrap gap-2",
9581
+ children: [
9582
+ canExecute && /* @__PURE__ */ jsxs(Button$1, {
9583
+ size: "sm",
9584
+ variant: "outline",
9585
+ disabled: busy !== null,
9586
+ onClick: () => void act("trigger", async () => {
9587
+ const result = await triggerSchedule({
9588
+ workflowKey,
9589
+ triggerNodeId
9590
+ });
9591
+ toast.success(result.runId ? tr("fluidgrids.builder.schedule.triggered", "Run started") : tr("fluidgrids.builder.schedule.triggeredOutcome", "Tick recorded: {{outcome}}", { outcome: result.tick.outcome }));
9592
+ await describe.refetch();
9593
+ }),
9594
+ "data-testid": "schedule-trigger",
9595
+ children: [/* @__PURE__ */ jsx(Icon, {
9596
+ icon: "mdi:play",
9597
+ className: "mr-1",
9598
+ width: 14
9599
+ }), tr("fluidgrids.builder.schedule.trigger", "Trigger now")]
8577
9600
  }),
8578
- " ",
8579
- "· ",
8580
- tick.kind,
8581
- " · ",
8582
- tick.outcome
8583
- ] }, tick.id)), live.recentActions.length === 0 && /* @__PURE__ */ jsx("li", { children: "—" })]
8584
- })] })]
8585
- }),
8586
- /* @__PURE__ */ jsxs("div", {
8587
- className: "flex flex-wrap gap-2",
8588
- children: [
8589
- canExecute && /* @__PURE__ */ jsxs(Button$1, {
8590
- size: "sm",
8591
- variant: "outline",
8592
- disabled: busy !== null,
8593
- onClick: () => void act("trigger", async () => {
8594
- const result = await triggerSchedule({
9601
+ canUpdate && live && !live.state.paused && /* @__PURE__ */ jsxs(Button$1, {
9602
+ size: "sm",
9603
+ variant: "outline",
9604
+ disabled: busy !== null,
9605
+ onClick: () => void act("pause", async () => {
9606
+ describe.set(await pauseSchedule({
9607
+ workflowKey,
9608
+ triggerNodeId,
9609
+ note
9610
+ }));
9611
+ setNote("");
9612
+ }),
9613
+ "data-testid": "schedule-pause",
9614
+ children: [/* @__PURE__ */ jsx(Icon, {
9615
+ icon: "mdi:pause",
9616
+ className: "mr-1",
9617
+ width: 14
9618
+ }), tr("fluidgrids.builder.schedule.pause", "Pause")]
9619
+ }),
9620
+ canUpdate && live?.state.paused && /* @__PURE__ */ jsxs(Button$1, {
9621
+ size: "sm",
9622
+ variant: "outline",
9623
+ disabled: busy !== null,
9624
+ onClick: () => void act("unpause", async () => describe.set(await unpauseSchedule({
8595
9625
  workflowKey,
8596
9626
  triggerNodeId
8597
- });
8598
- toast.success(result.runId ? tr("fluidgrids.builder.schedule.triggered", "Run started") : tr("fluidgrids.builder.schedule.triggeredOutcome", "Tick recorded: {{outcome}}", { outcome: result.tick.outcome }));
8599
- await describe.refetch();
9627
+ }))),
9628
+ "data-testid": "schedule-unpause",
9629
+ children: [/* @__PURE__ */ jsx(Icon, {
9630
+ icon: "mdi:play-pause",
9631
+ className: "mr-1",
9632
+ width: 14
9633
+ }), tr("fluidgrids.builder.schedule.unpause", "Unpause")]
9634
+ })
9635
+ ]
9636
+ }),
9637
+ canUpdate && live && !live.state.paused && /* @__PURE__ */ jsx(Input, {
9638
+ value: note,
9639
+ maxLength: 1e3,
9640
+ placeholder: tr("fluidgrids.builder.schedule.notePlaceholder", "Note for the pause (optional)"),
9641
+ onChange: (event) => setNote(event.target.value),
9642
+ "data-testid": "schedule-pause-note"
9643
+ }),
9644
+ canExecute && /* @__PURE__ */ jsxs("div", {
9645
+ className: "space-y-2 rounded-md border border-border p-3",
9646
+ children: [
9647
+ /* @__PURE__ */ jsx("p", {
9648
+ className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
9649
+ children: tr("fluidgrids.builder.schedule.backfill", "Backfill")
8600
9650
  }),
8601
- "data-testid": "schedule-trigger",
8602
- children: [/* @__PURE__ */ jsx(Icon, {
8603
- icon: "mdi:play",
8604
- className: "mr-1",
8605
- width: 14
8606
- }), tr("fluidgrids.builder.schedule.trigger", "Trigger now")]
8607
- }),
8608
- canUpdate && live && !live.state.paused && /* @__PURE__ */ jsxs(Button$1, {
8609
- size: "sm",
8610
- variant: "outline",
8611
- disabled: busy !== null,
8612
- onClick: () => void act("pause", async () => {
8613
- describe.set(await pauseSchedule({
8614
- workflowKey,
8615
- triggerNodeId,
8616
- note
8617
- }));
8618
- setNote("");
9651
+ /* @__PURE__ */ jsxs("div", {
9652
+ className: "grid gap-2 sm:grid-cols-2",
9653
+ children: [/* @__PURE__ */ jsx(Input, {
9654
+ type: "datetime-local",
9655
+ value: from,
9656
+ onChange: (event) => setFrom(event.target.value),
9657
+ "aria-label": tr("fluidgrids.builder.schedule.from", "From"),
9658
+ "data-testid": "schedule-backfill-from"
9659
+ }), /* @__PURE__ */ jsx(Input, {
9660
+ type: "datetime-local",
9661
+ value: to,
9662
+ onChange: (event) => setTo(event.target.value),
9663
+ "aria-label": tr("fluidgrids.builder.schedule.to", "To"),
9664
+ "data-testid": "schedule-backfill-to"
9665
+ })]
8619
9666
  }),
8620
- "data-testid": "schedule-pause",
8621
- children: [/* @__PURE__ */ jsx(Icon, {
8622
- icon: "mdi:pause",
8623
- className: "mr-1",
8624
- width: 14
8625
- }), tr("fluidgrids.builder.schedule.pause", "Pause")]
8626
- }),
8627
- canUpdate && live?.state.paused && /* @__PURE__ */ jsxs(Button$1, {
8628
- size: "sm",
8629
- variant: "outline",
8630
- disabled: busy !== null,
8631
- onClick: () => void act("unpause", async () => describe.set(await unpauseSchedule({
8632
- workflowKey,
8633
- triggerNodeId
8634
- }))),
8635
- "data-testid": "schedule-unpause",
8636
- children: [/* @__PURE__ */ jsx(Icon, {
8637
- icon: "mdi:play-pause",
8638
- className: "mr-1",
8639
- width: 14
8640
- }), tr("fluidgrids.builder.schedule.unpause", "Unpause")]
8641
- })
8642
- ]
8643
- }),
8644
- canUpdate && live && !live.state.paused && /* @__PURE__ */ jsx(Input, {
8645
- value: note,
8646
- maxLength: 1e3,
8647
- placeholder: tr("fluidgrids.builder.schedule.notePlaceholder", "Note for the pause (optional)"),
8648
- onChange: (event) => setNote(event.target.value),
8649
- "data-testid": "schedule-pause-note"
8650
- }),
8651
- canExecute && /* @__PURE__ */ jsxs("div", {
8652
- className: "space-y-2 rounded-md border border-border p-3",
8653
- children: [
8654
- /* @__PURE__ */ jsx("p", {
8655
- className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground",
8656
- children: tr("fluidgrids.builder.schedule.backfill", "Backfill")
8657
- }),
8658
- /* @__PURE__ */ jsxs("div", {
8659
- className: "grid gap-2 sm:grid-cols-2",
8660
- children: [/* @__PURE__ */ jsx(Input, {
8661
- type: "datetime-local",
8662
- value: from,
8663
- onChange: (event) => setFrom(event.target.value),
8664
- "aria-label": tr("fluidgrids.builder.schedule.from", "From"),
8665
- "data-testid": "schedule-backfill-from"
8666
- }), /* @__PURE__ */ jsx(Input, {
8667
- type: "datetime-local",
8668
- value: to,
8669
- onChange: (event) => setTo(event.target.value),
8670
- "aria-label": tr("fluidgrids.builder.schedule.to", "To"),
8671
- "data-testid": "schedule-backfill-to"
8672
- })]
8673
- }),
8674
- /* @__PURE__ */ jsx(Button$1, {
8675
- size: "sm",
8676
- variant: "outline",
8677
- disabled: !backfillValid || busy !== null,
8678
- onClick: () => void act("backfill", async () => {
8679
- if (!backfillFrom || !backfillTo) return;
8680
- const result = await backfillSchedule({
8681
- workflowKey,
8682
- triggerNodeId,
8683
- from: backfillFrom.toISOString(),
8684
- to: backfillTo.toISOString()
8685
- });
8686
- toast.success(tr("fluidgrids.builder.schedule.backfilled", "{{queued}} of {{fireTimes}} fire times queued", {
8687
- queued: result.queued,
8688
- fireTimes: result.fireTimes
8689
- }));
8690
- await describe.refetch();
9667
+ /* @__PURE__ */ jsx(Button$1, {
9668
+ size: "sm",
9669
+ variant: "outline",
9670
+ disabled: !backfillValid || busy !== null,
9671
+ onClick: () => void act("backfill", async () => {
9672
+ if (!backfillFrom || !backfillTo) return;
9673
+ const result = await backfillSchedule({
9674
+ workflowKey,
9675
+ triggerNodeId,
9676
+ from: backfillFrom.toISOString(),
9677
+ to: backfillTo.toISOString()
9678
+ });
9679
+ toast.success(tr("fluidgrids.builder.schedule.backfilled", "{{queued}} of {{fireTimes}} fire times queued", {
9680
+ queued: result.queued,
9681
+ fireTimes: result.fireTimes
9682
+ }));
9683
+ await describe.refetch();
9684
+ }),
9685
+ "data-testid": "schedule-backfill",
9686
+ children: tr("fluidgrids.builder.schedule.runBackfill", "Run the fire times in this range")
8691
9687
  }),
8692
- "data-testid": "schedule-backfill",
8693
- children: tr("fluidgrids.builder.schedule.runBackfill", "Run the fire times in this range")
8694
- }),
8695
- /* @__PURE__ */ jsx("p", {
8696
- className: "text-xs text-muted-foreground",
8697
- children: tr("fluidgrids.builder.schedule.backfillHint", "At most {{max}} past fire times; each started run counts toward the monthly runs.", { max: 1e3 })
8698
- })
8699
- ]
8700
- }),
8701
- /* @__PURE__ */ jsx(FieldError, {
8702
- message: error,
8703
- testId: "schedule-error"
8704
- })
8705
- ]
8706
- })]
9688
+ /* @__PURE__ */ jsx("p", {
9689
+ className: "text-xs text-muted-foreground",
9690
+ children: tr("fluidgrids.builder.schedule.backfillHint", "At most {{max}} past fire times; each started run counts toward the monthly runs.", { max: 1e3 })
9691
+ })
9692
+ ]
9693
+ }),
9694
+ /* @__PURE__ */ jsx(FieldError, {
9695
+ message: error,
9696
+ testId: "schedule-error"
9697
+ })
9698
+ ]
9699
+ })
9700
+ ]
8707
9701
  });
8708
9702
  }
8709
9703