@timeax/digital-service-engine 0.3.5 → 0.3.6

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.
@@ -384,6 +384,7 @@ function templateTime(template) {
384
384
  return (_a = parseTimestamp(template.updatedAt)) != null ? _a : parseTimestamp(template.createdAt);
385
385
  }
386
386
  function shouldReplaceTemplates(params) {
387
+ var _a;
387
388
  if (!params.requestedSince) return true;
388
389
  if (!params.lastUpdatedAt) return false;
389
390
  const requested = parseTimestamp(params.requestedSince);
@@ -391,7 +392,9 @@ function shouldReplaceTemplates(params) {
391
392
  if (requested === void 0 || last === void 0) {
392
393
  return false;
393
394
  }
394
- return requested < last;
395
+ const currentTimes = ((_a = params.current) != null ? _a : []).map((template) => templateTime(template)).filter((time) => time !== void 0);
396
+ if (!currentTimes.length) return requested < last;
397
+ return requested < Math.min(...currentTimes);
395
398
  }
396
399
  function pickNewestTemplate(current, incoming) {
397
400
  const currentTime = templateTime(current);
@@ -399,7 +402,8 @@ function pickNewestTemplate(current, incoming) {
399
402
  if (currentTime !== void 0 && incomingTime !== void 0) {
400
403
  return incomingTime >= currentTime ? incoming : current;
401
404
  }
402
- if (currentTime === void 0 && incomingTime !== void 0) return incoming;
405
+ if (currentTime === void 0 && incomingTime !== void 0)
406
+ return incoming;
403
407
  if (currentTime !== void 0 && incomingTime === void 0) return current;
404
408
  return incoming;
405
409
  }
@@ -472,7 +476,8 @@ function useTemplatesSlice(params) {
472
476
  setTemplates((current) => {
473
477
  const replace = shouldReplaceTemplates({
474
478
  requestedSince,
475
- lastUpdatedAt: current.updatedAt
479
+ lastUpdatedAt: current.updatedAt,
480
+ current: current.data
476
481
  });
477
482
  return {
478
483
  data: replace ? res.value : mergeTemplates(current.data, res.value, {
@@ -571,7 +576,9 @@ function useTemplatesSlice(params) {
571
576
  var _a, _b;
572
577
  return {
573
578
  ...current,
574
- data: (_b = (_a = current.data) == null ? void 0 : _a.filter((template) => template.id !== id)) != null ? _b : current.data,
579
+ data: (_b = (_a = current.data) == null ? void 0 : _a.filter(
580
+ (template) => template.id !== id
581
+ )) != null ? _b : current.data,
575
582
  updatedAt: deleteRefreshSince
576
583
  };
577
584
  });
@@ -3634,6 +3641,9 @@ function normalise(input, opts = {}) {
3634
3641
  const excludes_for_buttons = toStringArrayMap(
3635
3642
  obj.excludes_for_buttons
3636
3643
  );
3644
+ const option_effects_for_buttons = toOptionEffectMap(
3645
+ obj.option_effects_for_buttons
3646
+ );
3637
3647
  const orderKinds = toStringMap(obj.orderKinds);
3638
3648
  const notices = toNoticeArray(obj.notices);
3639
3649
  let filters = rawFilters.map((t) => coerceTag(t, constraints));
@@ -3649,6 +3659,9 @@ function normalise(input, opts = {}) {
3649
3659
  ...isNonEmpty(orderKinds) && { orderKinds },
3650
3660
  ...isNonEmpty(includes_for_buttons) && { includes_for_buttons },
3651
3661
  ...isNonEmpty(excludes_for_buttons) && { excludes_for_buttons },
3662
+ ...isNonEmpty(option_effects_for_buttons) && {
3663
+ option_effects_for_buttons
3664
+ },
3652
3665
  ...fallbacks && (isNonEmpty(fallbacks.nodes) || isNonEmpty(fallbacks.global)) && {
3653
3666
  fallbacks
3654
3667
  },
@@ -3803,6 +3816,7 @@ function coerceOption(src, inheritRole) {
3803
3816
  const value = typeof src.value === "string" || typeof src.value === "number" ? src.value : void 0;
3804
3817
  const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : inheritRole;
3805
3818
  const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
3819
+ const children = Array.isArray(src.children) ? src.children.map((child) => coerceOption(child, pricing_role)) : void 0;
3806
3820
  const option = {
3807
3821
  id: "",
3808
3822
  label: "",
@@ -3811,7 +3825,8 @@ function coerceOption(src, inheritRole) {
3811
3825
  ...value !== void 0 && { value },
3812
3826
  ...service_id !== void 0 && { service_id },
3813
3827
  pricing_role,
3814
- ...meta && { meta }
3828
+ ...meta && { meta },
3829
+ ...children && children.length && { children }
3815
3830
  };
3816
3831
  return option;
3817
3832
  }
@@ -3876,6 +3891,35 @@ function toStringArrayMap(src) {
3876
3891
  }
3877
3892
  return Object.keys(out).length ? out : void 0;
3878
3893
  }
3894
+ function toOptionEffectMap(src) {
3895
+ var _a, _b;
3896
+ if (!src || typeof src !== "object") return void 0;
3897
+ const out = {};
3898
+ for (const [triggerId, rawTargets] of Object.entries(src)) {
3899
+ if (!triggerId || !rawTargets || typeof rawTargets !== "object") {
3900
+ continue;
3901
+ }
3902
+ const targets = {};
3903
+ for (const [fieldId, rawEffect] of Object.entries(rawTargets)) {
3904
+ if (!fieldId || !rawEffect || typeof rawEffect !== "object") {
3905
+ continue;
3906
+ }
3907
+ const effect = rawEffect;
3908
+ const include2 = toStringArray(effect.include);
3909
+ const exclude2 = toStringArray(effect.exclude);
3910
+ const next = {
3911
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
3912
+ ...include2.length ? { include: dedupe(include2) } : {},
3913
+ ...exclude2.length ? { exclude: dedupe(exclude2) } : {}
3914
+ };
3915
+ if (next.forceVisible === true || ((_a = next.include) == null ? void 0 : _a.length) || ((_b = next.exclude) == null ? void 0 : _b.length)) {
3916
+ targets[fieldId] = next;
3917
+ }
3918
+ }
3919
+ if (Object.keys(targets).length) out[triggerId] = targets;
3920
+ }
3921
+ return Object.keys(out).length ? out : void 0;
3922
+ }
3879
3923
  function toStringArray(v) {
3880
3924
  if (!Array.isArray(v)) return [];
3881
3925
  return v.map((x) => String(x)).filter((s) => !!s && s.trim().length > 0);
@@ -3950,6 +3994,57 @@ function normalizeFieldValidation(input) {
3950
3994
  return one ? [one] : void 0;
3951
3995
  }
3952
3996
 
3997
+ // src/core/options.ts
3998
+ function walkFieldOptions(field) {
3999
+ const out = [];
4000
+ const visit = (options, depth, parentId) => {
4001
+ for (const option of options != null ? options : []) {
4002
+ out.push({
4003
+ field,
4004
+ fieldId: field.id,
4005
+ option,
4006
+ optionId: option.id,
4007
+ depth,
4008
+ parentId
4009
+ });
4010
+ visit(option.children, depth + 1, option.id);
4011
+ }
4012
+ };
4013
+ visit(field.options, 0);
4014
+ return out;
4015
+ }
4016
+ function fieldOptionIds(field) {
4017
+ return walkFieldOptions(field).map((visit) => visit.optionId);
4018
+ }
4019
+ function fieldOptionIdSet(field) {
4020
+ return new Set(fieldOptionIds(field));
4021
+ }
4022
+ function findFieldOption(field, optionId) {
4023
+ var _a;
4024
+ if (!field) return void 0;
4025
+ return (_a = walkFieldOptions(field).find((visit) => visit.optionId === optionId)) == null ? void 0 : _a.option;
4026
+ }
4027
+ function findOptionOwnerField(fields, optionId) {
4028
+ for (const field of fields) {
4029
+ if (findFieldOption(field, optionId)) return field;
4030
+ }
4031
+ return void 0;
4032
+ }
4033
+ function optionOwnerMap(fields) {
4034
+ const out = /* @__PURE__ */ new Map();
4035
+ for (const field of fields) {
4036
+ for (const visit of walkFieldOptions(field)) {
4037
+ if (!out.has(visit.optionId)) {
4038
+ out.set(visit.optionId, {
4039
+ fieldId: field.id,
4040
+ option: visit.option
4041
+ });
4042
+ }
4043
+ }
4044
+ }
4045
+ return out;
4046
+ }
4047
+
3953
4048
  // src/core/validate/shared.ts
3954
4049
  function isFiniteNumber(v) {
3955
4050
  return typeof v === "number" && Number.isFinite(v);
@@ -3958,8 +4053,9 @@ function isServiceIdRef(v) {
3958
4053
  return typeof v === "string" && v.trim().length > 0 || typeof v === "number" && Number.isFinite(v);
3959
4054
  }
3960
4055
  function hasAnyServiceOption(f) {
3961
- var _a;
3962
- return ((_a = f.options) != null ? _a : []).some((o) => isServiceIdRef(o.service_id));
4056
+ return walkFieldOptions(f).some(
4057
+ (visit) => isServiceIdRef(visit.option.service_id)
4058
+ );
3963
4059
  }
3964
4060
  function getByPath(obj, path) {
3965
4061
  if (!path) return void 0;
@@ -4048,14 +4144,14 @@ function withAffected(details, ids) {
4048
4144
 
4049
4145
  // src/core/node-map.ts
4050
4146
  function buildNodeMap(props) {
4051
- var _a, _b, _c;
4147
+ var _a, _b;
4052
4148
  const map = /* @__PURE__ */ new Map();
4053
4149
  for (const t of (_a = props.filters) != null ? _a : []) {
4054
4150
  if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
4055
4151
  }
4056
4152
  for (const f of (_b = props.fields) != null ? _b : []) {
4057
4153
  if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
4058
- for (const o of (_c = f.options) != null ? _c : []) {
4154
+ for (const { option: o } of walkFieldOptions(f)) {
4059
4155
  if (!map.has(o.id))
4060
4156
  map.set(o.id, {
4061
4157
  kind: "option",
@@ -4068,12 +4164,6 @@ function buildNodeMap(props) {
4068
4164
  return map;
4069
4165
  }
4070
4166
  function resolveTrigger(trigger, nodeMap) {
4071
- const idx = trigger.indexOf("::");
4072
- if (idx !== -1) {
4073
- const fieldId = trigger.slice(0, idx);
4074
- const optionId = trigger.slice(idx + 2);
4075
- return { kind: "composite", triggerKey: trigger, fieldId, optionId };
4076
- }
4077
4167
  const direct = nodeMap.get(trigger);
4078
4168
  if (!direct) return void 0;
4079
4169
  if (direct.kind === "option") {
@@ -4125,11 +4215,6 @@ function visibleFieldIdsUnder(props, tagId, opts = {}) {
4125
4215
  const ownerDepthForTriggerKey = (triggerKey) => {
4126
4216
  const t = resolveTrigger(triggerKey, nodeMap);
4127
4217
  if (!t) return void 0;
4128
- if (t.kind === "composite") {
4129
- const f = fieldById.get(t.fieldId);
4130
- if (!f) return void 0;
4131
- return ownerDepthForField(f);
4132
- }
4133
4218
  if (t.kind === "field") {
4134
4219
  const f = fieldById.get(t.id);
4135
4220
  if (!f || f.button !== true) return void 0;
@@ -4212,6 +4297,84 @@ function visibleFieldsUnder(props, tagId, opts = {}) {
4212
4297
  const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
4213
4298
  return ids.map((id) => fieldById.get(id)).filter(Boolean);
4214
4299
  }
4300
+ function resolveVisibility(props, tagId, selectedKeys) {
4301
+ var _a, _b, _c, _d;
4302
+ const selected = new Set(selectedKeys != null ? selectedKeys : []);
4303
+ const baseFieldIds = visibleFieldIdsUnder(props, tagId, { selectedKeys: selected });
4304
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((field) => [field.id, field]));
4305
+ const visible = new Set(baseFieldIds);
4306
+ const forced = /* @__PURE__ */ new Set();
4307
+ const optionsByFieldId = {};
4308
+ const optionIdsByFieldId = /* @__PURE__ */ new Map();
4309
+ const getOptionIds = (field) => {
4310
+ let ids = optionIdsByFieldId.get(field.id);
4311
+ if (!ids) {
4312
+ ids = fieldOptionIds(field);
4313
+ optionIdsByFieldId.set(field.id, ids);
4314
+ }
4315
+ return ids;
4316
+ };
4317
+ const ensureOptions = (field) => {
4318
+ const ids = getOptionIds(field);
4319
+ if (!ids.length) return void 0;
4320
+ if (!optionsByFieldId[field.id]) optionsByFieldId[field.id] = [...ids];
4321
+ return optionsByFieldId[field.id];
4322
+ };
4323
+ for (const fieldId of baseFieldIds) {
4324
+ const field = fieldById.get(fieldId);
4325
+ if (field) ensureOptions(field);
4326
+ }
4327
+ const effects = (_b = props.option_effects_for_buttons) != null ? _b : {};
4328
+ for (const triggerId of selected) {
4329
+ const targetRules = effects[triggerId];
4330
+ if (!targetRules) continue;
4331
+ for (const [targetFieldId, rule] of Object.entries(targetRules)) {
4332
+ const field = fieldById.get(targetFieldId);
4333
+ if (!field) continue;
4334
+ const isVisible = visible.has(targetFieldId);
4335
+ if (!isVisible && rule.forceVisible !== true) continue;
4336
+ if (!isVisible && rule.forceVisible === true) {
4337
+ visible.add(targetFieldId);
4338
+ forced.add(targetFieldId);
4339
+ }
4340
+ const orderedOptionIds = getOptionIds(field);
4341
+ if (!orderedOptionIds.length) continue;
4342
+ const known = new Set(orderedOptionIds);
4343
+ let allowed = (_c = optionsByFieldId[targetFieldId]) != null ? _c : [...orderedOptionIds];
4344
+ if (Array.isArray(rule.include) && rule.include.length) {
4345
+ const include2 = new Set(
4346
+ rule.include.filter((optionId) => known.has(optionId))
4347
+ );
4348
+ allowed = orderedOptionIds.filter(
4349
+ (optionId) => include2.has(optionId) && allowed.includes(optionId)
4350
+ );
4351
+ }
4352
+ if (Array.isArray(rule.exclude) && rule.exclude.length) {
4353
+ const exclude2 = new Set(
4354
+ rule.exclude.filter((optionId) => known.has(optionId))
4355
+ );
4356
+ allowed = allowed.filter((optionId) => !exclude2.has(optionId));
4357
+ }
4358
+ optionsByFieldId[targetFieldId] = allowed;
4359
+ }
4360
+ }
4361
+ const visibleFieldIds = baseFieldIds.filter((fieldId) => visible.has(fieldId));
4362
+ const seen = new Set(visibleFieldIds);
4363
+ for (const field of (_d = props.fields) != null ? _d : []) {
4364
+ if (!visible.has(field.id) || seen.has(field.id)) continue;
4365
+ seen.add(field.id);
4366
+ visibleFieldIds.push(field.id);
4367
+ ensureOptions(field);
4368
+ }
4369
+ for (const fieldId of Object.keys(optionsByFieldId)) {
4370
+ if (!visible.has(fieldId)) delete optionsByFieldId[fieldId];
4371
+ }
4372
+ return {
4373
+ fieldIds: visibleFieldIds,
4374
+ optionsByFieldId,
4375
+ forcedFieldIds: visibleFieldIds.filter((fieldId) => forced.has(fieldId))
4376
+ };
4377
+ }
4215
4378
 
4216
4379
  // src/core/validate/steps/visibility.ts
4217
4380
  function createFieldsVisibleUnder(v) {
@@ -4229,7 +4392,6 @@ function resolveRootTags(tags) {
4229
4392
  return roots.length ? roots : tags.slice(0, 1);
4230
4393
  }
4231
4394
  function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
4232
- var _a;
4233
4395
  const visible = visibleFieldsUnder(v.props, tagId, {
4234
4396
  selectedKeys
4235
4397
  });
@@ -4239,7 +4401,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
4239
4401
  const t = f.id;
4240
4402
  if (effectfulKeys.has(t)) triggers.push(t);
4241
4403
  }
4242
- for (const o of (_a = f.options) != null ? _a : []) {
4404
+ for (const { option: o } of walkFieldOptions(f)) {
4243
4405
  const t = o.id;
4244
4406
  if (effectfulKeys.has(t)) triggers.push(t);
4245
4407
  }
@@ -4248,7 +4410,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
4248
4410
  return triggers;
4249
4411
  }
4250
4412
  function runVisibilityRulesOnce(v) {
4251
- var _a, _b, _c, _d, _e;
4413
+ var _a, _b, _c, _d;
4252
4414
  for (const t of v.tags) {
4253
4415
  const visible = v.fieldsVisibleUnder(t.id);
4254
4416
  const seen = /* @__PURE__ */ new Map();
@@ -4298,9 +4460,9 @@ function runVisibilityRulesOnce(v) {
4298
4460
  let hasUtility = false;
4299
4461
  const utilityOptionIds = [];
4300
4462
  for (const f of visible) {
4301
- for (const o of (_c = f.options) != null ? _c : []) {
4463
+ for (const { option: o } of walkFieldOptions(f)) {
4302
4464
  if (!isServiceIdRef(o.service_id)) continue;
4303
- const role = (_e = (_d = o.pricing_role) != null ? _d : f.pricing_role) != null ? _e : "base";
4465
+ const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
4304
4466
  if (role === "base") hasBase = true;
4305
4467
  else if (role === "utility") {
4306
4468
  hasUtility = true;
@@ -4341,7 +4503,7 @@ function dedupeErrorsInPlace(v, startIndex) {
4341
4503
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
4342
4504
  }
4343
4505
  function validateVisibility(v, options = {}) {
4344
- var _a, _b, _c, _d, _e;
4506
+ var _a, _b, _c, _d, _e, _f;
4345
4507
  v.simulatedVisibilityContexts = [];
4346
4508
  const simulate = options.simulate === true;
4347
4509
  if (!simulate) {
@@ -4366,10 +4528,13 @@ function validateVisibility(v, options = {}) {
4366
4528
  for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
4367
4529
  effectfulKeys.add(key);
4368
4530
  }
4531
+ for (const key of Object.keys((_e = v.props.option_effects_for_buttons) != null ? _e : {})) {
4532
+ effectfulKeys.add(key);
4533
+ }
4369
4534
  }
4370
4535
  const roots = resolveRootTags(v.tags);
4371
4536
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
4372
- const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
4537
+ const originalSelected = new Set((_f = v.selectedKeys) != null ? _f : []);
4373
4538
  const errorsStart = v.errors.length;
4374
4539
  const visited = /* @__PURE__ */ new Set();
4375
4540
  const seenContexts = /* @__PURE__ */ new Set();
@@ -4510,7 +4675,7 @@ function validateStructure(v) {
4510
4675
 
4511
4676
  // src/core/validate/steps/identity.ts
4512
4677
  function validateIdentity(v) {
4513
- var _a, _b;
4678
+ var _a;
4514
4679
  const tags = v.tags;
4515
4680
  const fields = v.fields;
4516
4681
  {
@@ -4610,7 +4775,7 @@ function validateIdentity(v) {
4610
4775
  }
4611
4776
  }
4612
4777
  for (const f of fields) {
4613
- for (const o of (_b = f.options) != null ? _b : []) {
4778
+ for (const { option: o } of walkFieldOptions(f)) {
4614
4779
  if (!o.label || !o.label.trim()) {
4615
4780
  v.errors.push({
4616
4781
  code: "label_missing",
@@ -4625,25 +4790,11 @@ function validateIdentity(v) {
4625
4790
  }
4626
4791
 
4627
4792
  // src/core/validate/steps/option-maps.ts
4628
- function parseFieldOptionKey(key) {
4629
- const idx = key.indexOf("::");
4630
- if (idx === -1) return null;
4631
- const fieldId = key.slice(0, idx).trim();
4632
- const optionId = key.slice(idx + 2).trim();
4633
- if (!fieldId || !optionId) return null;
4634
- return { fieldId, optionId };
4635
- }
4636
- function hasOption(v, fid, oid) {
4637
- var _a;
4638
- const f = v.fieldById.get(fid);
4639
- if (!f) return false;
4640
- return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
4641
- }
4642
4793
  function validateOptionMaps(v) {
4643
- var _a, _b;
4794
+ var _a, _b, _c;
4644
4795
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
4645
4796
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
4646
- const badKeyMessage = (key) => `Invalid trigger-map key "${key}". Expected a known node id (option or button-field), or "fieldId::optionId" pointing to an existing option.`;
4797
+ const badKeyMessage = (key) => `Invalid trigger-map key "${key}". Expected a known option id or button-field id.`;
4647
4798
  const validateTriggerKey = (key) => {
4648
4799
  const ref = v.nodeMap.get(key);
4649
4800
  if (ref) {
@@ -4662,19 +4813,7 @@ function validateOptionMaps(v) {
4662
4813
  }
4663
4814
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
4664
4815
  }
4665
- const p = parseFieldOptionKey(key);
4666
- if (!p) return { ok: false };
4667
- if (!hasOption(v, p.fieldId, p.optionId))
4668
- return {
4669
- ok: false,
4670
- nodeId: p.fieldId,
4671
- affected: [p.fieldId, p.optionId]
4672
- };
4673
- return {
4674
- ok: true,
4675
- nodeId: p.fieldId,
4676
- affected: [p.fieldId, p.optionId]
4677
- };
4816
+ return { ok: false };
4678
4817
  };
4679
4818
  for (const k of Object.keys(incMap)) {
4680
4819
  const r = validateTriggerKey(k);
@@ -4700,6 +4839,57 @@ function validateOptionMaps(v) {
4700
4839
  });
4701
4840
  }
4702
4841
  }
4842
+ const effectMap = (_c = v.props.option_effects_for_buttons) != null ? _c : {};
4843
+ for (const [triggerKey, targets] of Object.entries(effectMap)) {
4844
+ const trigger = validateTriggerKey(triggerKey);
4845
+ if (!trigger.ok) {
4846
+ v.errors.push({
4847
+ code: "bad_option_effect_key",
4848
+ severity: "error",
4849
+ message: badKeyMessage(triggerKey),
4850
+ nodeId: trigger.nodeId,
4851
+ details: withAffected({ key: triggerKey }, trigger.affected)
4852
+ });
4853
+ }
4854
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
4855
+ const field = v.fieldById.get(targetFieldId);
4856
+ if (!field) {
4857
+ v.errors.push({
4858
+ code: "bad_option_effect_target",
4859
+ severity: "error",
4860
+ message: `Option effect trigger "${triggerKey}" targets unknown field "${targetFieldId}".`,
4861
+ details: withAffected(
4862
+ { key: triggerKey, targetFieldId },
4863
+ trigger.affected
4864
+ )
4865
+ });
4866
+ continue;
4867
+ }
4868
+ const validOptionIds = fieldOptionIdSet(field);
4869
+ const checkTargetOptions = (kind, optionIds) => {
4870
+ for (const optionId of optionIds != null ? optionIds : []) {
4871
+ if (validOptionIds.has(optionId)) continue;
4872
+ v.errors.push({
4873
+ code: "bad_option_effect_option",
4874
+ severity: "error",
4875
+ message: `Option effect trigger "${triggerKey}" references unknown ${kind} option "${optionId}" for field "${targetFieldId}".`,
4876
+ nodeId: targetFieldId,
4877
+ details: withAffected(
4878
+ {
4879
+ key: triggerKey,
4880
+ targetFieldId,
4881
+ optionId,
4882
+ kind
4883
+ },
4884
+ [targetFieldId, optionId]
4885
+ )
4886
+ });
4887
+ }
4888
+ };
4889
+ checkTargetOptions("include", effect == null ? void 0 : effect.include);
4890
+ checkTargetOptions("exclude", effect == null ? void 0 : effect.exclude);
4891
+ }
4892
+ }
4703
4893
  for (const k of Object.keys(incMap)) {
4704
4894
  if (!(k in excMap)) continue;
4705
4895
  const r = validateTriggerKey(k);
@@ -4713,27 +4903,231 @@ function validateOptionMaps(v) {
4713
4903
  }
4714
4904
  }
4715
4905
 
4716
- // src/utils/order-kind.ts
4717
- function normalizeSelectedTriggerKey(key, nodeMap) {
4718
- if (!key) return void 0;
4719
- const compositeIdx = key.indexOf("::");
4720
- if (compositeIdx !== -1) {
4721
- const fieldId = key.slice(0, compositeIdx).trim();
4722
- const optionId = key.slice(compositeIdx + 2).trim();
4723
- if (optionId) {
4724
- const optionRef = nodeMap.get(optionId);
4725
- if ((optionRef == null ? void 0 : optionRef.kind) === "option") {
4726
- return { nodeId: optionRef.id, nodeKind: "option" };
4906
+ // src/core/validate/steps/visibility-cycles.ts
4907
+ var MAX_VISIBILITY_CYCLE_DEPTH = 20;
4908
+ function validateVisibilityCycles(v) {
4909
+ const triggerById = buildTriggerIndex(v.fields);
4910
+ if (!triggerById.size) return;
4911
+ const fieldTriggers = buildFieldTriggerIndex(v.fields);
4912
+ const revealTargetsByTrigger = buildRevealIndex(v, triggerById);
4913
+ const reported = /* @__PURE__ */ new Set();
4914
+ for (const rootTriggerId of Array.from(triggerById.keys()).sort()) {
4915
+ const required = makeRequiredState(triggerById, [rootTriggerId]);
4916
+ walkFromTrigger({
4917
+ v,
4918
+ triggerById,
4919
+ fieldTriggers,
4920
+ revealTargetsByTrigger,
4921
+ rootTriggerId,
4922
+ currentTriggerId: rootTriggerId,
4923
+ required,
4924
+ path: [rootTriggerId],
4925
+ visited: /* @__PURE__ */ new Set(),
4926
+ reported,
4927
+ depth: 0
4928
+ });
4929
+ }
4930
+ }
4931
+ function buildTriggerIndex(fields) {
4932
+ const out = /* @__PURE__ */ new Map();
4933
+ const owners = optionOwnerMap(fields);
4934
+ for (const field of fields) {
4935
+ if (field.button === true) {
4936
+ out.set(field.id, {
4937
+ kind: "field",
4938
+ id: field.id,
4939
+ ownerFieldId: field.id
4940
+ });
4941
+ }
4942
+ }
4943
+ for (const [optionId, owner] of owners) {
4944
+ out.set(optionId, {
4945
+ kind: "option",
4946
+ id: optionId,
4947
+ ownerFieldId: owner.fieldId
4948
+ });
4949
+ }
4950
+ return out;
4951
+ }
4952
+ function buildFieldTriggerIndex(fields) {
4953
+ const out = /* @__PURE__ */ new Map();
4954
+ for (const field of fields) {
4955
+ const triggers = [];
4956
+ if (field.button === true) triggers.push(field.id);
4957
+ for (const visit of walkFieldOptions(field)) {
4958
+ triggers.push(visit.optionId);
4959
+ }
4960
+ out.set(field.id, triggers);
4961
+ }
4962
+ return out;
4963
+ }
4964
+ function buildRevealIndex(v, triggerById) {
4965
+ var _a, _b;
4966
+ const out = /* @__PURE__ */ new Map();
4967
+ const addReveal = (triggerId, targetFieldId) => {
4968
+ var _a2;
4969
+ if (!triggerById.has(triggerId)) return;
4970
+ if (!v.fieldById.has(targetFieldId)) return;
4971
+ const set = (_a2 = out.get(triggerId)) != null ? _a2 : /* @__PURE__ */ new Set();
4972
+ set.add(targetFieldId);
4973
+ out.set(triggerId, set);
4974
+ };
4975
+ for (const [triggerId, targetIds] of Object.entries(
4976
+ (_a = v.props.includes_for_buttons) != null ? _a : {}
4977
+ )) {
4978
+ for (const targetId of targetIds != null ? targetIds : []) addReveal(triggerId, targetId);
4979
+ }
4980
+ for (const [triggerId, targets] of Object.entries(
4981
+ (_b = v.props.option_effects_for_buttons) != null ? _b : {}
4982
+ )) {
4983
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
4984
+ if ((effect == null ? void 0 : effect.forceVisible) === true)
4985
+ addReveal(triggerId, targetFieldId);
4986
+ }
4987
+ }
4988
+ return new Map(
4989
+ Array.from(out.entries()).map(([triggerId, fieldIds]) => [
4990
+ triggerId,
4991
+ Array.from(fieldIds).sort()
4992
+ ])
4993
+ );
4994
+ }
4995
+ function walkFromTrigger(args) {
4996
+ var _a, _b, _c;
4997
+ if (args.depth >= MAX_VISIBILITY_CYCLE_DEPTH) return;
4998
+ const visitedKey = `${args.rootTriggerId}::${args.currentTriggerId}::${args.path.join(">")}`;
4999
+ if (args.visited.has(visitedKey)) return;
5000
+ args.visited.add(visitedKey);
5001
+ const revealedFieldIds = (_a = args.revealTargetsByTrigger.get(args.currentTriggerId)) != null ? _a : [];
5002
+ for (const revealedFieldId of revealedFieldIds) {
5003
+ const reachableTriggers = (_c = (_b = args.fieldTriggers.get(revealedFieldId)) == null ? void 0 : _b.slice().sort()) != null ? _c : [];
5004
+ for (const reachableTriggerId of reachableTriggers) {
5005
+ const invalidation = invalidatesRequiredPath(
5006
+ args.v,
5007
+ args.triggerById,
5008
+ reachableTriggerId,
5009
+ args.required
5010
+ );
5011
+ if (invalidation) {
5012
+ emitCycleError({
5013
+ v: args.v,
5014
+ rootTriggerId: args.rootTriggerId,
5015
+ revealedFieldId,
5016
+ conflictingTriggerId: reachableTriggerId,
5017
+ invalidatedId: invalidation.invalidatedId,
5018
+ path: [...args.path, reachableTriggerId],
5019
+ reported: args.reported
5020
+ });
4727
5021
  }
5022
+ if (args.path.includes(reachableTriggerId)) continue;
5023
+ walkFromTrigger({
5024
+ ...args,
5025
+ currentTriggerId: reachableTriggerId,
5026
+ required: addRequiredTrigger(
5027
+ args.triggerById,
5028
+ args.required,
5029
+ reachableTriggerId
5030
+ ),
5031
+ path: [...args.path, reachableTriggerId],
5032
+ depth: args.depth + 1
5033
+ });
4728
5034
  }
4729
- if (fieldId) {
4730
- const fieldRef = nodeMap.get(fieldId);
4731
- if ((fieldRef == null ? void 0 : fieldRef.kind) === "field") {
4732
- return { nodeId: fieldRef.id, nodeKind: "field" };
5035
+ }
5036
+ }
5037
+ function makeRequiredState(triggerById, triggerIds) {
5038
+ let required = {
5039
+ triggers: /* @__PURE__ */ new Set(),
5040
+ ownerFields: /* @__PURE__ */ new Set()
5041
+ };
5042
+ for (const triggerId of triggerIds) {
5043
+ required = addRequiredTrigger(triggerById, required, triggerId);
5044
+ }
5045
+ return required;
5046
+ }
5047
+ function addRequiredTrigger(triggerById, current, triggerId) {
5048
+ const next = {
5049
+ triggers: new Set(current.triggers),
5050
+ ownerFields: new Set(current.ownerFields)
5051
+ };
5052
+ const trigger = triggerById.get(triggerId);
5053
+ if (!trigger) return next;
5054
+ next.triggers.add(triggerId);
5055
+ next.ownerFields.add(trigger.ownerFieldId);
5056
+ return next;
5057
+ }
5058
+ function invalidatesRequiredPath(v, triggerById, conflictingTriggerId, required) {
5059
+ var _a, _b, _c, _d, _e, _f;
5060
+ for (const targetId of (_b = (_a = v.props.excludes_for_buttons) == null ? void 0 : _a[conflictingTriggerId]) != null ? _b : []) {
5061
+ if (required.ownerFields.has(targetId)) {
5062
+ return { invalidatedId: targetId };
5063
+ }
5064
+ const targetTrigger = triggerById.get(targetId);
5065
+ if ((targetTrigger == null ? void 0 : targetTrigger.kind) === "option" && required.triggers.has(targetId)) {
5066
+ return { invalidatedId: targetId };
5067
+ }
5068
+ }
5069
+ const effects = (_d = (_c = v.props.option_effects_for_buttons) == null ? void 0 : _c[conflictingTriggerId]) != null ? _d : {};
5070
+ for (const [targetFieldId, effect] of Object.entries(effects)) {
5071
+ if (!v.fieldById.has(targetFieldId)) continue;
5072
+ if ((_e = effect == null ? void 0 : effect.exclude) == null ? void 0 : _e.length) {
5073
+ const excluded = new Set(effect.exclude);
5074
+ for (const requiredTriggerId of required.triggers) {
5075
+ const requiredTrigger = triggerById.get(requiredTriggerId);
5076
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
5077
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
5078
+ if (excluded.has(requiredTriggerId)) {
5079
+ return { invalidatedId: requiredTriggerId };
5080
+ }
5081
+ }
5082
+ }
5083
+ if ((_f = effect == null ? void 0 : effect.include) == null ? void 0 : _f.length) {
5084
+ const included = new Set(effect.include);
5085
+ for (const requiredTriggerId of required.triggers) {
5086
+ const requiredTrigger = triggerById.get(requiredTriggerId);
5087
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
5088
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
5089
+ if (!included.has(requiredTriggerId)) {
5090
+ return { invalidatedId: requiredTriggerId };
5091
+ }
4733
5092
  }
4734
5093
  }
4735
- return void 0;
4736
5094
  }
5095
+ return void 0;
5096
+ }
5097
+ function emitCycleError(args) {
5098
+ const key = [
5099
+ args.rootTriggerId,
5100
+ args.conflictingTriggerId,
5101
+ args.invalidatedId,
5102
+ args.path.join(">")
5103
+ ].join("::");
5104
+ if (args.reported.has(key)) return;
5105
+ args.reported.add(key);
5106
+ args.v.errors.push({
5107
+ code: "visibility_dependency_cycle",
5108
+ severity: "error",
5109
+ message: `Visibility dependency cycle: trigger "${args.rootTriggerId}" reveals "${args.revealedFieldId}", but reachable trigger "${args.conflictingTriggerId}" can hide or remove "${args.invalidatedId}".`,
5110
+ nodeId: args.conflictingTriggerId,
5111
+ details: withAffected(
5112
+ {
5113
+ rootTriggerId: args.rootTriggerId,
5114
+ conflictingTriggerId: args.conflictingTriggerId,
5115
+ invalidatedId: args.invalidatedId,
5116
+ path: args.path
5117
+ },
5118
+ [
5119
+ args.rootTriggerId,
5120
+ args.revealedFieldId,
5121
+ args.conflictingTriggerId,
5122
+ args.invalidatedId
5123
+ ]
5124
+ )
5125
+ });
5126
+ }
5127
+
5128
+ // src/utils/order-kind.ts
5129
+ function normalizeSelectedTriggerKey(key, nodeMap) {
5130
+ if (!key) return void 0;
4737
5131
  const ref = nodeMap.get(key);
4738
5132
  if (!ref) return void 0;
4739
5133
  if (ref.kind !== "field" && ref.kind !== "option") return void 0;
@@ -4892,8 +5286,7 @@ function validateUtilityMarkers(v) {
4892
5286
  "percent"
4893
5287
  ]);
4894
5288
  for (const f of v.fields) {
4895
- const optsArr = Array.isArray(f.options) ? f.options : [];
4896
- for (const o of optsArr) {
5289
+ for (const { option: o } of walkFieldOptions(f)) {
4897
5290
  const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
4898
5291
  const hasService = isServiceIdRef(o.service_id);
4899
5292
  const util = (_c = o.meta) == null ? void 0 : _c.utility;
@@ -5115,13 +5508,13 @@ function normalizeServiceRef(value) {
5115
5508
 
5116
5509
  // src/core/validate/steps/rates.ts
5117
5510
  function validateRates(v) {
5118
- var _a, _b, _c;
5511
+ var _a, _b;
5119
5512
  const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
5120
5513
  for (const f of v.fields) {
5121
5514
  if (!isMultiField(f)) continue;
5122
5515
  const baseRates = [];
5123
- for (const o of (_a = f.options) != null ? _a : []) {
5124
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
5516
+ for (const { option: o } of walkFieldOptions(f)) {
5517
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
5125
5518
  if (role !== "base") continue;
5126
5519
  const sid = o.service_id;
5127
5520
  if (!isServiceIdRef(sid)) continue;
@@ -5532,7 +5925,7 @@ function effectiveConstraints(v, tagId) {
5532
5925
  return out;
5533
5926
  }
5534
5927
  function validateConstraints(v) {
5535
- var _a, _b;
5928
+ var _a;
5536
5929
  for (const t of v.tags) {
5537
5930
  const eff = effectiveConstraints(v, t.id);
5538
5931
  const hasAnyRequired = Object.values(eff).some(
@@ -5541,7 +5934,7 @@ function validateConstraints(v) {
5541
5934
  if (!hasAnyRequired) continue;
5542
5935
  const visible = v.fieldsVisibleUnder(t.id);
5543
5936
  for (const f of visible) {
5544
- for (const o of (_a = f.options) != null ? _a : []) {
5937
+ for (const { option: o } of walkFieldOptions(f)) {
5545
5938
  if (!isServiceIdRef(o.service_id)) continue;
5546
5939
  const svc = getServiceCapability(v.serviceMap, o.service_id);
5547
5940
  if (!svc || typeof svc !== "object") continue;
@@ -5595,7 +5988,7 @@ function validateConstraints(v) {
5595
5988
  if (!row) continue;
5596
5989
  const from = row.from === true;
5597
5990
  const to = row.to === true;
5598
- const origin = String((_b = row.origin) != null ? _b : "");
5991
+ const origin = String((_a = row.origin) != null ? _a : "");
5599
5992
  v.errors.push({
5600
5993
  code: "constraint_overridden",
5601
5994
  severity: "warning",
@@ -5629,14 +6022,14 @@ function validateCustomFields(v) {
5629
6022
 
5630
6023
  // src/core/validate/steps/global-utility-guard.ts
5631
6024
  function validateGlobalUtilityGuard(v) {
5632
- var _a, _b, _c;
6025
+ var _a, _b;
5633
6026
  if (!v.options.globalUtilityGuard) return;
5634
6027
  let hasUtility = false;
5635
6028
  let hasBase = false;
5636
6029
  for (const f of v.fields) {
5637
- for (const o of (_a = f.options) != null ? _a : []) {
6030
+ for (const { option: o } of walkFieldOptions(f)) {
5638
6031
  if (!isServiceIdRef(o.service_id)) continue;
5639
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
6032
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
5640
6033
  if (role === "base") hasBase = true;
5641
6034
  else if (role === "utility") hasUtility = true;
5642
6035
  if (hasUtility && hasBase) break;
@@ -5838,7 +6231,7 @@ function applyFilterAllowLists(tagId, fieldId, filter) {
5838
6231
  return true;
5839
6232
  }
5840
6233
  function collectServiceItems(args) {
5841
- var _a, _b, _c, _d, _e;
6234
+ var _a, _b, _c, _d;
5842
6235
  const filter = args.filter;
5843
6236
  const roleFilter = (_a = filter == null ? void 0 : filter.role) != null ? _a : "both";
5844
6237
  const where = filter == null ? void 0 : filter.where;
@@ -5888,7 +6281,7 @@ function collectServiceItems(args) {
5888
6281
  affectedIds: [`field:${f.id}`, `service:${String(fSid)}`]
5889
6282
  });
5890
6283
  }
5891
- for (const o of (_d = f.options) != null ? _d : []) {
6284
+ for (const { option: o } of walkFieldOptions(f)) {
5892
6285
  const oSid = o.service_id;
5893
6286
  if (!isServiceIdRef2(oSid)) continue;
5894
6287
  const role = fieldRoleOf(f, o);
@@ -5973,7 +6366,7 @@ function collectServiceItems(args) {
5973
6366
  }
5974
6367
  } else if (includeGroupFallbacks) {
5975
6368
  const allowPrimaries = new Set(
5976
- ((_e = args.visiblePrimaries) != null ? _e : []).map((x) => String(x))
6369
+ ((_d = args.visiblePrimaries) != null ? _d : []).map((x) => String(x))
5977
6370
  );
5978
6371
  for (const primaryKey of allowPrimaries) {
5979
6372
  const list = globalFb[primaryKey];
@@ -6054,17 +6447,15 @@ function affectedFromItems(items) {
6054
6447
  return uniq(ids);
6055
6448
  }
6056
6449
  function visibleGroupNodeIds(tag, fields) {
6057
- var _a;
6058
6450
  const ids = [tag.id];
6059
6451
  for (const f of fields) {
6060
- for (const o of (_a = f.options) != null ? _a : []) {
6452
+ for (const { option: o } of walkFieldOptions(f)) {
6061
6453
  ids.push(o.id);
6062
6454
  }
6063
6455
  }
6064
6456
  return uniq(ids);
6065
6457
  }
6066
6458
  function visibleGroupPrimaries(tag, fields) {
6067
- var _a;
6068
6459
  const prim = [];
6069
6460
  const tagSid = tag.service_id;
6070
6461
  if (typeof tagSid === "string" || typeof tagSid === "number" && Number.isFinite(tagSid)) {
@@ -6075,7 +6466,7 @@ function visibleGroupPrimaries(tag, fields) {
6075
6466
  if (typeof fsid === "string" || typeof fsid === "number" && Number.isFinite(fsid)) {
6076
6467
  prim.push(fsid);
6077
6468
  }
6078
- for (const o of (_a = f.options) != null ? _a : []) {
6469
+ for (const { option: o } of walkFieldOptions(f)) {
6079
6470
  const osid = o.service_id;
6080
6471
  if (typeof osid === "string" || typeof osid === "number" && Number.isFinite(osid)) {
6081
6472
  prim.push(osid);
@@ -6299,6 +6690,7 @@ function validate(props, ctx = {}) {
6299
6690
  validateStructure(v);
6300
6691
  validateIdentity(v);
6301
6692
  validateOptionMaps(v);
6693
+ validateVisibilityCycles(v);
6302
6694
  validateOrderKinds(v);
6303
6695
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
6304
6696
  const visSim = readVisibilitySimOpts(options);
@@ -6432,14 +6824,14 @@ var BuilderImpl = class {
6432
6824
  const showOptions = showSet.has(f.id);
6433
6825
  if (!showOptions) continue;
6434
6826
  if (!Array.isArray(f.options)) continue;
6435
- for (const o of f.options) {
6827
+ for (const { option: o, parentId } of walkFieldOptions(f)) {
6436
6828
  nodes.push({
6437
6829
  id: o.id,
6438
6830
  kind: "option",
6439
6831
  label: o.label
6440
6832
  });
6441
6833
  const e = {
6442
- from: f.id,
6834
+ from: parentId != null ? parentId : f.id,
6443
6835
  to: o.id,
6444
6836
  kind: "option",
6445
6837
  meta: { ownerField: f.id }
@@ -6486,7 +6878,7 @@ var BuilderImpl = class {
6486
6878
  return { nodes, edges };
6487
6879
  }
6488
6880
  cleanedProps() {
6489
- var _a, _b, _c, _d, _e;
6881
+ var _a, _b, _c, _d, _e, _f;
6490
6882
  const fieldIds = new Set(this.props.fields.map((f) => f.id));
6491
6883
  const optionIds = /* @__PURE__ */ new Set();
6492
6884
  this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
@@ -6498,6 +6890,7 @@ var BuilderImpl = class {
6498
6890
  }
6499
6891
  const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
6500
6892
  const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
6893
+ const effectMap = (_e = this.props.option_effects_for_buttons) != null ? _e : {};
6501
6894
  const includedByButtons = /* @__PURE__ */ new Set();
6502
6895
  const referencedKeys = /* @__PURE__ */ new Set();
6503
6896
  const referencedOwnerFields = /* @__PURE__ */ new Set();
@@ -6517,6 +6910,14 @@ var BuilderImpl = class {
6517
6910
  void fid;
6518
6911
  }
6519
6912
  }
6913
+ for (const [key, targets] of Object.entries(effectMap)) {
6914
+ referencedKeys.add(key);
6915
+ const owner = this.optionOwnerById.get(key);
6916
+ if (owner) referencedOwnerFields.add(owner.fieldId);
6917
+ for (const [fid, effect] of Object.entries(targets != null ? targets : {})) {
6918
+ if ((effect == null ? void 0 : effect.forceVisible) === true) includedByButtons.add(fid);
6919
+ }
6920
+ }
6520
6921
  const boundIds = /* @__PURE__ */ new Set();
6521
6922
  for (const f of this.props.fields) {
6522
6923
  const b = f.bind_id;
@@ -6534,6 +6935,7 @@ var BuilderImpl = class {
6534
6935
  return bound || included || referenced || !excluded;
6535
6936
  });
6536
6937
  const allowedTargets = new Set(fields.map((f) => f.id));
6938
+ const allowedFieldById = new Map(fields.map((f) => [f.id, f]));
6537
6939
  const pruneButtons = (src) => {
6538
6940
  if (!src) return void 0;
6539
6941
  const out2 = {};
@@ -6553,13 +6955,52 @@ var BuilderImpl = class {
6553
6955
  const excludes_for_buttons = pruneButtons(
6554
6956
  this.props.excludes_for_buttons
6555
6957
  );
6958
+ const pruneOptionEffects = (src) => {
6959
+ var _a2, _b2, _c2, _d2;
6960
+ if (!src) return void 0;
6961
+ const out2 = {};
6962
+ for (const [key, targets] of Object.entries(src)) {
6963
+ const keyIsValid = optionIds.has(key) || fieldIds.has(key);
6964
+ if (!keyIsValid) continue;
6965
+ const cleanedTargets = {};
6966
+ for (const [targetFieldId, effect] of Object.entries(
6967
+ targets != null ? targets : {}
6968
+ )) {
6969
+ const field = allowedFieldById.get(targetFieldId);
6970
+ if (!field || !effect) continue;
6971
+ const validOptionIds = fieldOptionIdSet(field);
6972
+ const include2 = Array.from(
6973
+ new Set((_a2 = effect.include) != null ? _a2 : [])
6974
+ ).filter((optionId) => validOptionIds.has(optionId));
6975
+ const exclude2 = Array.from(
6976
+ new Set((_b2 = effect.exclude) != null ? _b2 : [])
6977
+ ).filter((optionId) => validOptionIds.has(optionId));
6978
+ const next = {
6979
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
6980
+ ...include2.length ? { include: include2 } : {},
6981
+ ...exclude2.length ? { exclude: exclude2 } : {}
6982
+ };
6983
+ if (next.forceVisible === true || ((_c2 = next.include) == null ? void 0 : _c2.length) || ((_d2 = next.exclude) == null ? void 0 : _d2.length)) {
6984
+ cleanedTargets[targetFieldId] = next;
6985
+ }
6986
+ }
6987
+ if (Object.keys(cleanedTargets).length) {
6988
+ out2[key] = cleanedTargets;
6989
+ }
6990
+ }
6991
+ return Object.keys(out2).length ? out2 : void 0;
6992
+ };
6993
+ const option_effects_for_buttons = pruneOptionEffects(
6994
+ this.props.option_effects_for_buttons
6995
+ );
6556
6996
  const out = {
6557
6997
  filters: this.props.filters.slice(),
6558
6998
  fields,
6559
6999
  ...this.props.orderKinds ? { orderKinds: this.props.orderKinds } : {},
6560
7000
  ...includes_for_buttons && { includes_for_buttons },
6561
7001
  ...excludes_for_buttons && { excludes_for_buttons },
6562
- schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
7002
+ ...option_effects_for_buttons && { option_effects_for_buttons },
7003
+ schema_version: (_f = this.props.schema_version) != null ? _f : "1.0",
6563
7004
  // keep fallbacks & other maps as-is
6564
7005
  ...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
6565
7006
  };
@@ -6572,12 +7013,15 @@ var BuilderImpl = class {
6572
7013
  return (0, import_lodash_es2.cloneDeep)(this.options);
6573
7014
  }
6574
7015
  visibleFields(tagId, selectedKeys) {
7016
+ return this.resolveVisibility(tagId, selectedKeys).fieldIds;
7017
+ }
7018
+ resolveVisibility(tagId, selectedKeys) {
6575
7019
  var _a;
6576
- return visibleFieldIdsUnder(this.props, tagId, {
6577
- selectedKeys: new Set(
6578
- (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
6579
- )
6580
- });
7020
+ return resolveVisibility(
7021
+ this.props,
7022
+ tagId,
7023
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
7024
+ );
6581
7025
  }
6582
7026
  getNodeMap() {
6583
7027
  if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
@@ -6592,9 +7036,8 @@ var BuilderImpl = class {
6592
7036
  for (const t of this.props.filters) this.tagById.set(t.id, t);
6593
7037
  for (const f of this.props.fields) {
6594
7038
  this.fieldById.set(f.id, f);
6595
- if (Array.isArray(f.options)) {
6596
- for (const o of f.options)
6597
- this.optionOwnerById.set(o.id, { fieldId: f.id });
7039
+ for (const [optionId, owner] of optionOwnerMap([f])) {
7040
+ this.optionOwnerById.set(optionId, { fieldId: owner.fieldId });
6598
7041
  }
6599
7042
  }
6600
7043
  }
@@ -7542,44 +7985,135 @@ function bumpSuffix(old) {
7542
7985
  return `${stem}${parseInt(m[2], 10) + 1}`;
7543
7986
  }
7544
7987
 
7545
- // src/react/canvas/editor/editor-duplicate.ts
7546
- function duplicate(ctx, ref, opts = {}) {
7547
- const snapBefore = ctx.makeSnapshot("duplicate:before");
7548
- try {
7549
- let newId2 = "";
7550
- ctx.transact("duplicate", () => {
7551
- newId2 = duplicateInPlace(ctx, ref, opts);
7552
- });
7553
- return newId2;
7554
- } catch (err) {
7555
- ctx.loadSnapshot(snapBefore, "undo");
7556
- throw err;
7988
+ // src/react/canvas/editor/editor-utils.ts
7989
+ function ownerOfOption(props, optionId) {
7990
+ var _a;
7991
+ for (const f of (_a = props.fields) != null ? _a : []) {
7992
+ const found = findOptionLocationInField(f, optionId);
7993
+ if (found) return { fieldId: f.id, index: found.index };
7557
7994
  }
7995
+ return null;
7558
7996
  }
7559
- function duplicateMany(ctx, ids, opts = {}) {
7560
- const ordered = Array.from(new Set((ids != null ? ids : []).map((id) => String(id))));
7561
- if (!ordered.length) return [];
7562
- const snapBefore = ctx.makeSnapshot("duplicateMany:before");
7563
- try {
7564
- const created = [];
7565
- ctx.transact("duplicateMany", () => {
7566
- var _a, _b, _c;
7567
- const props = ctx.getProps();
7568
- const selectedFields = /* @__PURE__ */ new Set();
7569
- for (const id of ordered) {
7570
- if (ctx.isFieldId(id) && ((_a = props.fields) != null ? _a : []).some((f) => f.id === id)) {
7571
- selectedFields.add(id);
7572
- }
7573
- }
7574
- for (const id of ordered) {
7575
- if (ctx.isTagId(id)) {
7576
- if (!((_b = ctx.getProps().filters) != null ? _b : []).some((t) => t.id === id)) continue;
7577
- created.push(
7578
- duplicateInPlace(ctx, { kind: "tag", id }, opts)
7579
- );
7580
- continue;
7581
- }
7582
- if (ctx.isFieldId(id)) {
7997
+ function findMutableOption(props, optionId) {
7998
+ var _a;
7999
+ for (const field of (_a = props.fields) != null ? _a : []) {
8000
+ const found = findOptionLocationInField(field, optionId);
8001
+ if (found) return { field, ...found };
8002
+ }
8003
+ return void 0;
8004
+ }
8005
+ function collectFieldOptionIds(field) {
8006
+ const out = [];
8007
+ const visit = (options) => {
8008
+ for (const option of options != null ? options : []) {
8009
+ out.push(String(option.id));
8010
+ visit(option.children);
8011
+ }
8012
+ };
8013
+ visit(field == null ? void 0 : field.options);
8014
+ return out;
8015
+ }
8016
+ function findOptionLocationInField(field, optionId) {
8017
+ const visit = (siblings, parent) => {
8018
+ if (!siblings) return void 0;
8019
+ const index = siblings.findIndex((option) => option.id === optionId);
8020
+ if (index >= 0) {
8021
+ return {
8022
+ option: siblings[index],
8023
+ siblings,
8024
+ index,
8025
+ parent
8026
+ };
8027
+ }
8028
+ for (const option of siblings) {
8029
+ const found = visit(option.children, option);
8030
+ if (found) return found;
8031
+ }
8032
+ return void 0;
8033
+ };
8034
+ return visit(field.options);
8035
+ }
8036
+ function hasFieldOptions(field) {
8037
+ return Array.isArray(field == null ? void 0 : field.options) && field.options.length > 0;
8038
+ }
8039
+ function isActualButtonField(field) {
8040
+ return (field == null ? void 0 : field.button) === true && !hasFieldOptions(field);
8041
+ }
8042
+ function clearFieldButtonReceiverMaps(props, fieldId) {
8043
+ var _a, _b, _c;
8044
+ if ((_a = props.includes_for_buttons) == null ? void 0 : _a[fieldId]) {
8045
+ delete props.includes_for_buttons[fieldId];
8046
+ }
8047
+ if ((_b = props.excludes_for_buttons) == null ? void 0 : _b[fieldId]) {
8048
+ delete props.excludes_for_buttons[fieldId];
8049
+ }
8050
+ if (props.includes_for_buttons && Object.keys(props.includes_for_buttons).length === 0) {
8051
+ delete props.includes_for_buttons;
8052
+ }
8053
+ if (props.excludes_for_buttons && Object.keys(props.excludes_for_buttons).length === 0) {
8054
+ delete props.excludes_for_buttons;
8055
+ }
8056
+ if ((_c = props.option_effects_for_buttons) == null ? void 0 : _c[fieldId]) {
8057
+ delete props.option_effects_for_buttons[fieldId];
8058
+ }
8059
+ if (props.option_effects_for_buttons && Object.keys(props.option_effects_for_buttons).length === 0) {
8060
+ delete props.option_effects_for_buttons;
8061
+ }
8062
+ }
8063
+ function ensureServiceExists(opts, id) {
8064
+ if (typeof opts.serviceExists === "function") {
8065
+ if (!opts.serviceExists(id)) {
8066
+ throw new Error(`service_not_found:${String(id)}`);
8067
+ }
8068
+ return;
8069
+ }
8070
+ if (opts.serviceMap) {
8071
+ if (!Object.prototype.hasOwnProperty.call(opts.serviceMap, id)) {
8072
+ throw new Error(`service_not_found:${String(id)}`);
8073
+ }
8074
+ return;
8075
+ }
8076
+ throw new Error("service_checker_missing");
8077
+ }
8078
+
8079
+ // src/react/canvas/editor/editor-duplicate.ts
8080
+ function duplicate(ctx, ref, opts = {}) {
8081
+ const snapBefore = ctx.makeSnapshot("duplicate:before");
8082
+ try {
8083
+ let newId2 = "";
8084
+ ctx.transact("duplicate", () => {
8085
+ newId2 = duplicateInPlace(ctx, ref, opts);
8086
+ });
8087
+ return newId2;
8088
+ } catch (err) {
8089
+ ctx.loadSnapshot(snapBefore, "undo");
8090
+ throw err;
8091
+ }
8092
+ }
8093
+ function duplicateMany(ctx, ids, opts = {}) {
8094
+ const ordered = Array.from(new Set((ids != null ? ids : []).map((id) => String(id))));
8095
+ if (!ordered.length) return [];
8096
+ const snapBefore = ctx.makeSnapshot("duplicateMany:before");
8097
+ try {
8098
+ const created = [];
8099
+ ctx.transact("duplicateMany", () => {
8100
+ var _a, _b, _c;
8101
+ const props = ctx.getProps();
8102
+ const selectedFields = /* @__PURE__ */ new Set();
8103
+ for (const id of ordered) {
8104
+ if (ctx.isFieldId(id) && ((_a = props.fields) != null ? _a : []).some((f) => f.id === id)) {
8105
+ selectedFields.add(id);
8106
+ }
8107
+ }
8108
+ for (const id of ordered) {
8109
+ if (ctx.isTagId(id)) {
8110
+ if (!((_b = ctx.getProps().filters) != null ? _b : []).some((t) => t.id === id)) continue;
8111
+ created.push(
8112
+ duplicateInPlace(ctx, { kind: "tag", id }, opts)
8113
+ );
8114
+ continue;
8115
+ }
8116
+ if (ctx.isFieldId(id)) {
7583
8117
  if (!((_c = ctx.getProps().fields) != null ? _c : []).some((f) => f.id === id)) continue;
7584
8118
  created.push(
7585
8119
  duplicateInPlace(ctx, { kind: "field", id }, opts)
@@ -7616,14 +8150,66 @@ function duplicateInPlace(ctx, ref, opts = {}) {
7616
8150
  return duplicateOption(ctx, ref.fieldId, ref.id, opts);
7617
8151
  }
7618
8152
  function ownerFieldOfOption(props, optionId) {
7619
- var _a, _b;
8153
+ var _a;
7620
8154
  for (const field of (_a = props.fields) != null ? _a : []) {
7621
- if (((_b = field.options) != null ? _b : []).some((o) => o.id === optionId)) {
8155
+ if (findMutableOption({ ...props, fields: [field] }, optionId)) {
7622
8156
  return { fieldId: field.id };
7623
8157
  }
7624
8158
  }
7625
8159
  return null;
7626
8160
  }
8161
+ function cloneOptionTree(ctx, fieldId, option, opts, optionIdMap) {
8162
+ var _a, _b, _c, _d;
8163
+ const newId2 = ctx.uniqueOptionId(
8164
+ fieldId,
8165
+ ((_a = opts.optionIdStrategy) != null ? _a : defaultOptionIdStrategy)(option.id)
8166
+ );
8167
+ optionIdMap.set(option.id, newId2);
8168
+ const children = (_b = option.children) == null ? void 0 : _b.map(
8169
+ (child) => cloneOptionTree(ctx, fieldId, child, opts, optionIdMap)
8170
+ );
8171
+ return {
8172
+ ...option,
8173
+ id: newId2,
8174
+ label: ((_c = opts.labelStrategy) != null ? _c : nextCopyLabel)((_d = option.label) != null ? _d : option.id),
8175
+ ...(children == null ? void 0 : children.length) ? { children } : {}
8176
+ };
8177
+ }
8178
+ function remapEffect(effect, optionIdMap) {
8179
+ const remapList = (values) => values == null ? void 0 : values.map((value) => {
8180
+ var _a;
8181
+ return (_a = optionIdMap.get(value)) != null ? _a : value;
8182
+ });
8183
+ return {
8184
+ ...effect,
8185
+ ...effect.include ? { include: remapList(effect.include) } : {},
8186
+ ...effect.exclude ? { exclude: remapList(effect.exclude) } : {}
8187
+ };
8188
+ }
8189
+ function copyOptionEffects(props, args) {
8190
+ var _a, _b, _c, _d, _e;
8191
+ const source = props.option_effects_for_buttons;
8192
+ if (!source) return;
8193
+ const next = {
8194
+ ...source
8195
+ };
8196
+ const triggerIdMap = (_a = args.triggerIdMap) != null ? _a : /* @__PURE__ */ new Map();
8197
+ const targetFieldIdMap = (_b = args.targetFieldIdMap) != null ? _b : /* @__PURE__ */ new Map();
8198
+ const optionIdMap = (_c = args.optionIdMap) != null ? _c : /* @__PURE__ */ new Map();
8199
+ for (const [oldTriggerId, targetMap] of Object.entries(source)) {
8200
+ const newTriggerId = triggerIdMap.get(oldTriggerId);
8201
+ if (!newTriggerId) continue;
8202
+ const copiedTargets = {
8203
+ ...(_d = next[newTriggerId]) != null ? _d : {}
8204
+ };
8205
+ for (const [oldTargetFieldId, effect] of Object.entries(targetMap != null ? targetMap : {})) {
8206
+ const newTargetFieldId = (_e = targetFieldIdMap.get(oldTargetFieldId)) != null ? _e : oldTargetFieldId;
8207
+ copiedTargets[newTargetFieldId] = remapEffect(effect, optionIdMap);
8208
+ }
8209
+ next[newTriggerId] = copiedTargets;
8210
+ }
8211
+ props.option_effects_for_buttons = next;
8212
+ }
7627
8213
  function duplicateTag(ctx, tagId, opts) {
7628
8214
  var _a, _b, _c, _d;
7629
8215
  const props = ctx.getProps();
@@ -7679,7 +8265,7 @@ function duplicateTag(ctx, tagId, opts) {
7679
8265
  return id;
7680
8266
  }
7681
8267
  function duplicateField(ctx, fieldId, opts) {
7682
- var _a, _b, _c, _d, _e, _f, _g;
8268
+ var _a, _b, _c, _d, _e, _f;
7683
8269
  const props = ctx.getProps();
7684
8270
  const fields = (_a = props.fields) != null ? _a : [];
7685
8271
  const src = fields.find((f) => f.id === fieldId);
@@ -7687,21 +8273,10 @@ function duplicateField(ctx, fieldId, opts) {
7687
8273
  const id = (_b = opts.id) != null ? _b : ctx.uniqueId(src.id);
7688
8274
  const label = ((_c = opts.labelStrategy) != null ? _c : nextCopyLabel)((_d = src.label) != null ? _d : id);
7689
8275
  const name = opts.nameStrategy ? opts.nameStrategy(src.name) : nextCopyName(src.name);
7690
- const optId = (old) => {
7691
- var _a2;
7692
- return ctx.uniqueOptionId(
7693
- id,
7694
- ((_a2 = opts.optionIdStrategy) != null ? _a2 : defaultOptionIdStrategy)(old)
7695
- );
7696
- };
7697
- const clonedOptions = ((_e = src.options) != null ? _e : []).map((o) => {
7698
- var _a2, _b2;
7699
- return {
7700
- ...o,
7701
- id: optId(o.id),
7702
- label: ((_a2 = opts.labelStrategy) != null ? _a2 : nextCopyLabel)((_b2 = o.label) != null ? _b2 : o.id)
7703
- };
7704
- });
8276
+ const optionIdMap = /* @__PURE__ */ new Map();
8277
+ const clonedOptions = ((_e = src.options) != null ? _e : []).map(
8278
+ (o) => cloneOptionTree(ctx, id, o, opts, optionIdMap)
8279
+ );
7705
8280
  const cloned = {
7706
8281
  ...src,
7707
8282
  id,
@@ -7710,14 +8285,8 @@ function duplicateField(ctx, fieldId, opts) {
7710
8285
  bind_id: ((_f = opts.copyBindings) != null ? _f : true) ? src.bind_id : void 0,
7711
8286
  options: clonedOptions
7712
8287
  };
7713
- const optionIdMap = /* @__PURE__ */ new Map();
7714
- ((_g = src.options) != null ? _g : []).forEach((o, i) => {
7715
- var _a2, _b2;
7716
- const newOptId = (_b2 = (_a2 = clonedOptions[i]) == null ? void 0 : _a2.id) != null ? _b2 : o.id;
7717
- optionIdMap.set(o.id, newOptId);
7718
- });
7719
8288
  ctx.patchProps((p) => {
7720
- var _a2, _b2, _c2, _d2, _e2, _f2, _g2;
8289
+ var _a2, _b2, _c2, _d2, _e2, _f2, _g;
7721
8290
  const arr = (_a2 = p.fields) != null ? _a2 : [];
7722
8291
  const idx = arr.findIndex((f) => f.id === fieldId);
7723
8292
  arr.splice(idx + 1, 0, cloned);
@@ -7755,52 +8324,56 @@ function duplicateField(ctx, fieldId, opts) {
7755
8324
  }
7756
8325
  if (optionIdMap.has(key)) {
7757
8326
  const newKey = optionIdMap.get(key);
7758
- const merged = /* @__PURE__ */ new Set([...(_g2 = nextMap[newKey]) != null ? _g2 : [], ...targets]);
8327
+ const merged = /* @__PURE__ */ new Set([...(_g = nextMap[newKey]) != null ? _g : [], ...targets]);
7759
8328
  nextMap[newKey] = Array.from(merged);
7760
8329
  }
7761
8330
  }
7762
8331
  p[mapKey] = nextMap;
7763
8332
  }
8333
+ copyOptionEffects(p, {
8334
+ triggerIdMap: new Map([
8335
+ [fieldId, id],
8336
+ ...Array.from(optionIdMap.entries())
8337
+ ]),
8338
+ targetFieldIdMap: /* @__PURE__ */ new Map([[fieldId, id]]),
8339
+ optionIdMap
8340
+ });
7764
8341
  }
7765
8342
  });
7766
8343
  return id;
7767
8344
  }
7768
8345
  function duplicateOption(ctx, fieldId, optionId, opts) {
7769
- var _a, _b, _c, _d, _e, _f;
7770
8346
  const props = ctx.getProps();
7771
- const fields = (_a = props.fields) != null ? _a : [];
7772
- const f = fields.find((x) => x.id === fieldId);
7773
- if (!f) throw new Error(`Field not found: ${fieldId}`);
7774
- const optIdx = ((_b = f.options) != null ? _b : []).findIndex((o) => o.id === optionId);
7775
- if (optIdx < 0) {
7776
- throw new Error(`Option not found: ${fieldId}::${optionId}`);
8347
+ const location = findMutableOption(props, optionId);
8348
+ if (!location || location.field.id !== fieldId) {
8349
+ throw new Error(`Option not found: ${fieldId}/${optionId}`);
7777
8350
  }
7778
- const src = ((_c = f.options) != null ? _c : [])[optIdx];
7779
- const newId2 = ctx.uniqueOptionId(
7780
- fieldId,
7781
- ((_d = opts.optionIdStrategy) != null ? _d : defaultOptionIdStrategy)(src.id)
7782
- );
7783
- const newLabel = ((_e = opts.labelStrategy) != null ? _e : nextCopyLabel)((_f = src.label) != null ? _f : src.id);
8351
+ const src = location.option;
8352
+ const optionIdMap = /* @__PURE__ */ new Map();
8353
+ const clone2 = cloneOptionTree(ctx, fieldId, src, opts, optionIdMap);
8354
+ const newId2 = clone2.id;
7784
8355
  ctx.patchProps((p) => {
7785
- var _a2, _b2, _c2;
7786
- const fld = ((_a2 = p.fields) != null ? _a2 : []).find((x) => x.id === fieldId);
7787
- const arr = (_b2 = fld.options) != null ? _b2 : [];
7788
- const clone2 = { ...src, id: newId2, label: newLabel };
7789
- arr.splice(optIdx + 1, 0, clone2);
7790
- fld.options = arr;
8356
+ var _a;
8357
+ const current = findMutableOption(p, optionId);
8358
+ if (!current) return;
8359
+ current.siblings.splice(current.index + 1, 0, clone2);
7791
8360
  if (opts.copyOptionMaps) {
7792
- const oldKey = `${fieldId}::${optionId}`;
7793
- const newKey = `${fieldId}::${newId2}`;
7794
8361
  for (const mapKey of [
7795
8362
  "includes_for_buttons",
7796
8363
  "excludes_for_buttons"
7797
8364
  ]) {
7798
- const m = (_c2 = p[mapKey]) != null ? _c2 : {};
7799
- if (m[oldKey]) {
7800
- m[newKey] = Array.from(new Set(m[oldKey]));
7801
- p[mapKey] = m;
8365
+ const m = (_a = p[mapKey]) != null ? _a : {};
8366
+ for (const [oldKey, newKey] of optionIdMap.entries()) {
8367
+ if (m[oldKey]) {
8368
+ m[newKey] = Array.from(new Set(m[oldKey]));
8369
+ p[mapKey] = m;
8370
+ }
7802
8371
  }
7803
8372
  }
8373
+ copyOptionEffects(p, {
8374
+ triggerIdMap: optionIdMap,
8375
+ optionIdMap
8376
+ });
7804
8377
  }
7805
8378
  });
7806
8379
  return newId2;
@@ -7862,54 +8435,6 @@ function removeNotice(ctx, id) {
7862
8435
 
7863
8436
  // src/react/canvas/editor/editor-nodes.ts
7864
8437
  var import_lodash_es3 = require("lodash-es");
7865
-
7866
- // src/react/canvas/editor/editor-utils.ts
7867
- function ownerOfOption(props, optionId) {
7868
- var _a, _b;
7869
- for (const f of (_a = props.fields) != null ? _a : []) {
7870
- const idx = ((_b = f.options) != null ? _b : []).findIndex((o) => o.id === optionId);
7871
- if (idx >= 0) return { fieldId: f.id, index: idx };
7872
- }
7873
- return null;
7874
- }
7875
- function hasFieldOptions(field) {
7876
- return Array.isArray(field == null ? void 0 : field.options) && field.options.length > 0;
7877
- }
7878
- function isActualButtonField(field) {
7879
- return (field == null ? void 0 : field.button) === true && !hasFieldOptions(field);
7880
- }
7881
- function clearFieldButtonReceiverMaps(props, fieldId) {
7882
- var _a, _b;
7883
- if ((_a = props.includes_for_buttons) == null ? void 0 : _a[fieldId]) {
7884
- delete props.includes_for_buttons[fieldId];
7885
- }
7886
- if ((_b = props.excludes_for_buttons) == null ? void 0 : _b[fieldId]) {
7887
- delete props.excludes_for_buttons[fieldId];
7888
- }
7889
- if (props.includes_for_buttons && Object.keys(props.includes_for_buttons).length === 0) {
7890
- delete props.includes_for_buttons;
7891
- }
7892
- if (props.excludes_for_buttons && Object.keys(props.excludes_for_buttons).length === 0) {
7893
- delete props.excludes_for_buttons;
7894
- }
7895
- }
7896
- function ensureServiceExists(opts, id) {
7897
- if (typeof opts.serviceExists === "function") {
7898
- if (!opts.serviceExists(id)) {
7899
- throw new Error(`service_not_found:${String(id)}`);
7900
- }
7901
- return;
7902
- }
7903
- if (opts.serviceMap) {
7904
- if (!Object.prototype.hasOwnProperty.call(opts.serviceMap, id)) {
7905
- throw new Error(`service_not_found:${String(id)}`);
7906
- }
7907
- return;
7908
- }
7909
- throw new Error("service_checker_missing");
7910
- }
7911
-
7912
- // src/react/canvas/editor/editor-nodes.ts
7913
8438
  var RELATION_MAP_KEYS = [
7914
8439
  "includes_for_buttons",
7915
8440
  "excludes_for_buttons",
@@ -7969,6 +8494,43 @@ function cleanRelationMapsForDeleted(p, deleted) {
7969
8494
  if (!Object.keys(map).length) delete p[key];
7970
8495
  }
7971
8496
  }
8497
+ function cleanOptionEffectsForDeleted(p, deleted) {
8498
+ var _a, _b;
8499
+ const map = p.option_effects_for_buttons;
8500
+ if (!map) return;
8501
+ for (const triggerId of Object.keys(map)) {
8502
+ if (deleted.has(String(triggerId))) {
8503
+ delete map[triggerId];
8504
+ continue;
8505
+ }
8506
+ const targets = map[triggerId];
8507
+ for (const targetFieldId of Object.keys(targets != null ? targets : {})) {
8508
+ if (deleted.has(String(targetFieldId))) {
8509
+ delete targets[targetFieldId];
8510
+ continue;
8511
+ }
8512
+ const effect = targets[targetFieldId];
8513
+ if (!effect) continue;
8514
+ if (effect.include) {
8515
+ effect.include = effect.include.filter(
8516
+ (optionId) => !deleted.has(String(optionId))
8517
+ );
8518
+ if (!effect.include.length) delete effect.include;
8519
+ }
8520
+ if (effect.exclude) {
8521
+ effect.exclude = effect.exclude.filter(
8522
+ (optionId) => !deleted.has(String(optionId))
8523
+ );
8524
+ if (!effect.exclude.length) delete effect.exclude;
8525
+ }
8526
+ if (effect.forceVisible !== true && !((_a = effect.include) == null ? void 0 : _a.length) && !((_b = effect.exclude) == null ? void 0 : _b.length)) {
8527
+ delete targets[targetFieldId];
8528
+ }
8529
+ }
8530
+ if (!Object.keys(targets != null ? targets : {}).length) delete map[triggerId];
8531
+ }
8532
+ if (!Object.keys(map).length) delete p.option_effects_for_buttons;
8533
+ }
7972
8534
  function cleanOrderForTagsForDeleted(p, deleted) {
7973
8535
  var _a, _b;
7974
8536
  const map = p.order_for_tags;
@@ -8004,28 +8566,37 @@ function applyDeleteCleanup(p, deleted) {
8004
8566
  cleanTagRelationsForDeleted(p, deleted);
8005
8567
  cleanFieldBindsForDeleted(p, deleted);
8006
8568
  cleanRelationMapsForDeleted(p, deleted);
8569
+ cleanOptionEffectsForDeleted(p, deleted);
8007
8570
  cleanOrderForTagsForDeleted(p, deleted);
8008
8571
  cleanNoticesForDeleted(p, deleted);
8009
8572
  }
8573
+ function collectOptionSubtreeIds(option) {
8574
+ var _a;
8575
+ return [
8576
+ String(option.id),
8577
+ ...((_a = option.children) != null ? _a : []).flatMap((child) => collectOptionSubtreeIds(child))
8578
+ ];
8579
+ }
8010
8580
  function removeOptionInPlace(p, optionId) {
8011
8581
  var _a;
8012
- const owner = ownerOfOption(p, optionId);
8013
- if (!owner) return false;
8014
- const f = ((_a = p.fields) != null ? _a : []).find((x) => x.id === owner.fieldId);
8015
- if (!(f == null ? void 0 : f.options)) return false;
8016
- const before = f.options.length;
8017
- f.options = f.options.filter((o) => o.id !== optionId);
8018
- return f.options.length !== before;
8582
+ const found = findMutableOption(p, optionId);
8583
+ if (!found) return [];
8584
+ const deleted = collectOptionSubtreeIds(found.option);
8585
+ found.siblings.splice(found.index, 1);
8586
+ if (found.parent && ((_a = found.parent.children) == null ? void 0 : _a.length) === 0) {
8587
+ delete found.parent.children;
8588
+ }
8589
+ return deleted;
8019
8590
  }
8020
8591
  function removeFieldInPlace(p, fieldId) {
8021
- var _a, _b, _c, _d, _e;
8592
+ var _a, _b, _c, _d;
8022
8593
  const field = ((_a = p.fields) != null ? _a : []).find((f) => f.id === fieldId);
8023
8594
  if (!field) return [];
8024
- const deleted = [fieldId, ...((_b = field.options) != null ? _b : []).map((o) => String(o.id))];
8025
- const before = ((_c = p.fields) != null ? _c : []).length;
8026
- p.fields = ((_d = p.fields) != null ? _d : []).filter((f) => f.id !== fieldId);
8595
+ const deleted = [fieldId, ...collectFieldOptionIds(field)];
8596
+ const before = ((_b = p.fields) != null ? _b : []).length;
8597
+ p.fields = ((_c = p.fields) != null ? _c : []).filter((f) => f.id !== fieldId);
8027
8598
  clearFieldButtonReceiverMaps(p, fieldId);
8028
- return ((_e = p.fields) != null ? _e : []).length !== before ? deleted : [];
8599
+ return ((_d = p.fields) != null ? _d : []).length !== before ? deleted : [];
8029
8600
  }
8030
8601
  function removeTagInPlace(p, tagId) {
8031
8602
  var _a, _b, _c;
@@ -8038,7 +8609,7 @@ function reLabel(ctx, id, nextLabel) {
8038
8609
  ctx.exec({
8039
8610
  name: "reLabel",
8040
8611
  do: () => ctx.patchProps((p) => {
8041
- var _a, _b, _c, _d, _e, _f, _g;
8612
+ var _a, _b, _c, _d, _e, _f;
8042
8613
  if (ctx.isTagId(id)) {
8043
8614
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
8044
8615
  if (!t) return;
@@ -8048,19 +8619,16 @@ function reLabel(ctx, id, nextLabel) {
8048
8619
  return;
8049
8620
  }
8050
8621
  if (ctx.isOptionId(id)) {
8051
- const own = ownerOfOption(p, id);
8052
- if (!own) return;
8053
- const f = ((_c = p.fields) != null ? _c : []).find((x) => x.id === own.fieldId);
8054
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
8622
+ const o = (_c = findMutableOption(p, id)) == null ? void 0 : _c.option;
8055
8623
  if (!o) return;
8056
- if (((_e = o.label) != null ? _e : "") === label) return;
8624
+ if (((_d = o.label) != null ? _d : "") === label) return;
8057
8625
  o.label = label;
8058
8626
  ctx.api.refreshGraph();
8059
8627
  return;
8060
8628
  }
8061
- const fld = ((_f = p.fields) != null ? _f : []).find((x) => x.id === id);
8629
+ const fld = ((_e = p.fields) != null ? _e : []).find((x) => x.id === id);
8062
8630
  if (!fld) return;
8063
- if (((_g = fld.label) != null ? _g : "") === label) return;
8631
+ if (((_f = fld.label) != null ? _f : "") === label) return;
8064
8632
  fld.label = label;
8065
8633
  ctx.api.refreshGraph();
8066
8634
  }),
@@ -8150,11 +8718,7 @@ function updateOption(ctx, optionId, patch) {
8150
8718
  name: "updateOption",
8151
8719
  do: () => ctx.patchProps((p) => {
8152
8720
  var _a;
8153
- const owner = ownerOfOption(p, optionId);
8154
- if (!owner) return;
8155
- const f = ((_a = p.fields) != null ? _a : []).find((x) => x.id === owner.fieldId);
8156
- if (!(f == null ? void 0 : f.options)) return;
8157
- const o = f.options.find((x) => x.id === optionId);
8721
+ const o = (_a = findMutableOption(p, optionId)) == null ? void 0 : _a.option;
8158
8722
  if (o) Object.assign(o, patch);
8159
8723
  }),
8160
8724
  undo: () => ctx.undo()
@@ -8167,9 +8731,9 @@ function removeOption(ctx, optionId) {
8167
8731
  ctx.exec({
8168
8732
  name: "removeOption",
8169
8733
  do: () => ctx.patchProps((p) => {
8170
- const removed = removeOptionInPlace(p, optionId);
8171
- if (!removed) return;
8172
- applyDeleteCleanup(p, /* @__PURE__ */ new Set([optionId]));
8734
+ const removedIds = removeOptionInPlace(p, optionId);
8735
+ if (!removedIds.length) return;
8736
+ applyDeleteCleanup(p, new Set(removedIds));
8173
8737
  }),
8174
8738
  undo: () => ctx.undo()
8175
8739
  });
@@ -8180,7 +8744,7 @@ function editLabel(ctx, id, label) {
8180
8744
  ctx.exec({
8181
8745
  name: "editLabel",
8182
8746
  do: () => ctx.patchProps((p) => {
8183
- var _a, _b, _c, _d;
8747
+ var _a, _b, _c;
8184
8748
  if (ctx.isTagId(id)) {
8185
8749
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
8186
8750
  if (t) t.label = next;
@@ -8192,10 +8756,7 @@ function editLabel(ctx, id, label) {
8192
8756
  return;
8193
8757
  }
8194
8758
  if (ctx.isOptionId(id)) {
8195
- const own = ownerOfOption(p, id);
8196
- if (!own) return;
8197
- const f = ((_c = p.fields) != null ? _c : []).find((x) => x.id === own.fieldId);
8198
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
8759
+ const o = (_c = findMutableOption(p, id)) == null ? void 0 : _c.option;
8199
8760
  if (o) o.label = next;
8200
8761
  return;
8201
8762
  }
@@ -8220,7 +8781,7 @@ function setService(ctx, id, input) {
8220
8781
  ctx.exec({
8221
8782
  name: "setService",
8222
8783
  do: () => ctx.patchProps((p) => {
8223
- var _a, _b, _c, _d, _e, _f;
8784
+ var _a, _b, _c, _d, _e;
8224
8785
  const hasSidKey = Object.prototype.hasOwnProperty.call(
8225
8786
  input,
8226
8787
  "service_id"
@@ -8238,12 +8799,9 @@ function setService(ctx, id, input) {
8238
8799
  return;
8239
8800
  }
8240
8801
  if (ctx.isOptionId(id)) {
8241
- const own = ownerOfOption(p, id);
8242
- if (!own) return;
8243
- const f2 = ((_b = p.fields) != null ? _b : []).find((x) => x.id === own.fieldId);
8244
- const o = (_c = f2 == null ? void 0 : f2.options) == null ? void 0 : _c.find((x) => x.id === id);
8802
+ const o = (_b = findMutableOption(p, id)) == null ? void 0 : _b.option;
8245
8803
  if (!o) return;
8246
- const currentRole = (_d = o.pricing_role) != null ? _d : "base";
8804
+ const currentRole = (_c = o.pricing_role) != null ? _c : "base";
8247
8805
  const role = nextRole != null ? nextRole : currentRole;
8248
8806
  if (role === "utility") {
8249
8807
  if (hasSidKey && sid !== void 0) {
@@ -8264,7 +8822,7 @@ function setService(ctx, id, input) {
8264
8822
  }
8265
8823
  return;
8266
8824
  }
8267
- const f = ((_e = p.fields) != null ? _e : []).find((x) => x.id === id);
8825
+ const f = ((_d = p.fields) != null ? _d : []).find((x) => x.id === id);
8268
8826
  if (!f) {
8269
8827
  throw new Error(
8270
8828
  'setService only supports tag ("t:*"), option ("o:*"), or field ("f:*") ids'
@@ -8275,7 +8833,7 @@ function setService(ctx, id, input) {
8275
8833
  if (nextRole) {
8276
8834
  f.pricing_role = nextRole;
8277
8835
  }
8278
- const effectiveRole = (_f = f.pricing_role) != null ? _f : "base";
8836
+ const effectiveRole = (_e = f.pricing_role) != null ? _e : "base";
8279
8837
  if (isOptionBased) {
8280
8838
  if (hasSidKey) {
8281
8839
  ctx.api.emit("error", {
@@ -8387,13 +8945,15 @@ function updateField(ctx, id, patch) {
8387
8945
  let prev;
8388
8946
  let prevIncludesForButton;
8389
8947
  let prevExcludesForButton;
8948
+ let prevOptionEffectsForButton;
8390
8949
  ctx.exec({
8391
8950
  name: "updateField",
8392
8951
  do: () => ctx.patchProps((p) => {
8393
- var _a, _b, _c, _d, _e, _f, _g;
8952
+ var _a, _b, _c, _d, _e, _f, _g, _h;
8394
8953
  prevIncludesForButton = ((_a = p.includes_for_buttons) == null ? void 0 : _a[id]) ? [...(_c = (_b = p.includes_for_buttons) == null ? void 0 : _b[id]) != null ? _c : []] : void 0;
8395
8954
  prevExcludesForButton = ((_d = p.excludes_for_buttons) == null ? void 0 : _d[id]) ? [...(_f = (_e = p.excludes_for_buttons) == null ? void 0 : _e[id]) != null ? _f : []] : void 0;
8396
- p.fields = ((_g = p.fields) != null ? _g : []).map((f) => {
8955
+ prevOptionEffectsForButton = ((_g = p.option_effects_for_buttons) == null ? void 0 : _g[id]) ? (0, import_lodash_es3.cloneDeep)(p.option_effects_for_buttons[id]) : void 0;
8956
+ p.fields = ((_h = p.fields) != null ? _h : []).map((f) => {
8397
8957
  if (f.id !== id) return f;
8398
8958
  prev = (0, import_lodash_es3.cloneDeep)(f);
8399
8959
  const nextField = { ...f, ...patch };
@@ -8404,7 +8964,7 @@ function updateField(ctx, id, patch) {
8404
8964
  });
8405
8965
  }),
8406
8966
  undo: () => ctx.patchProps((p) => {
8407
- var _a, _b, _c;
8967
+ var _a, _b, _c, _d;
8408
8968
  p.fields = ((_a = p.fields) != null ? _a : []).map(
8409
8969
  (f) => f.id === id && prev ? prev : f
8410
8970
  );
@@ -8422,6 +8982,12 @@ function updateField(ctx, id, patch) {
8422
8982
  [id]: [...prevExcludesForButton]
8423
8983
  };
8424
8984
  }
8985
+ if (prevOptionEffectsForButton) {
8986
+ p.option_effects_for_buttons = {
8987
+ ...(_d = p.option_effects_for_buttons) != null ? _d : {},
8988
+ [id]: (0, import_lodash_es3.cloneDeep)(prevOptionEffectsForButton)
8989
+ };
8990
+ }
8425
8991
  })
8426
8992
  });
8427
8993
  }
@@ -8468,9 +9034,9 @@ function remove(ctx, id) {
8468
9034
  ctx.exec({
8469
9035
  name: "removeOption",
8470
9036
  do: () => ctx.patchProps((p) => {
8471
- const removed = removeOptionInPlace(p, key);
8472
- if (!removed) return;
8473
- applyDeleteCleanup(p, /* @__PURE__ */ new Set([key]));
9037
+ const removedIds = removeOptionInPlace(p, key);
9038
+ if (!removedIds.length) return;
9039
+ applyDeleteCleanup(p, new Set(removedIds));
8474
9040
  }),
8475
9041
  undo: () => ctx.undo()
8476
9042
  });
@@ -8487,10 +9053,7 @@ function removeMany(ctx, ids) {
8487
9053
  const existingFieldIds = new Set(((_a = p.fields) != null ? _a : []).map((f) => String(f.id)));
8488
9054
  const existingTagIds = new Set(((_b = p.filters) != null ? _b : []).map((t) => String(t.id)));
8489
9055
  const existingOptionIds = new Set(
8490
- ((_c = p.fields) != null ? _c : []).flatMap((f) => {
8491
- var _a2;
8492
- return ((_a2 = f.options) != null ? _a2 : []).map((o) => String(o.id));
8493
- })
9056
+ ((_c = p.fields) != null ? _c : []).flatMap((f) => collectFieldOptionIds(f))
8494
9057
  );
8495
9058
  const fieldIds = ordered.filter((id) => ctx.isFieldId(id) && existingFieldIds.has(id));
8496
9059
  const fieldIdSet = new Set(fieldIds);
@@ -8503,7 +9066,9 @@ function removeMany(ctx, ids) {
8503
9066
  });
8504
9067
  const deleted = /* @__PURE__ */ new Set();
8505
9068
  for (const optionId of optionIds) {
8506
- if (removeOptionInPlace(p, optionId)) deleted.add(optionId);
9069
+ for (const removedId of removeOptionInPlace(p, optionId)) {
9070
+ deleted.add(removedId);
9071
+ }
8507
9072
  }
8508
9073
  for (const fieldId of fieldIds) {
8509
9074
  const removedIds = removeFieldInPlace(p, fieldId);
@@ -8518,7 +9083,7 @@ function removeMany(ctx, ids) {
8518
9083
  });
8519
9084
  }
8520
9085
  function getNode(ctx, id) {
8521
- var _a, _b, _c, _d;
9086
+ var _a, _b, _c;
8522
9087
  const props = ctx.getProps();
8523
9088
  if (ctx.isTagId(id)) {
8524
9089
  const t = ((_a = props.filters) != null ? _a : []).find((x) => x.id === id);
@@ -8535,8 +9100,7 @@ function getNode(ctx, id) {
8535
9100
  }
8536
9101
  if (ctx.isOptionId(id)) {
8537
9102
  const own = ownerOfOption(props, id);
8538
- const f = own ? ((_c = props.fields) != null ? _c : []).find((x) => x.id === own.fieldId) : void 0;
8539
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
9103
+ const o = (_c = findMutableOption(props, id)) == null ? void 0 : _c.option;
8540
9104
  return {
8541
9105
  kind: "option",
8542
9106
  data: o,
@@ -9073,7 +9637,7 @@ function connect(ctx, kind, fromId, toId2) {
9073
9637
  ctx.exec({
9074
9638
  name: `connect:${kind}`,
9075
9639
  do: () => ctx.patchProps((p) => {
9076
- var _a, _b, _c, _d, _e, _f, _g, _h;
9640
+ var _a, _b, _c, _d, _e, _f, _g;
9077
9641
  if (kind === "bind") {
9078
9642
  if (ctx.isTagId(fromId) && ctx.isTagId(toId2)) {
9079
9643
  if (wouldCreateTagCycle(ctx, p, fromId, toId2)) {
@@ -9153,12 +9717,10 @@ function connect(ctx, kind, fromId, toId2) {
9153
9717
  return;
9154
9718
  }
9155
9719
  if (toId2.startsWith("o:")) {
9156
- for (const f of (_g = p.fields) != null ? _g : []) {
9157
- const o = (_h = f.options) == null ? void 0 : _h.find((x) => x.id === toId2);
9158
- if (o) {
9159
- o.service_id = fromId;
9160
- return;
9161
- }
9720
+ const o = (_g = findMutableOption(p, toId2)) == null ? void 0 : _g.option;
9721
+ if (o) {
9722
+ o.service_id = fromId;
9723
+ return;
9162
9724
  }
9163
9725
  return;
9164
9726
  }
@@ -9175,7 +9737,7 @@ function disconnect(ctx, kind, fromId, toId2) {
9175
9737
  ctx.exec({
9176
9738
  name: `disconnect:${kind}`,
9177
9739
  do: () => ctx.patchProps((p) => {
9178
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
9740
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
9179
9741
  if (kind === "bind") {
9180
9742
  if (ctx.isTagId(fromId) && ctx.isTagId(toId2)) {
9181
9743
  const child = ((_a = p.filters) != null ? _a : []).find(
@@ -9251,12 +9813,10 @@ function disconnect(ctx, kind, fromId, toId2) {
9251
9813
  return;
9252
9814
  }
9253
9815
  if (toId2.startsWith("o:")) {
9254
- for (const f of (_i = p.fields) != null ? _i : []) {
9255
- const o = (_j = f.options) == null ? void 0 : _j.find((x) => x.id === toId2);
9256
- if (o) {
9257
- delete o.service_id;
9258
- return;
9259
- }
9816
+ const o = (_i = findMutableOption(p, toId2)) == null ? void 0 : _i.option;
9817
+ if (o) {
9818
+ delete o.service_id;
9819
+ return;
9260
9820
  }
9261
9821
  return;
9262
9822
  }
@@ -9279,6 +9839,250 @@ function addMappedField(p, mapKey, fromId, toId2) {
9279
9839
  p[mapKey] = maps;
9280
9840
  }
9281
9841
 
9842
+ // src/react/canvas/editor/editor-option-effects.ts
9843
+ function assertCanonicalId(id, label) {
9844
+ if (!id || id.includes("::") || id.includes("/")) {
9845
+ throw new Error(
9846
+ `${label}: expected a raw field or option id, not a composite/path id`
9847
+ );
9848
+ }
9849
+ }
9850
+ function assertTrigger(ctx, triggerId) {
9851
+ assertCanonicalId(triggerId, "option effect trigger");
9852
+ const trigger = ctx.getNode(triggerId);
9853
+ if (trigger.kind === "option" && trigger.data) return;
9854
+ if (trigger.kind === "field" && trigger.data && isActualButtonField(trigger.data)) {
9855
+ return;
9856
+ }
9857
+ throw new Error(
9858
+ "option effect trigger must be an option id or button field id"
9859
+ );
9860
+ }
9861
+ function assertTargetField(props, targetFieldId) {
9862
+ var _a;
9863
+ assertCanonicalId(targetFieldId, "option effect target");
9864
+ const field = ((_a = props.fields) != null ? _a : []).find((item) => item.id === targetFieldId);
9865
+ if (!field) {
9866
+ throw new Error(`option effect target field not found: ${targetFieldId}`);
9867
+ }
9868
+ return field;
9869
+ }
9870
+ function dedupe2(values) {
9871
+ if (!values) return void 0;
9872
+ const out = [];
9873
+ for (const value of values) {
9874
+ const id = String(value);
9875
+ if (!id || out.includes(id)) continue;
9876
+ out.push(id);
9877
+ }
9878
+ return out.length ? out : void 0;
9879
+ }
9880
+ function assertTargetOptions(props, targetFieldId, ids, kind) {
9881
+ if (!(ids == null ? void 0 : ids.length)) return;
9882
+ const field = assertTargetField(props, targetFieldId);
9883
+ const valid = fieldOptionIdSet(field);
9884
+ for (const id of ids) {
9885
+ assertCanonicalId(String(id), `option effect ${kind} option`);
9886
+ if (!valid.has(String(id))) {
9887
+ throw new Error(
9888
+ `option effect ${kind} option not found under ${targetFieldId}: ${String(id)}`
9889
+ );
9890
+ }
9891
+ }
9892
+ }
9893
+ function normalizeEffect(effect) {
9894
+ var _a;
9895
+ if (!effect) return void 0;
9896
+ const exclude2 = dedupe2(effect.exclude);
9897
+ const excluded = new Set(exclude2 != null ? exclude2 : []);
9898
+ const include2 = (_a = dedupe2(effect.include)) == null ? void 0 : _a.filter((id) => !excluded.has(id));
9899
+ const out = {};
9900
+ if (effect.forceVisible === true) out.forceVisible = true;
9901
+ if (include2 == null ? void 0 : include2.length) out.include = include2;
9902
+ if (exclude2 == null ? void 0 : exclude2.length) out.exclude = exclude2;
9903
+ return Object.keys(out).length ? out : void 0;
9904
+ }
9905
+ function ensureTargetMap(props, triggerId) {
9906
+ var _a, _b, _c;
9907
+ (_a = props.option_effects_for_buttons) != null ? _a : props.option_effects_for_buttons = {};
9908
+ (_c = (_b = props.option_effects_for_buttons)[triggerId]) != null ? _c : _b[triggerId] = {};
9909
+ return props.option_effects_for_buttons[triggerId];
9910
+ }
9911
+ function pruneEffectMap(props, triggerId) {
9912
+ const map = props.option_effects_for_buttons;
9913
+ if (!map) return;
9914
+ const keys = triggerId ? [triggerId] : Object.keys(map);
9915
+ for (const key of keys) {
9916
+ const targets = map[key];
9917
+ if (!targets || Object.keys(targets).length === 0) delete map[key];
9918
+ }
9919
+ if (Object.keys(map).length === 0) delete props.option_effects_for_buttons;
9920
+ }
9921
+ function validateEffect(ctx, props, triggerId, targetFieldId, effect) {
9922
+ assertTrigger(ctx, triggerId);
9923
+ assertTargetField(props, targetFieldId);
9924
+ assertTargetOptions(props, targetFieldId, effect == null ? void 0 : effect.include, "include");
9925
+ assertTargetOptions(props, targetFieldId, effect == null ? void 0 : effect.exclude, "exclude");
9926
+ return normalizeEffect(effect);
9927
+ }
9928
+ function setOptionEffect(ctx, triggerId, targetFieldId, effect) {
9929
+ ctx.exec({
9930
+ name: "setOptionEffect",
9931
+ do: () => ctx.patchProps((props) => {
9932
+ var _a;
9933
+ const normalized = validateEffect(
9934
+ ctx,
9935
+ props,
9936
+ triggerId,
9937
+ targetFieldId,
9938
+ effect
9939
+ );
9940
+ if (!normalized) {
9941
+ const map = (_a = props.option_effects_for_buttons) == null ? void 0 : _a[triggerId];
9942
+ if (map) delete map[targetFieldId];
9943
+ pruneEffectMap(props, triggerId);
9944
+ return;
9945
+ }
9946
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
9947
+ }),
9948
+ undo: () => ctx.undo()
9949
+ });
9950
+ }
9951
+ function patchOptionEffect(ctx, triggerId, targetFieldId, patch) {
9952
+ ctx.exec({
9953
+ name: "patchOptionEffect",
9954
+ do: () => ctx.patchProps((props) => {
9955
+ var _a, _b, _c, _d;
9956
+ const current = (_c = (_b = (_a = props.option_effects_for_buttons) == null ? void 0 : _a[triggerId]) == null ? void 0 : _b[targetFieldId]) != null ? _c : {};
9957
+ const merged = {
9958
+ ...current,
9959
+ ...patch
9960
+ };
9961
+ const normalized = validateEffect(
9962
+ ctx,
9963
+ props,
9964
+ triggerId,
9965
+ targetFieldId,
9966
+ merged
9967
+ );
9968
+ if (!normalized) {
9969
+ const map = (_d = props.option_effects_for_buttons) == null ? void 0 : _d[triggerId];
9970
+ if (map) delete map[targetFieldId];
9971
+ pruneEffectMap(props, triggerId);
9972
+ return;
9973
+ }
9974
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
9975
+ }),
9976
+ undo: () => ctx.undo()
9977
+ });
9978
+ }
9979
+ function clearOptionEffect(ctx, triggerId, targetFieldId) {
9980
+ ctx.exec({
9981
+ name: "clearOptionEffect",
9982
+ do: () => ctx.patchProps((props) => {
9983
+ var _a;
9984
+ const map = (_a = props.option_effects_for_buttons) == null ? void 0 : _a[triggerId];
9985
+ if (!map) return;
9986
+ delete map[targetFieldId];
9987
+ pruneEffectMap(props, triggerId);
9988
+ }),
9989
+ undo: () => ctx.undo()
9990
+ });
9991
+ }
9992
+ function clearOptionEffectsForTrigger(ctx, triggerId) {
9993
+ ctx.exec({
9994
+ name: "clearOptionEffectsForTrigger",
9995
+ do: () => ctx.patchProps((props) => {
9996
+ if (!props.option_effects_for_buttons) return;
9997
+ delete props.option_effects_for_buttons[triggerId];
9998
+ pruneEffectMap(props);
9999
+ }),
10000
+ undo: () => ctx.undo()
10001
+ });
10002
+ }
10003
+ function clearOptionEffectsForTarget(ctx, targetFieldId) {
10004
+ ctx.exec({
10005
+ name: "clearOptionEffectsForTarget",
10006
+ do: () => ctx.patchProps((props) => {
10007
+ var _a;
10008
+ const map = props.option_effects_for_buttons;
10009
+ if (!map) return;
10010
+ for (const triggerId of Object.keys(map)) {
10011
+ (_a = map[triggerId]) == null ? true : delete _a[targetFieldId];
10012
+ }
10013
+ pruneEffectMap(props);
10014
+ }),
10015
+ undo: () => ctx.undo()
10016
+ });
10017
+ }
10018
+ function addOptionEffectOptions(ctx, triggerId, targetFieldId, kind, optionIds) {
10019
+ var _a;
10020
+ const additions = (_a = dedupe2(optionIds)) != null ? _a : [];
10021
+ if (!additions.length) return;
10022
+ ctx.exec({
10023
+ name: "addOptionEffectOptions",
10024
+ do: () => ctx.patchProps((props) => {
10025
+ var _a2, _b, _c, _d;
10026
+ const current = (_c = (_b = (_a2 = props.option_effects_for_buttons) == null ? void 0 : _a2[triggerId]) == null ? void 0 : _b[targetFieldId]) != null ? _c : {};
10027
+ const nextValues = dedupe2([
10028
+ ...(_d = current[kind]) != null ? _d : [],
10029
+ ...additions
10030
+ ]);
10031
+ const normalized = validateEffect(
10032
+ ctx,
10033
+ props,
10034
+ triggerId,
10035
+ targetFieldId,
10036
+ {
10037
+ ...current,
10038
+ [kind]: nextValues
10039
+ }
10040
+ );
10041
+ if (!normalized) return;
10042
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
10043
+ }),
10044
+ undo: () => ctx.undo()
10045
+ });
10046
+ }
10047
+ function removeOptionEffectOptions(ctx, triggerId, targetFieldId, kind, optionIds) {
10048
+ var _a;
10049
+ const removals = new Set((_a = dedupe2(optionIds)) != null ? _a : []);
10050
+ if (!removals.size) return;
10051
+ ctx.exec({
10052
+ name: "removeOptionEffectOptions",
10053
+ do: () => ctx.patchProps((props) => {
10054
+ var _a2, _b, _c, _d, _e;
10055
+ const current = (_b = (_a2 = props.option_effects_for_buttons) == null ? void 0 : _a2[triggerId]) == null ? void 0 : _b[targetFieldId];
10056
+ if (!current) return;
10057
+ const next = {
10058
+ ...current,
10059
+ [kind]: ((_c = current[kind]) != null ? _c : []).filter(
10060
+ (optionId) => !removals.has(optionId)
10061
+ )
10062
+ };
10063
+ const normalized = validateEffect(
10064
+ ctx,
10065
+ props,
10066
+ triggerId,
10067
+ targetFieldId,
10068
+ next
10069
+ );
10070
+ if (!normalized) {
10071
+ (_e = (_d = props.option_effects_for_buttons) == null ? void 0 : _d[triggerId]) == null ? true : delete _e[targetFieldId];
10072
+ pruneEffectMap(props, triggerId);
10073
+ return;
10074
+ }
10075
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
10076
+ }),
10077
+ undo: () => ctx.undo()
10078
+ });
10079
+ }
10080
+ function setOptionEffectForceVisible(ctx, triggerId, targetFieldId, forceVisible) {
10081
+ patchOptionEffect(ctx, triggerId, targetFieldId, {
10082
+ forceVisible: forceVisible === true ? true : void 0
10083
+ });
10084
+ }
10085
+
9282
10086
  // src/react/canvas/editor/editor-service-filter.ts
9283
10087
  function filterServicesForVisibleGroup2(ctx, candidates, input) {
9284
10088
  const coreInput = {
@@ -9865,7 +10669,7 @@ var Editor = class {
9865
10669
  if (!ordered.length) return;
9866
10670
  this.transact("clearServiceMany", () => {
9867
10671
  this.patchProps((p) => {
9868
- var _a, _b, _c, _d;
10672
+ var _a, _b;
9869
10673
  for (const id of ordered) {
9870
10674
  if (this.isTagId(id)) {
9871
10675
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
@@ -9878,10 +10682,8 @@ var Editor = class {
9878
10682
  continue;
9879
10683
  }
9880
10684
  if (this.isOptionId(id)) {
9881
- const own = ownerOfOption(p, id);
9882
- if (!own) continue;
9883
- const f = ((_c = p.fields) != null ? _c : []).find((x) => x.id === own.fieldId);
9884
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
10685
+ const found = findMutableOption(p, id);
10686
+ const o = found == null ? void 0 : found.option;
9885
10687
  if (o && "service_id" in o) delete o.service_id;
9886
10688
  }
9887
10689
  }
@@ -9930,7 +10732,7 @@ var Editor = class {
9930
10732
  if (!selected.size) return;
9931
10733
  this.transact("clearRelationsMany", () => {
9932
10734
  this.patchProps((p) => {
9933
- var _a, _b, _c;
10735
+ var _a, _b, _c, _d, _e;
9934
10736
  const clearOwned = mode === "owned" || mode === "both";
9935
10737
  const clearIncoming = mode === "incoming" || mode === "both";
9936
10738
  for (const t of (_a = p.filters) != null ? _a : []) {
@@ -9970,6 +10772,44 @@ var Editor = class {
9970
10772
  }
9971
10773
  if (!Object.keys(map).length) delete p[k];
9972
10774
  }
10775
+ const effectMap = p.option_effects_for_buttons;
10776
+ if (effectMap) {
10777
+ for (const triggerId of Object.keys(effectMap)) {
10778
+ if (clearOwned && selected.has(String(triggerId))) {
10779
+ delete effectMap[triggerId];
10780
+ continue;
10781
+ }
10782
+ const targets = effectMap[triggerId];
10783
+ if (!targets || !clearIncoming) continue;
10784
+ for (const targetFieldId of Object.keys(targets)) {
10785
+ if (selected.has(String(targetFieldId))) {
10786
+ delete targets[targetFieldId];
10787
+ continue;
10788
+ }
10789
+ const effect = targets[targetFieldId];
10790
+ if (!effect) continue;
10791
+ if (effect.include) {
10792
+ effect.include = effect.include.filter(
10793
+ (optionId) => !selected.has(String(optionId))
10794
+ );
10795
+ if (!effect.include.length) delete effect.include;
10796
+ }
10797
+ if (effect.exclude) {
10798
+ effect.exclude = effect.exclude.filter(
10799
+ (optionId) => !selected.has(String(optionId))
10800
+ );
10801
+ if (!effect.exclude.length) delete effect.exclude;
10802
+ }
10803
+ if (effect.forceVisible !== true && !((_d = effect.include) == null ? void 0 : _d.length) && !((_e = effect.exclude) == null ? void 0 : _e.length)) {
10804
+ delete targets[targetFieldId];
10805
+ }
10806
+ }
10807
+ if (!Object.keys(targets).length) delete effectMap[triggerId];
10808
+ }
10809
+ if (!Object.keys(effectMap).length) {
10810
+ delete p.option_effects_for_buttons;
10811
+ }
10812
+ }
9973
10813
  });
9974
10814
  });
9975
10815
  }
@@ -9981,7 +10821,7 @@ var Editor = class {
9981
10821
  const suffix = (_b = input.suffix) != null ? _b : "";
9982
10822
  this.transact("renameLabelsMany", () => {
9983
10823
  this.patchProps((p) => {
9984
- var _a2, _b2, _c, _d, _e, _f, _g;
10824
+ var _a2, _b2, _c, _d, _e, _f;
9985
10825
  for (const id of ordered) {
9986
10826
  if (this.isTagId(id)) {
9987
10827
  const t = ((_a2 = p.filters) != null ? _a2 : []).find((x) => x.id === id);
@@ -9994,11 +10834,8 @@ var Editor = class {
9994
10834
  continue;
9995
10835
  }
9996
10836
  if (this.isOptionId(id)) {
9997
- const own = ownerOfOption(p, id);
9998
- if (!own) continue;
9999
- const f = ((_e = p.fields) != null ? _e : []).find((x) => x.id === own.fieldId);
10000
- const o = (_f = f == null ? void 0 : f.options) == null ? void 0 : _f.find((x) => x.id === id);
10001
- if (o) o.label = `${prefix}${(_g = o.label) != null ? _g : ""}${suffix}`.trim();
10837
+ const o = (_e = findMutableOption(p, id)) == null ? void 0 : _e.option;
10838
+ if (o) o.label = `${prefix}${(_f = o.label) != null ? _f : ""}${suffix}`.trim();
10002
10839
  }
10003
10840
  }
10004
10841
  });
@@ -10151,6 +10988,57 @@ var Editor = class {
10151
10988
  exclude(receiverId, idOrIds) {
10152
10989
  return exclude(this.moduleCtx(), receiverId, idOrIds);
10153
10990
  }
10991
+ setOptionEffect(triggerId, targetFieldId, effect) {
10992
+ return setOptionEffect(
10993
+ this.moduleCtx(),
10994
+ triggerId,
10995
+ targetFieldId,
10996
+ effect
10997
+ );
10998
+ }
10999
+ patchOptionEffect(triggerId, targetFieldId, patch) {
11000
+ return patchOptionEffect(
11001
+ this.moduleCtx(),
11002
+ triggerId,
11003
+ targetFieldId,
11004
+ patch
11005
+ );
11006
+ }
11007
+ clearOptionEffect(triggerId, targetFieldId) {
11008
+ return clearOptionEffect(this.moduleCtx(), triggerId, targetFieldId);
11009
+ }
11010
+ clearOptionEffectsForTrigger(triggerId) {
11011
+ return clearOptionEffectsForTrigger(this.moduleCtx(), triggerId);
11012
+ }
11013
+ clearOptionEffectsForTarget(targetFieldId) {
11014
+ return clearOptionEffectsForTarget(this.moduleCtx(), targetFieldId);
11015
+ }
11016
+ addOptionEffectOptions(triggerId, targetFieldId, kind, optionIds) {
11017
+ return addOptionEffectOptions(
11018
+ this.moduleCtx(),
11019
+ triggerId,
11020
+ targetFieldId,
11021
+ kind,
11022
+ optionIds
11023
+ );
11024
+ }
11025
+ removeOptionEffectOptions(triggerId, targetFieldId, kind, optionIds) {
11026
+ return removeOptionEffectOptions(
11027
+ this.moduleCtx(),
11028
+ triggerId,
11029
+ targetFieldId,
11030
+ kind,
11031
+ optionIds
11032
+ );
11033
+ }
11034
+ setOptionEffectForceVisible(triggerId, targetFieldId, forceVisible) {
11035
+ return setOptionEffectForceVisible(
11036
+ this.moduleCtx(),
11037
+ triggerId,
11038
+ targetFieldId,
11039
+ forceVisible
11040
+ );
11041
+ }
10154
11042
  connect(kind, fromId, toId2) {
10155
11043
  return connect(this.moduleCtx(), kind, fromId, toId2);
10156
11044
  }
@@ -10519,11 +11407,10 @@ var Selection = class {
10519
11407
  * What counts as a "button selection" (trigger key):
10520
11408
  * - field key where the field has button === true (e.g. "f:dripfeed")
10521
11409
  * - option key (e.g. "o:fast")
10522
- * - composite key "fieldId::optionId" (e.g. "f:speed::o:fast")
10523
11410
  *
10524
11411
  * Grouping:
10525
11412
  * - button-field trigger groups under its own fieldId
10526
- * - option/composite groups under the option's owning fieldId (from nodeMap)
11413
+ * - option trigger groups under the option's owning fieldId (from nodeMap)
10527
11414
  *
10528
11415
  * Deterministic:
10529
11416
  * - preserves selection insertion order
@@ -10540,15 +11427,6 @@ var Selection = class {
10540
11427
  };
10541
11428
  for (const key of this.set) {
10542
11429
  if (!key) continue;
10543
- const idx = key.indexOf("::");
10544
- if (idx !== -1) {
10545
- const optionId = key.slice(idx + 2);
10546
- const optRef = nodeMap.get(optionId);
10547
- if ((optRef == null ? void 0 : optRef.kind) === "option" && typeof optRef.fieldId === "string") {
10548
- push(optRef.fieldId, key);
10549
- }
10550
- continue;
10551
- }
10552
11430
  const ref = nodeMap.get(key);
10553
11431
  if (!ref) continue;
10554
11432
  if (ref.kind === "option" && typeof ref.fieldId === "string") {
@@ -10569,7 +11447,6 @@ var Selection = class {
10569
11447
  * Returns only selection keys that are valid "trigger buttons":
10570
11448
  * - field keys where field.button === true
10571
11449
  * - option keys
10572
- * - composite keys "fieldId::optionId" (validated by optionId)
10573
11450
  * Excludes tags and non-button fields.
10574
11451
  */
10575
11452
  selectedButtons() {
@@ -10585,13 +11462,6 @@ var Selection = class {
10585
11462
  };
10586
11463
  for (const key of this.set) {
10587
11464
  if (!key) continue;
10588
- const idx = key.indexOf("::");
10589
- if (idx !== -1) {
10590
- const optionId = key.slice(idx + 2);
10591
- const optRef = nodeMap.get(optionId);
10592
- if ((optRef == null ? void 0 : optRef.kind) === "option") push(key);
10593
- continue;
10594
- }
10595
11465
  const ref = nodeMap.get(key);
10596
11466
  if (!ref) continue;
10597
11467
  if (ref.kind === "option") {
@@ -10630,17 +11500,7 @@ var Selection = class {
10630
11500
  const direct = fields.find((x) => x.id === id);
10631
11501
  if (direct) return direct;
10632
11502
  if (this.builder.isOptionId(id)) {
10633
- return fields.find(
10634
- (x) => {
10635
- var _a2;
10636
- return ((_a2 = x.options) != null ? _a2 : []).some((o) => o.id === id);
10637
- }
10638
- );
10639
- }
10640
- if (id.includes("::")) {
10641
- const [fieldId] = id.split("::");
10642
- if (!fieldId) return void 0;
10643
- return fields.find((x) => x.id === fieldId);
11503
+ return findOptionOwnerField(fields, id);
10644
11504
  }
10645
11505
  return void 0;
10646
11506
  };
@@ -10671,18 +11531,7 @@ var Selection = class {
10671
11531
  }
10672
11532
  for (const id of this.set) {
10673
11533
  if (this.builder.isOptionId(id)) {
10674
- const host = fields.find(
10675
- (x) => {
10676
- var _a2;
10677
- return ((_a2 = x.options) != null ? _a2 : []).some((o) => o.id === id);
10678
- }
10679
- );
10680
- if (host == null ? void 0 : host.bind_id)
10681
- return Array.isArray(host.bind_id) ? host.bind_id[0] : host.bind_id;
10682
- }
10683
- if (id.includes("::")) {
10684
- const [fid] = id.split("::");
10685
- const host = fields.find((x) => x.id === fid);
11534
+ const host = findOptionOwnerField(fields, id);
10686
11535
  if (host == null ? void 0 : host.bind_id)
10687
11536
  return Array.isArray(host.bind_id) ? host.bind_id[0] : host.bind_id;
10688
11537
  }
@@ -10696,7 +11545,11 @@ var Selection = class {
10696
11545
  const tagById = new Map(tags.map((t) => [t.id, t]));
10697
11546
  const tag = tagById.get(tagId);
10698
11547
  const selectedTriggerIds = this.selectedButtons();
10699
- const fieldIds = this.builder.visibleFields(tagId, selectedTriggerIds);
11548
+ const visibility = this.builder.resolveVisibility(
11549
+ tagId,
11550
+ selectedTriggerIds
11551
+ );
11552
+ const fieldIds = visibility.fieldIds;
10700
11553
  const fieldById = new Map(fields.map((f) => [f.id, f]));
10701
11554
  const visible = fieldIds.map((id) => fieldById.get(id)).filter(Boolean);
10702
11555
  const parentTags = [];
@@ -10722,6 +11575,14 @@ var Selection = class {
10722
11575
  let baseOverridden = false;
10723
11576
  for (const selId of this.set) {
10724
11577
  const opt = this.findOptionById(fields, selId);
11578
+ if (opt && !this.isSelectedOptionVisible(
11579
+ fields,
11580
+ selId,
11581
+ fieldIds,
11582
+ visibility.optionsByFieldId
11583
+ )) {
11584
+ continue;
11585
+ }
10725
11586
  if ((opt == null ? void 0 : opt.service_id) != null) {
10726
11587
  const role = (_d = opt.pricing_role) != null ? _d : "base";
10727
11588
  const cap = (_e = resolve == null ? void 0 : resolve(opt.service_id)) != null ? _e : { id: opt.service_id };
@@ -10752,6 +11613,8 @@ var Selection = class {
10752
11613
  tag,
10753
11614
  fields: visible,
10754
11615
  fieldIds,
11616
+ optionsByFieldId: visibility.optionsByFieldId,
11617
+ forcedFieldIds: visibility.forcedFieldIds,
10755
11618
  parentTags,
10756
11619
  childrenTags,
10757
11620
  services
@@ -10775,21 +11638,19 @@ var Selection = class {
10775
11638
  return baseOverridden;
10776
11639
  }
10777
11640
  findOptionById(fields, selId) {
10778
- var _a, _b;
10779
11641
  if (this.builder.isOptionId(selId)) {
10780
- for (const f of fields) {
10781
- const o = (_a = f.options) == null ? void 0 : _a.find((x) => x.id === selId);
10782
- if (o) return o;
10783
- }
10784
- }
10785
- if (selId.includes("::")) {
10786
- const [fid, oid] = selId.split("::");
10787
- const f = fields.find((x) => x.id === fid);
10788
- const o = (_b = f == null ? void 0 : f.options) == null ? void 0 : _b.find((x) => x.id === oid || x.id === selId);
10789
- if (o) return o;
11642
+ const field = findOptionOwnerField(fields, selId);
11643
+ return findFieldOption(field, selId);
10790
11644
  }
10791
11645
  return void 0;
10792
11646
  }
11647
+ isSelectedOptionVisible(fields, selId, visibleFieldIds, optionsByFieldId) {
11648
+ const visibleFields = new Set(visibleFieldIds);
11649
+ const field = findOptionOwnerField(fields, selId);
11650
+ if (!field || !visibleFields.has(field.id)) return false;
11651
+ const allowed = optionsByFieldId[field.id];
11652
+ return !allowed || allowed.includes(selId);
11653
+ }
10793
11654
  };
10794
11655
 
10795
11656
  // src/react/canvas/api.ts