@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.
@@ -332,6 +332,7 @@ function templateTime(template) {
332
332
  return (_a = parseTimestamp(template.updatedAt)) != null ? _a : parseTimestamp(template.createdAt);
333
333
  }
334
334
  function shouldReplaceTemplates(params) {
335
+ var _a;
335
336
  if (!params.requestedSince) return true;
336
337
  if (!params.lastUpdatedAt) return false;
337
338
  const requested = parseTimestamp(params.requestedSince);
@@ -339,7 +340,9 @@ function shouldReplaceTemplates(params) {
339
340
  if (requested === void 0 || last === void 0) {
340
341
  return false;
341
342
  }
342
- return requested < last;
343
+ const currentTimes = ((_a = params.current) != null ? _a : []).map((template) => templateTime(template)).filter((time) => time !== void 0);
344
+ if (!currentTimes.length) return requested < last;
345
+ return requested < Math.min(...currentTimes);
343
346
  }
344
347
  function pickNewestTemplate(current, incoming) {
345
348
  const currentTime = templateTime(current);
@@ -347,7 +350,8 @@ function pickNewestTemplate(current, incoming) {
347
350
  if (currentTime !== void 0 && incomingTime !== void 0) {
348
351
  return incomingTime >= currentTime ? incoming : current;
349
352
  }
350
- if (currentTime === void 0 && incomingTime !== void 0) return incoming;
353
+ if (currentTime === void 0 && incomingTime !== void 0)
354
+ return incoming;
351
355
  if (currentTime !== void 0 && incomingTime === void 0) return current;
352
356
  return incoming;
353
357
  }
@@ -420,7 +424,8 @@ function useTemplatesSlice(params) {
420
424
  setTemplates((current) => {
421
425
  const replace = shouldReplaceTemplates({
422
426
  requestedSince,
423
- lastUpdatedAt: current.updatedAt
427
+ lastUpdatedAt: current.updatedAt,
428
+ current: current.data
424
429
  });
425
430
  return {
426
431
  data: replace ? res.value : mergeTemplates(current.data, res.value, {
@@ -519,7 +524,9 @@ function useTemplatesSlice(params) {
519
524
  var _a, _b;
520
525
  return {
521
526
  ...current,
522
- data: (_b = (_a = current.data) == null ? void 0 : _a.filter((template) => template.id !== id)) != null ? _b : current.data,
527
+ data: (_b = (_a = current.data) == null ? void 0 : _a.filter(
528
+ (template) => template.id !== id
529
+ )) != null ? _b : current.data,
523
530
  updatedAt: deleteRefreshSince
524
531
  };
525
532
  });
@@ -3590,6 +3597,9 @@ function normalise(input, opts = {}) {
3590
3597
  const excludes_for_buttons = toStringArrayMap(
3591
3598
  obj.excludes_for_buttons
3592
3599
  );
3600
+ const option_effects_for_buttons = toOptionEffectMap(
3601
+ obj.option_effects_for_buttons
3602
+ );
3593
3603
  const orderKinds = toStringMap(obj.orderKinds);
3594
3604
  const notices = toNoticeArray(obj.notices);
3595
3605
  let filters = rawFilters.map((t) => coerceTag(t, constraints));
@@ -3605,6 +3615,9 @@ function normalise(input, opts = {}) {
3605
3615
  ...isNonEmpty(orderKinds) && { orderKinds },
3606
3616
  ...isNonEmpty(includes_for_buttons) && { includes_for_buttons },
3607
3617
  ...isNonEmpty(excludes_for_buttons) && { excludes_for_buttons },
3618
+ ...isNonEmpty(option_effects_for_buttons) && {
3619
+ option_effects_for_buttons
3620
+ },
3608
3621
  ...fallbacks && (isNonEmpty(fallbacks.nodes) || isNonEmpty(fallbacks.global)) && {
3609
3622
  fallbacks
3610
3623
  },
@@ -3759,6 +3772,7 @@ function coerceOption(src, inheritRole) {
3759
3772
  const value = typeof src.value === "string" || typeof src.value === "number" ? src.value : void 0;
3760
3773
  const pricing_role = src.pricing_role === "utility" || src.pricing_role === "base" ? src.pricing_role : inheritRole;
3761
3774
  const meta = src.meta && typeof src.meta === "object" ? src.meta : void 0;
3775
+ const children = Array.isArray(src.children) ? src.children.map((child) => coerceOption(child, pricing_role)) : void 0;
3762
3776
  const option = {
3763
3777
  id: "",
3764
3778
  label: "",
@@ -3767,7 +3781,8 @@ function coerceOption(src, inheritRole) {
3767
3781
  ...value !== void 0 && { value },
3768
3782
  ...service_id !== void 0 && { service_id },
3769
3783
  pricing_role,
3770
- ...meta && { meta }
3784
+ ...meta && { meta },
3785
+ ...children && children.length && { children }
3771
3786
  };
3772
3787
  return option;
3773
3788
  }
@@ -3832,6 +3847,35 @@ function toStringArrayMap(src) {
3832
3847
  }
3833
3848
  return Object.keys(out).length ? out : void 0;
3834
3849
  }
3850
+ function toOptionEffectMap(src) {
3851
+ var _a, _b;
3852
+ if (!src || typeof src !== "object") return void 0;
3853
+ const out = {};
3854
+ for (const [triggerId, rawTargets] of Object.entries(src)) {
3855
+ if (!triggerId || !rawTargets || typeof rawTargets !== "object") {
3856
+ continue;
3857
+ }
3858
+ const targets = {};
3859
+ for (const [fieldId, rawEffect] of Object.entries(rawTargets)) {
3860
+ if (!fieldId || !rawEffect || typeof rawEffect !== "object") {
3861
+ continue;
3862
+ }
3863
+ const effect = rawEffect;
3864
+ const include2 = toStringArray(effect.include);
3865
+ const exclude2 = toStringArray(effect.exclude);
3866
+ const next = {
3867
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
3868
+ ...include2.length ? { include: dedupe(include2) } : {},
3869
+ ...exclude2.length ? { exclude: dedupe(exclude2) } : {}
3870
+ };
3871
+ if (next.forceVisible === true || ((_a = next.include) == null ? void 0 : _a.length) || ((_b = next.exclude) == null ? void 0 : _b.length)) {
3872
+ targets[fieldId] = next;
3873
+ }
3874
+ }
3875
+ if (Object.keys(targets).length) out[triggerId] = targets;
3876
+ }
3877
+ return Object.keys(out).length ? out : void 0;
3878
+ }
3835
3879
  function toStringArray(v) {
3836
3880
  if (!Array.isArray(v)) return [];
3837
3881
  return v.map((x) => String(x)).filter((s) => !!s && s.trim().length > 0);
@@ -3906,6 +3950,57 @@ function normalizeFieldValidation(input) {
3906
3950
  return one ? [one] : void 0;
3907
3951
  }
3908
3952
 
3953
+ // src/core/options.ts
3954
+ function walkFieldOptions(field) {
3955
+ const out = [];
3956
+ const visit = (options, depth, parentId) => {
3957
+ for (const option of options != null ? options : []) {
3958
+ out.push({
3959
+ field,
3960
+ fieldId: field.id,
3961
+ option,
3962
+ optionId: option.id,
3963
+ depth,
3964
+ parentId
3965
+ });
3966
+ visit(option.children, depth + 1, option.id);
3967
+ }
3968
+ };
3969
+ visit(field.options, 0);
3970
+ return out;
3971
+ }
3972
+ function fieldOptionIds(field) {
3973
+ return walkFieldOptions(field).map((visit) => visit.optionId);
3974
+ }
3975
+ function fieldOptionIdSet(field) {
3976
+ return new Set(fieldOptionIds(field));
3977
+ }
3978
+ function findFieldOption(field, optionId) {
3979
+ var _a;
3980
+ if (!field) return void 0;
3981
+ return (_a = walkFieldOptions(field).find((visit) => visit.optionId === optionId)) == null ? void 0 : _a.option;
3982
+ }
3983
+ function findOptionOwnerField(fields, optionId) {
3984
+ for (const field of fields) {
3985
+ if (findFieldOption(field, optionId)) return field;
3986
+ }
3987
+ return void 0;
3988
+ }
3989
+ function optionOwnerMap(fields) {
3990
+ const out = /* @__PURE__ */ new Map();
3991
+ for (const field of fields) {
3992
+ for (const visit of walkFieldOptions(field)) {
3993
+ if (!out.has(visit.optionId)) {
3994
+ out.set(visit.optionId, {
3995
+ fieldId: field.id,
3996
+ option: visit.option
3997
+ });
3998
+ }
3999
+ }
4000
+ }
4001
+ return out;
4002
+ }
4003
+
3909
4004
  // src/core/validate/shared.ts
3910
4005
  function isFiniteNumber(v) {
3911
4006
  return typeof v === "number" && Number.isFinite(v);
@@ -3914,8 +4009,9 @@ function isServiceIdRef(v) {
3914
4009
  return typeof v === "string" && v.trim().length > 0 || typeof v === "number" && Number.isFinite(v);
3915
4010
  }
3916
4011
  function hasAnyServiceOption(f) {
3917
- var _a;
3918
- return ((_a = f.options) != null ? _a : []).some((o) => isServiceIdRef(o.service_id));
4012
+ return walkFieldOptions(f).some(
4013
+ (visit) => isServiceIdRef(visit.option.service_id)
4014
+ );
3919
4015
  }
3920
4016
  function getByPath(obj, path) {
3921
4017
  if (!path) return void 0;
@@ -4004,14 +4100,14 @@ function withAffected(details, ids) {
4004
4100
 
4005
4101
  // src/core/node-map.ts
4006
4102
  function buildNodeMap(props) {
4007
- var _a, _b, _c;
4103
+ var _a, _b;
4008
4104
  const map = /* @__PURE__ */ new Map();
4009
4105
  for (const t of (_a = props.filters) != null ? _a : []) {
4010
4106
  if (!map.has(t.id)) map.set(t.id, { kind: "tag", id: t.id, node: t });
4011
4107
  }
4012
4108
  for (const f of (_b = props.fields) != null ? _b : []) {
4013
4109
  if (!map.has(f.id)) map.set(f.id, { kind: "field", id: f.id, node: f });
4014
- for (const o of (_c = f.options) != null ? _c : []) {
4110
+ for (const { option: o } of walkFieldOptions(f)) {
4015
4111
  if (!map.has(o.id))
4016
4112
  map.set(o.id, {
4017
4113
  kind: "option",
@@ -4024,12 +4120,6 @@ function buildNodeMap(props) {
4024
4120
  return map;
4025
4121
  }
4026
4122
  function resolveTrigger(trigger, nodeMap) {
4027
- const idx = trigger.indexOf("::");
4028
- if (idx !== -1) {
4029
- const fieldId = trigger.slice(0, idx);
4030
- const optionId = trigger.slice(idx + 2);
4031
- return { kind: "composite", triggerKey: trigger, fieldId, optionId };
4032
- }
4033
4123
  const direct = nodeMap.get(trigger);
4034
4124
  if (!direct) return void 0;
4035
4125
  if (direct.kind === "option") {
@@ -4081,11 +4171,6 @@ function visibleFieldIdsUnder(props, tagId, opts = {}) {
4081
4171
  const ownerDepthForTriggerKey = (triggerKey) => {
4082
4172
  const t = resolveTrigger(triggerKey, nodeMap);
4083
4173
  if (!t) return void 0;
4084
- if (t.kind === "composite") {
4085
- const f = fieldById.get(t.fieldId);
4086
- if (!f) return void 0;
4087
- return ownerDepthForField(f);
4088
- }
4089
4174
  if (t.kind === "field") {
4090
4175
  const f = fieldById.get(t.id);
4091
4176
  if (!f || f.button !== true) return void 0;
@@ -4168,6 +4253,84 @@ function visibleFieldsUnder(props, tagId, opts = {}) {
4168
4253
  const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((f) => [f.id, f]));
4169
4254
  return ids.map((id) => fieldById.get(id)).filter(Boolean);
4170
4255
  }
4256
+ function resolveVisibility(props, tagId, selectedKeys) {
4257
+ var _a, _b, _c, _d;
4258
+ const selected = new Set(selectedKeys != null ? selectedKeys : []);
4259
+ const baseFieldIds = visibleFieldIdsUnder(props, tagId, { selectedKeys: selected });
4260
+ const fieldById = new Map(((_a = props.fields) != null ? _a : []).map((field) => [field.id, field]));
4261
+ const visible = new Set(baseFieldIds);
4262
+ const forced = /* @__PURE__ */ new Set();
4263
+ const optionsByFieldId = {};
4264
+ const optionIdsByFieldId = /* @__PURE__ */ new Map();
4265
+ const getOptionIds = (field) => {
4266
+ let ids = optionIdsByFieldId.get(field.id);
4267
+ if (!ids) {
4268
+ ids = fieldOptionIds(field);
4269
+ optionIdsByFieldId.set(field.id, ids);
4270
+ }
4271
+ return ids;
4272
+ };
4273
+ const ensureOptions = (field) => {
4274
+ const ids = getOptionIds(field);
4275
+ if (!ids.length) return void 0;
4276
+ if (!optionsByFieldId[field.id]) optionsByFieldId[field.id] = [...ids];
4277
+ return optionsByFieldId[field.id];
4278
+ };
4279
+ for (const fieldId of baseFieldIds) {
4280
+ const field = fieldById.get(fieldId);
4281
+ if (field) ensureOptions(field);
4282
+ }
4283
+ const effects = (_b = props.option_effects_for_buttons) != null ? _b : {};
4284
+ for (const triggerId of selected) {
4285
+ const targetRules = effects[triggerId];
4286
+ if (!targetRules) continue;
4287
+ for (const [targetFieldId, rule] of Object.entries(targetRules)) {
4288
+ const field = fieldById.get(targetFieldId);
4289
+ if (!field) continue;
4290
+ const isVisible = visible.has(targetFieldId);
4291
+ if (!isVisible && rule.forceVisible !== true) continue;
4292
+ if (!isVisible && rule.forceVisible === true) {
4293
+ visible.add(targetFieldId);
4294
+ forced.add(targetFieldId);
4295
+ }
4296
+ const orderedOptionIds = getOptionIds(field);
4297
+ if (!orderedOptionIds.length) continue;
4298
+ const known = new Set(orderedOptionIds);
4299
+ let allowed = (_c = optionsByFieldId[targetFieldId]) != null ? _c : [...orderedOptionIds];
4300
+ if (Array.isArray(rule.include) && rule.include.length) {
4301
+ const include2 = new Set(
4302
+ rule.include.filter((optionId) => known.has(optionId))
4303
+ );
4304
+ allowed = orderedOptionIds.filter(
4305
+ (optionId) => include2.has(optionId) && allowed.includes(optionId)
4306
+ );
4307
+ }
4308
+ if (Array.isArray(rule.exclude) && rule.exclude.length) {
4309
+ const exclude2 = new Set(
4310
+ rule.exclude.filter((optionId) => known.has(optionId))
4311
+ );
4312
+ allowed = allowed.filter((optionId) => !exclude2.has(optionId));
4313
+ }
4314
+ optionsByFieldId[targetFieldId] = allowed;
4315
+ }
4316
+ }
4317
+ const visibleFieldIds = baseFieldIds.filter((fieldId) => visible.has(fieldId));
4318
+ const seen = new Set(visibleFieldIds);
4319
+ for (const field of (_d = props.fields) != null ? _d : []) {
4320
+ if (!visible.has(field.id) || seen.has(field.id)) continue;
4321
+ seen.add(field.id);
4322
+ visibleFieldIds.push(field.id);
4323
+ ensureOptions(field);
4324
+ }
4325
+ for (const fieldId of Object.keys(optionsByFieldId)) {
4326
+ if (!visible.has(fieldId)) delete optionsByFieldId[fieldId];
4327
+ }
4328
+ return {
4329
+ fieldIds: visibleFieldIds,
4330
+ optionsByFieldId,
4331
+ forcedFieldIds: visibleFieldIds.filter((fieldId) => forced.has(fieldId))
4332
+ };
4333
+ }
4171
4334
 
4172
4335
  // src/core/validate/steps/visibility.ts
4173
4336
  function createFieldsVisibleUnder(v) {
@@ -4185,7 +4348,6 @@ function resolveRootTags(tags) {
4185
4348
  return roots.length ? roots : tags.slice(0, 1);
4186
4349
  }
4187
4350
  function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKeys) {
4188
- var _a;
4189
4351
  const visible = visibleFieldsUnder(v.props, tagId, {
4190
4352
  selectedKeys
4191
4353
  });
@@ -4195,7 +4357,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
4195
4357
  const t = f.id;
4196
4358
  if (effectfulKeys.has(t)) triggers.push(t);
4197
4359
  }
4198
- for (const o of (_a = f.options) != null ? _a : []) {
4360
+ for (const { option: o } of walkFieldOptions(f)) {
4199
4361
  const t = o.id;
4200
4362
  if (effectfulKeys.has(t)) triggers.push(t);
4201
4363
  }
@@ -4204,7 +4366,7 @@ function collectSelectableTriggersInContext(v, tagId, selectedKeys, effectfulKey
4204
4366
  return triggers;
4205
4367
  }
4206
4368
  function runVisibilityRulesOnce(v) {
4207
- var _a, _b, _c, _d, _e;
4369
+ var _a, _b, _c, _d;
4208
4370
  for (const t of v.tags) {
4209
4371
  const visible = v.fieldsVisibleUnder(t.id);
4210
4372
  const seen = /* @__PURE__ */ new Map();
@@ -4254,9 +4416,9 @@ function runVisibilityRulesOnce(v) {
4254
4416
  let hasUtility = false;
4255
4417
  const utilityOptionIds = [];
4256
4418
  for (const f of visible) {
4257
- for (const o of (_c = f.options) != null ? _c : []) {
4419
+ for (const { option: o } of walkFieldOptions(f)) {
4258
4420
  if (!isServiceIdRef(o.service_id)) continue;
4259
- const role = (_e = (_d = o.pricing_role) != null ? _d : f.pricing_role) != null ? _e : "base";
4421
+ const role = (_d = (_c = o.pricing_role) != null ? _c : f.pricing_role) != null ? _d : "base";
4260
4422
  if (role === "base") hasBase = true;
4261
4423
  else if (role === "utility") {
4262
4424
  hasUtility = true;
@@ -4297,7 +4459,7 @@ function dedupeErrorsInPlace(v, startIndex) {
4297
4459
  v.errors.splice(startIndex, v.errors.length - startIndex, ...kept);
4298
4460
  }
4299
4461
  function validateVisibility(v, options = {}) {
4300
- var _a, _b, _c, _d, _e;
4462
+ var _a, _b, _c, _d, _e, _f;
4301
4463
  v.simulatedVisibilityContexts = [];
4302
4464
  const simulate = options.simulate === true;
4303
4465
  if (!simulate) {
@@ -4322,10 +4484,13 @@ function validateVisibility(v, options = {}) {
4322
4484
  for (const key of Object.keys((_d = v.props.excludes_for_buttons) != null ? _d : {})) {
4323
4485
  effectfulKeys.add(key);
4324
4486
  }
4487
+ for (const key of Object.keys((_e = v.props.option_effects_for_buttons) != null ? _e : {})) {
4488
+ effectfulKeys.add(key);
4489
+ }
4325
4490
  }
4326
4491
  const roots = resolveRootTags(v.tags);
4327
4492
  const rootTags = options.simulateAllRoots ? roots : roots.slice(0, 1);
4328
- const originalSelected = new Set((_e = v.selectedKeys) != null ? _e : []);
4493
+ const originalSelected = new Set((_f = v.selectedKeys) != null ? _f : []);
4329
4494
  const errorsStart = v.errors.length;
4330
4495
  const visited = /* @__PURE__ */ new Set();
4331
4496
  const seenContexts = /* @__PURE__ */ new Set();
@@ -4466,7 +4631,7 @@ function validateStructure(v) {
4466
4631
 
4467
4632
  // src/core/validate/steps/identity.ts
4468
4633
  function validateIdentity(v) {
4469
- var _a, _b;
4634
+ var _a;
4470
4635
  const tags = v.tags;
4471
4636
  const fields = v.fields;
4472
4637
  {
@@ -4566,7 +4731,7 @@ function validateIdentity(v) {
4566
4731
  }
4567
4732
  }
4568
4733
  for (const f of fields) {
4569
- for (const o of (_b = f.options) != null ? _b : []) {
4734
+ for (const { option: o } of walkFieldOptions(f)) {
4570
4735
  if (!o.label || !o.label.trim()) {
4571
4736
  v.errors.push({
4572
4737
  code: "label_missing",
@@ -4581,25 +4746,11 @@ function validateIdentity(v) {
4581
4746
  }
4582
4747
 
4583
4748
  // src/core/validate/steps/option-maps.ts
4584
- function parseFieldOptionKey(key) {
4585
- const idx = key.indexOf("::");
4586
- if (idx === -1) return null;
4587
- const fieldId = key.slice(0, idx).trim();
4588
- const optionId = key.slice(idx + 2).trim();
4589
- if (!fieldId || !optionId) return null;
4590
- return { fieldId, optionId };
4591
- }
4592
- function hasOption(v, fid, oid) {
4593
- var _a;
4594
- const f = v.fieldById.get(fid);
4595
- if (!f) return false;
4596
- return !!((_a = f.options) != null ? _a : []).find((o) => o.id === oid);
4597
- }
4598
4749
  function validateOptionMaps(v) {
4599
- var _a, _b;
4750
+ var _a, _b, _c;
4600
4751
  const incMap = (_a = v.props.includes_for_buttons) != null ? _a : {};
4601
4752
  const excMap = (_b = v.props.excludes_for_buttons) != null ? _b : {};
4602
- 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.`;
4753
+ const badKeyMessage = (key) => `Invalid trigger-map key "${key}". Expected a known option id or button-field id.`;
4603
4754
  const validateTriggerKey = (key) => {
4604
4755
  const ref = v.nodeMap.get(key);
4605
4756
  if (ref) {
@@ -4618,19 +4769,7 @@ function validateOptionMaps(v) {
4618
4769
  }
4619
4770
  return { ok: false, nodeId: ref.id, affected: [ref.id] };
4620
4771
  }
4621
- const p = parseFieldOptionKey(key);
4622
- if (!p) return { ok: false };
4623
- if (!hasOption(v, p.fieldId, p.optionId))
4624
- return {
4625
- ok: false,
4626
- nodeId: p.fieldId,
4627
- affected: [p.fieldId, p.optionId]
4628
- };
4629
- return {
4630
- ok: true,
4631
- nodeId: p.fieldId,
4632
- affected: [p.fieldId, p.optionId]
4633
- };
4772
+ return { ok: false };
4634
4773
  };
4635
4774
  for (const k of Object.keys(incMap)) {
4636
4775
  const r = validateTriggerKey(k);
@@ -4656,6 +4795,57 @@ function validateOptionMaps(v) {
4656
4795
  });
4657
4796
  }
4658
4797
  }
4798
+ const effectMap = (_c = v.props.option_effects_for_buttons) != null ? _c : {};
4799
+ for (const [triggerKey, targets] of Object.entries(effectMap)) {
4800
+ const trigger = validateTriggerKey(triggerKey);
4801
+ if (!trigger.ok) {
4802
+ v.errors.push({
4803
+ code: "bad_option_effect_key",
4804
+ severity: "error",
4805
+ message: badKeyMessage(triggerKey),
4806
+ nodeId: trigger.nodeId,
4807
+ details: withAffected({ key: triggerKey }, trigger.affected)
4808
+ });
4809
+ }
4810
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
4811
+ const field = v.fieldById.get(targetFieldId);
4812
+ if (!field) {
4813
+ v.errors.push({
4814
+ code: "bad_option_effect_target",
4815
+ severity: "error",
4816
+ message: `Option effect trigger "${triggerKey}" targets unknown field "${targetFieldId}".`,
4817
+ details: withAffected(
4818
+ { key: triggerKey, targetFieldId },
4819
+ trigger.affected
4820
+ )
4821
+ });
4822
+ continue;
4823
+ }
4824
+ const validOptionIds = fieldOptionIdSet(field);
4825
+ const checkTargetOptions = (kind, optionIds) => {
4826
+ for (const optionId of optionIds != null ? optionIds : []) {
4827
+ if (validOptionIds.has(optionId)) continue;
4828
+ v.errors.push({
4829
+ code: "bad_option_effect_option",
4830
+ severity: "error",
4831
+ message: `Option effect trigger "${triggerKey}" references unknown ${kind} option "${optionId}" for field "${targetFieldId}".`,
4832
+ nodeId: targetFieldId,
4833
+ details: withAffected(
4834
+ {
4835
+ key: triggerKey,
4836
+ targetFieldId,
4837
+ optionId,
4838
+ kind
4839
+ },
4840
+ [targetFieldId, optionId]
4841
+ )
4842
+ });
4843
+ }
4844
+ };
4845
+ checkTargetOptions("include", effect == null ? void 0 : effect.include);
4846
+ checkTargetOptions("exclude", effect == null ? void 0 : effect.exclude);
4847
+ }
4848
+ }
4659
4849
  for (const k of Object.keys(incMap)) {
4660
4850
  if (!(k in excMap)) continue;
4661
4851
  const r = validateTriggerKey(k);
@@ -4669,27 +4859,231 @@ function validateOptionMaps(v) {
4669
4859
  }
4670
4860
  }
4671
4861
 
4672
- // src/utils/order-kind.ts
4673
- function normalizeSelectedTriggerKey(key, nodeMap) {
4674
- if (!key) return void 0;
4675
- const compositeIdx = key.indexOf("::");
4676
- if (compositeIdx !== -1) {
4677
- const fieldId = key.slice(0, compositeIdx).trim();
4678
- const optionId = key.slice(compositeIdx + 2).trim();
4679
- if (optionId) {
4680
- const optionRef = nodeMap.get(optionId);
4681
- if ((optionRef == null ? void 0 : optionRef.kind) === "option") {
4682
- return { nodeId: optionRef.id, nodeKind: "option" };
4862
+ // src/core/validate/steps/visibility-cycles.ts
4863
+ var MAX_VISIBILITY_CYCLE_DEPTH = 20;
4864
+ function validateVisibilityCycles(v) {
4865
+ const triggerById = buildTriggerIndex(v.fields);
4866
+ if (!triggerById.size) return;
4867
+ const fieldTriggers = buildFieldTriggerIndex(v.fields);
4868
+ const revealTargetsByTrigger = buildRevealIndex(v, triggerById);
4869
+ const reported = /* @__PURE__ */ new Set();
4870
+ for (const rootTriggerId of Array.from(triggerById.keys()).sort()) {
4871
+ const required = makeRequiredState(triggerById, [rootTriggerId]);
4872
+ walkFromTrigger({
4873
+ v,
4874
+ triggerById,
4875
+ fieldTriggers,
4876
+ revealTargetsByTrigger,
4877
+ rootTriggerId,
4878
+ currentTriggerId: rootTriggerId,
4879
+ required,
4880
+ path: [rootTriggerId],
4881
+ visited: /* @__PURE__ */ new Set(),
4882
+ reported,
4883
+ depth: 0
4884
+ });
4885
+ }
4886
+ }
4887
+ function buildTriggerIndex(fields) {
4888
+ const out = /* @__PURE__ */ new Map();
4889
+ const owners = optionOwnerMap(fields);
4890
+ for (const field of fields) {
4891
+ if (field.button === true) {
4892
+ out.set(field.id, {
4893
+ kind: "field",
4894
+ id: field.id,
4895
+ ownerFieldId: field.id
4896
+ });
4897
+ }
4898
+ }
4899
+ for (const [optionId, owner] of owners) {
4900
+ out.set(optionId, {
4901
+ kind: "option",
4902
+ id: optionId,
4903
+ ownerFieldId: owner.fieldId
4904
+ });
4905
+ }
4906
+ return out;
4907
+ }
4908
+ function buildFieldTriggerIndex(fields) {
4909
+ const out = /* @__PURE__ */ new Map();
4910
+ for (const field of fields) {
4911
+ const triggers = [];
4912
+ if (field.button === true) triggers.push(field.id);
4913
+ for (const visit of walkFieldOptions(field)) {
4914
+ triggers.push(visit.optionId);
4915
+ }
4916
+ out.set(field.id, triggers);
4917
+ }
4918
+ return out;
4919
+ }
4920
+ function buildRevealIndex(v, triggerById) {
4921
+ var _a, _b;
4922
+ const out = /* @__PURE__ */ new Map();
4923
+ const addReveal = (triggerId, targetFieldId) => {
4924
+ var _a2;
4925
+ if (!triggerById.has(triggerId)) return;
4926
+ if (!v.fieldById.has(targetFieldId)) return;
4927
+ const set = (_a2 = out.get(triggerId)) != null ? _a2 : /* @__PURE__ */ new Set();
4928
+ set.add(targetFieldId);
4929
+ out.set(triggerId, set);
4930
+ };
4931
+ for (const [triggerId, targetIds] of Object.entries(
4932
+ (_a = v.props.includes_for_buttons) != null ? _a : {}
4933
+ )) {
4934
+ for (const targetId of targetIds != null ? targetIds : []) addReveal(triggerId, targetId);
4935
+ }
4936
+ for (const [triggerId, targets] of Object.entries(
4937
+ (_b = v.props.option_effects_for_buttons) != null ? _b : {}
4938
+ )) {
4939
+ for (const [targetFieldId, effect] of Object.entries(targets != null ? targets : {})) {
4940
+ if ((effect == null ? void 0 : effect.forceVisible) === true)
4941
+ addReveal(triggerId, targetFieldId);
4942
+ }
4943
+ }
4944
+ return new Map(
4945
+ Array.from(out.entries()).map(([triggerId, fieldIds]) => [
4946
+ triggerId,
4947
+ Array.from(fieldIds).sort()
4948
+ ])
4949
+ );
4950
+ }
4951
+ function walkFromTrigger(args) {
4952
+ var _a, _b, _c;
4953
+ if (args.depth >= MAX_VISIBILITY_CYCLE_DEPTH) return;
4954
+ const visitedKey = `${args.rootTriggerId}::${args.currentTriggerId}::${args.path.join(">")}`;
4955
+ if (args.visited.has(visitedKey)) return;
4956
+ args.visited.add(visitedKey);
4957
+ const revealedFieldIds = (_a = args.revealTargetsByTrigger.get(args.currentTriggerId)) != null ? _a : [];
4958
+ for (const revealedFieldId of revealedFieldIds) {
4959
+ const reachableTriggers = (_c = (_b = args.fieldTriggers.get(revealedFieldId)) == null ? void 0 : _b.slice().sort()) != null ? _c : [];
4960
+ for (const reachableTriggerId of reachableTriggers) {
4961
+ const invalidation = invalidatesRequiredPath(
4962
+ args.v,
4963
+ args.triggerById,
4964
+ reachableTriggerId,
4965
+ args.required
4966
+ );
4967
+ if (invalidation) {
4968
+ emitCycleError({
4969
+ v: args.v,
4970
+ rootTriggerId: args.rootTriggerId,
4971
+ revealedFieldId,
4972
+ conflictingTriggerId: reachableTriggerId,
4973
+ invalidatedId: invalidation.invalidatedId,
4974
+ path: [...args.path, reachableTriggerId],
4975
+ reported: args.reported
4976
+ });
4683
4977
  }
4978
+ if (args.path.includes(reachableTriggerId)) continue;
4979
+ walkFromTrigger({
4980
+ ...args,
4981
+ currentTriggerId: reachableTriggerId,
4982
+ required: addRequiredTrigger(
4983
+ args.triggerById,
4984
+ args.required,
4985
+ reachableTriggerId
4986
+ ),
4987
+ path: [...args.path, reachableTriggerId],
4988
+ depth: args.depth + 1
4989
+ });
4684
4990
  }
4685
- if (fieldId) {
4686
- const fieldRef = nodeMap.get(fieldId);
4687
- if ((fieldRef == null ? void 0 : fieldRef.kind) === "field") {
4688
- return { nodeId: fieldRef.id, nodeKind: "field" };
4991
+ }
4992
+ }
4993
+ function makeRequiredState(triggerById, triggerIds) {
4994
+ let required = {
4995
+ triggers: /* @__PURE__ */ new Set(),
4996
+ ownerFields: /* @__PURE__ */ new Set()
4997
+ };
4998
+ for (const triggerId of triggerIds) {
4999
+ required = addRequiredTrigger(triggerById, required, triggerId);
5000
+ }
5001
+ return required;
5002
+ }
5003
+ function addRequiredTrigger(triggerById, current, triggerId) {
5004
+ const next = {
5005
+ triggers: new Set(current.triggers),
5006
+ ownerFields: new Set(current.ownerFields)
5007
+ };
5008
+ const trigger = triggerById.get(triggerId);
5009
+ if (!trigger) return next;
5010
+ next.triggers.add(triggerId);
5011
+ next.ownerFields.add(trigger.ownerFieldId);
5012
+ return next;
5013
+ }
5014
+ function invalidatesRequiredPath(v, triggerById, conflictingTriggerId, required) {
5015
+ var _a, _b, _c, _d, _e, _f;
5016
+ for (const targetId of (_b = (_a = v.props.excludes_for_buttons) == null ? void 0 : _a[conflictingTriggerId]) != null ? _b : []) {
5017
+ if (required.ownerFields.has(targetId)) {
5018
+ return { invalidatedId: targetId };
5019
+ }
5020
+ const targetTrigger = triggerById.get(targetId);
5021
+ if ((targetTrigger == null ? void 0 : targetTrigger.kind) === "option" && required.triggers.has(targetId)) {
5022
+ return { invalidatedId: targetId };
5023
+ }
5024
+ }
5025
+ const effects = (_d = (_c = v.props.option_effects_for_buttons) == null ? void 0 : _c[conflictingTriggerId]) != null ? _d : {};
5026
+ for (const [targetFieldId, effect] of Object.entries(effects)) {
5027
+ if (!v.fieldById.has(targetFieldId)) continue;
5028
+ if ((_e = effect == null ? void 0 : effect.exclude) == null ? void 0 : _e.length) {
5029
+ const excluded = new Set(effect.exclude);
5030
+ for (const requiredTriggerId of required.triggers) {
5031
+ const requiredTrigger = triggerById.get(requiredTriggerId);
5032
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
5033
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
5034
+ if (excluded.has(requiredTriggerId)) {
5035
+ return { invalidatedId: requiredTriggerId };
5036
+ }
5037
+ }
5038
+ }
5039
+ if ((_f = effect == null ? void 0 : effect.include) == null ? void 0 : _f.length) {
5040
+ const included = new Set(effect.include);
5041
+ for (const requiredTriggerId of required.triggers) {
5042
+ const requiredTrigger = triggerById.get(requiredTriggerId);
5043
+ if ((requiredTrigger == null ? void 0 : requiredTrigger.kind) !== "option") continue;
5044
+ if (requiredTrigger.ownerFieldId !== targetFieldId) continue;
5045
+ if (!included.has(requiredTriggerId)) {
5046
+ return { invalidatedId: requiredTriggerId };
5047
+ }
4689
5048
  }
4690
5049
  }
4691
- return void 0;
4692
5050
  }
5051
+ return void 0;
5052
+ }
5053
+ function emitCycleError(args) {
5054
+ const key = [
5055
+ args.rootTriggerId,
5056
+ args.conflictingTriggerId,
5057
+ args.invalidatedId,
5058
+ args.path.join(">")
5059
+ ].join("::");
5060
+ if (args.reported.has(key)) return;
5061
+ args.reported.add(key);
5062
+ args.v.errors.push({
5063
+ code: "visibility_dependency_cycle",
5064
+ severity: "error",
5065
+ message: `Visibility dependency cycle: trigger "${args.rootTriggerId}" reveals "${args.revealedFieldId}", but reachable trigger "${args.conflictingTriggerId}" can hide or remove "${args.invalidatedId}".`,
5066
+ nodeId: args.conflictingTriggerId,
5067
+ details: withAffected(
5068
+ {
5069
+ rootTriggerId: args.rootTriggerId,
5070
+ conflictingTriggerId: args.conflictingTriggerId,
5071
+ invalidatedId: args.invalidatedId,
5072
+ path: args.path
5073
+ },
5074
+ [
5075
+ args.rootTriggerId,
5076
+ args.revealedFieldId,
5077
+ args.conflictingTriggerId,
5078
+ args.invalidatedId
5079
+ ]
5080
+ )
5081
+ });
5082
+ }
5083
+
5084
+ // src/utils/order-kind.ts
5085
+ function normalizeSelectedTriggerKey(key, nodeMap) {
5086
+ if (!key) return void 0;
4693
5087
  const ref = nodeMap.get(key);
4694
5088
  if (!ref) return void 0;
4695
5089
  if (ref.kind !== "field" && ref.kind !== "option") return void 0;
@@ -4848,8 +5242,7 @@ function validateUtilityMarkers(v) {
4848
5242
  "percent"
4849
5243
  ]);
4850
5244
  for (const f of v.fields) {
4851
- const optsArr = Array.isArray(f.options) ? f.options : [];
4852
- for (const o of optsArr) {
5245
+ for (const { option: o } of walkFieldOptions(f)) {
4853
5246
  const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
4854
5247
  const hasService = isServiceIdRef(o.service_id);
4855
5248
  const util = (_c = o.meta) == null ? void 0 : _c.utility;
@@ -5071,13 +5464,13 @@ function normalizeServiceRef(value) {
5071
5464
 
5072
5465
  // src/core/validate/steps/rates.ts
5073
5466
  function validateRates(v) {
5074
- var _a, _b, _c;
5467
+ var _a, _b;
5075
5468
  const ratePolicy = normalizeRatePolicy(v.options.ratePolicy);
5076
5469
  for (const f of v.fields) {
5077
5470
  if (!isMultiField(f)) continue;
5078
5471
  const baseRates = [];
5079
- for (const o of (_a = f.options) != null ? _a : []) {
5080
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
5472
+ for (const { option: o } of walkFieldOptions(f)) {
5473
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
5081
5474
  if (role !== "base") continue;
5082
5475
  const sid = o.service_id;
5083
5476
  if (!isServiceIdRef(sid)) continue;
@@ -5488,7 +5881,7 @@ function effectiveConstraints(v, tagId) {
5488
5881
  return out;
5489
5882
  }
5490
5883
  function validateConstraints(v) {
5491
- var _a, _b;
5884
+ var _a;
5492
5885
  for (const t of v.tags) {
5493
5886
  const eff = effectiveConstraints(v, t.id);
5494
5887
  const hasAnyRequired = Object.values(eff).some(
@@ -5497,7 +5890,7 @@ function validateConstraints(v) {
5497
5890
  if (!hasAnyRequired) continue;
5498
5891
  const visible = v.fieldsVisibleUnder(t.id);
5499
5892
  for (const f of visible) {
5500
- for (const o of (_a = f.options) != null ? _a : []) {
5893
+ for (const { option: o } of walkFieldOptions(f)) {
5501
5894
  if (!isServiceIdRef(o.service_id)) continue;
5502
5895
  const svc = getServiceCapability(v.serviceMap, o.service_id);
5503
5896
  if (!svc || typeof svc !== "object") continue;
@@ -5551,7 +5944,7 @@ function validateConstraints(v) {
5551
5944
  if (!row) continue;
5552
5945
  const from = row.from === true;
5553
5946
  const to = row.to === true;
5554
- const origin = String((_b = row.origin) != null ? _b : "");
5947
+ const origin = String((_a = row.origin) != null ? _a : "");
5555
5948
  v.errors.push({
5556
5949
  code: "constraint_overridden",
5557
5950
  severity: "warning",
@@ -5585,14 +5978,14 @@ function validateCustomFields(v) {
5585
5978
 
5586
5979
  // src/core/validate/steps/global-utility-guard.ts
5587
5980
  function validateGlobalUtilityGuard(v) {
5588
- var _a, _b, _c;
5981
+ var _a, _b;
5589
5982
  if (!v.options.globalUtilityGuard) return;
5590
5983
  let hasUtility = false;
5591
5984
  let hasBase = false;
5592
5985
  for (const f of v.fields) {
5593
- for (const o of (_a = f.options) != null ? _a : []) {
5986
+ for (const { option: o } of walkFieldOptions(f)) {
5594
5987
  if (!isServiceIdRef(o.service_id)) continue;
5595
- const role = (_c = (_b = o.pricing_role) != null ? _b : f.pricing_role) != null ? _c : "base";
5988
+ const role = (_b = (_a = o.pricing_role) != null ? _a : f.pricing_role) != null ? _b : "base";
5596
5989
  if (role === "base") hasBase = true;
5597
5990
  else if (role === "utility") hasUtility = true;
5598
5991
  if (hasUtility && hasBase) break;
@@ -5794,7 +6187,7 @@ function applyFilterAllowLists(tagId, fieldId, filter) {
5794
6187
  return true;
5795
6188
  }
5796
6189
  function collectServiceItems(args) {
5797
- var _a, _b, _c, _d, _e;
6190
+ var _a, _b, _c, _d;
5798
6191
  const filter = args.filter;
5799
6192
  const roleFilter = (_a = filter == null ? void 0 : filter.role) != null ? _a : "both";
5800
6193
  const where = filter == null ? void 0 : filter.where;
@@ -5844,7 +6237,7 @@ function collectServiceItems(args) {
5844
6237
  affectedIds: [`field:${f.id}`, `service:${String(fSid)}`]
5845
6238
  });
5846
6239
  }
5847
- for (const o of (_d = f.options) != null ? _d : []) {
6240
+ for (const { option: o } of walkFieldOptions(f)) {
5848
6241
  const oSid = o.service_id;
5849
6242
  if (!isServiceIdRef2(oSid)) continue;
5850
6243
  const role = fieldRoleOf(f, o);
@@ -5929,7 +6322,7 @@ function collectServiceItems(args) {
5929
6322
  }
5930
6323
  } else if (includeGroupFallbacks) {
5931
6324
  const allowPrimaries = new Set(
5932
- ((_e = args.visiblePrimaries) != null ? _e : []).map((x) => String(x))
6325
+ ((_d = args.visiblePrimaries) != null ? _d : []).map((x) => String(x))
5933
6326
  );
5934
6327
  for (const primaryKey of allowPrimaries) {
5935
6328
  const list = globalFb[primaryKey];
@@ -6010,17 +6403,15 @@ function affectedFromItems(items) {
6010
6403
  return uniq(ids);
6011
6404
  }
6012
6405
  function visibleGroupNodeIds(tag, fields) {
6013
- var _a;
6014
6406
  const ids = [tag.id];
6015
6407
  for (const f of fields) {
6016
- for (const o of (_a = f.options) != null ? _a : []) {
6408
+ for (const { option: o } of walkFieldOptions(f)) {
6017
6409
  ids.push(o.id);
6018
6410
  }
6019
6411
  }
6020
6412
  return uniq(ids);
6021
6413
  }
6022
6414
  function visibleGroupPrimaries(tag, fields) {
6023
- var _a;
6024
6415
  const prim = [];
6025
6416
  const tagSid = tag.service_id;
6026
6417
  if (typeof tagSid === "string" || typeof tagSid === "number" && Number.isFinite(tagSid)) {
@@ -6031,7 +6422,7 @@ function visibleGroupPrimaries(tag, fields) {
6031
6422
  if (typeof fsid === "string" || typeof fsid === "number" && Number.isFinite(fsid)) {
6032
6423
  prim.push(fsid);
6033
6424
  }
6034
- for (const o of (_a = f.options) != null ? _a : []) {
6425
+ for (const { option: o } of walkFieldOptions(f)) {
6035
6426
  const osid = o.service_id;
6036
6427
  if (typeof osid === "string" || typeof osid === "number" && Number.isFinite(osid)) {
6037
6428
  prim.push(osid);
@@ -6255,6 +6646,7 @@ function validate(props, ctx = {}) {
6255
6646
  validateStructure(v);
6256
6647
  validateIdentity(v);
6257
6648
  validateOptionMaps(v);
6649
+ validateVisibilityCycles(v);
6258
6650
  validateOrderKinds(v);
6259
6651
  v.fieldsVisibleUnder = createFieldsVisibleUnder(v);
6260
6652
  const visSim = readVisibilitySimOpts(options);
@@ -6388,14 +6780,14 @@ var BuilderImpl = class {
6388
6780
  const showOptions = showSet.has(f.id);
6389
6781
  if (!showOptions) continue;
6390
6782
  if (!Array.isArray(f.options)) continue;
6391
- for (const o of f.options) {
6783
+ for (const { option: o, parentId } of walkFieldOptions(f)) {
6392
6784
  nodes.push({
6393
6785
  id: o.id,
6394
6786
  kind: "option",
6395
6787
  label: o.label
6396
6788
  });
6397
6789
  const e = {
6398
- from: f.id,
6790
+ from: parentId != null ? parentId : f.id,
6399
6791
  to: o.id,
6400
6792
  kind: "option",
6401
6793
  meta: { ownerField: f.id }
@@ -6442,7 +6834,7 @@ var BuilderImpl = class {
6442
6834
  return { nodes, edges };
6443
6835
  }
6444
6836
  cleanedProps() {
6445
- var _a, _b, _c, _d, _e;
6837
+ var _a, _b, _c, _d, _e, _f;
6446
6838
  const fieldIds = new Set(this.props.fields.map((f) => f.id));
6447
6839
  const optionIds = /* @__PURE__ */ new Set();
6448
6840
  this.optionOwnerById.forEach((_v, oid) => optionIds.add(oid));
@@ -6454,6 +6846,7 @@ var BuilderImpl = class {
6454
6846
  }
6455
6847
  const incMap = (_c = this.props.includes_for_buttons) != null ? _c : {};
6456
6848
  const excMap = (_d = this.props.excludes_for_buttons) != null ? _d : {};
6849
+ const effectMap = (_e = this.props.option_effects_for_buttons) != null ? _e : {};
6457
6850
  const includedByButtons = /* @__PURE__ */ new Set();
6458
6851
  const referencedKeys = /* @__PURE__ */ new Set();
6459
6852
  const referencedOwnerFields = /* @__PURE__ */ new Set();
@@ -6473,6 +6866,14 @@ var BuilderImpl = class {
6473
6866
  void fid;
6474
6867
  }
6475
6868
  }
6869
+ for (const [key, targets] of Object.entries(effectMap)) {
6870
+ referencedKeys.add(key);
6871
+ const owner = this.optionOwnerById.get(key);
6872
+ if (owner) referencedOwnerFields.add(owner.fieldId);
6873
+ for (const [fid, effect] of Object.entries(targets != null ? targets : {})) {
6874
+ if ((effect == null ? void 0 : effect.forceVisible) === true) includedByButtons.add(fid);
6875
+ }
6876
+ }
6476
6877
  const boundIds = /* @__PURE__ */ new Set();
6477
6878
  for (const f of this.props.fields) {
6478
6879
  const b = f.bind_id;
@@ -6490,6 +6891,7 @@ var BuilderImpl = class {
6490
6891
  return bound || included || referenced || !excluded;
6491
6892
  });
6492
6893
  const allowedTargets = new Set(fields.map((f) => f.id));
6894
+ const allowedFieldById = new Map(fields.map((f) => [f.id, f]));
6493
6895
  const pruneButtons = (src) => {
6494
6896
  if (!src) return void 0;
6495
6897
  const out2 = {};
@@ -6509,13 +6911,52 @@ var BuilderImpl = class {
6509
6911
  const excludes_for_buttons = pruneButtons(
6510
6912
  this.props.excludes_for_buttons
6511
6913
  );
6914
+ const pruneOptionEffects = (src) => {
6915
+ var _a2, _b2, _c2, _d2;
6916
+ if (!src) return void 0;
6917
+ const out2 = {};
6918
+ for (const [key, targets] of Object.entries(src)) {
6919
+ const keyIsValid = optionIds.has(key) || fieldIds.has(key);
6920
+ if (!keyIsValid) continue;
6921
+ const cleanedTargets = {};
6922
+ for (const [targetFieldId, effect] of Object.entries(
6923
+ targets != null ? targets : {}
6924
+ )) {
6925
+ const field = allowedFieldById.get(targetFieldId);
6926
+ if (!field || !effect) continue;
6927
+ const validOptionIds = fieldOptionIdSet(field);
6928
+ const include2 = Array.from(
6929
+ new Set((_a2 = effect.include) != null ? _a2 : [])
6930
+ ).filter((optionId) => validOptionIds.has(optionId));
6931
+ const exclude2 = Array.from(
6932
+ new Set((_b2 = effect.exclude) != null ? _b2 : [])
6933
+ ).filter((optionId) => validOptionIds.has(optionId));
6934
+ const next = {
6935
+ ...effect.forceVisible === true ? { forceVisible: true } : {},
6936
+ ...include2.length ? { include: include2 } : {},
6937
+ ...exclude2.length ? { exclude: exclude2 } : {}
6938
+ };
6939
+ if (next.forceVisible === true || ((_c2 = next.include) == null ? void 0 : _c2.length) || ((_d2 = next.exclude) == null ? void 0 : _d2.length)) {
6940
+ cleanedTargets[targetFieldId] = next;
6941
+ }
6942
+ }
6943
+ if (Object.keys(cleanedTargets).length) {
6944
+ out2[key] = cleanedTargets;
6945
+ }
6946
+ }
6947
+ return Object.keys(out2).length ? out2 : void 0;
6948
+ };
6949
+ const option_effects_for_buttons = pruneOptionEffects(
6950
+ this.props.option_effects_for_buttons
6951
+ );
6512
6952
  const out = {
6513
6953
  filters: this.props.filters.slice(),
6514
6954
  fields,
6515
6955
  ...this.props.orderKinds ? { orderKinds: this.props.orderKinds } : {},
6516
6956
  ...includes_for_buttons && { includes_for_buttons },
6517
6957
  ...excludes_for_buttons && { excludes_for_buttons },
6518
- schema_version: (_e = this.props.schema_version) != null ? _e : "1.0",
6958
+ ...option_effects_for_buttons && { option_effects_for_buttons },
6959
+ schema_version: (_f = this.props.schema_version) != null ? _f : "1.0",
6519
6960
  // keep fallbacks & other maps as-is
6520
6961
  ...this.props.fallbacks ? { fallbacks: this.props.fallbacks } : {}
6521
6962
  };
@@ -6528,12 +6969,15 @@ var BuilderImpl = class {
6528
6969
  return cloneDeep2(this.options);
6529
6970
  }
6530
6971
  visibleFields(tagId, selectedKeys) {
6972
+ return this.resolveVisibility(tagId, selectedKeys).fieldIds;
6973
+ }
6974
+ resolveVisibility(tagId, selectedKeys) {
6531
6975
  var _a;
6532
- return visibleFieldIdsUnder(this.props, tagId, {
6533
- selectedKeys: new Set(
6534
- (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
6535
- )
6536
- });
6976
+ return resolveVisibility(
6977
+ this.props,
6978
+ tagId,
6979
+ (_a = selectedKeys != null ? selectedKeys : this.options.selectedOptionKeys) != null ? _a : []
6980
+ );
6537
6981
  }
6538
6982
  getNodeMap() {
6539
6983
  if (!this._nodemap) this._nodemap = buildNodeMap(this.getProps());
@@ -6548,9 +6992,8 @@ var BuilderImpl = class {
6548
6992
  for (const t of this.props.filters) this.tagById.set(t.id, t);
6549
6993
  for (const f of this.props.fields) {
6550
6994
  this.fieldById.set(f.id, f);
6551
- if (Array.isArray(f.options)) {
6552
- for (const o of f.options)
6553
- this.optionOwnerById.set(o.id, { fieldId: f.id });
6995
+ for (const [optionId, owner] of optionOwnerMap([f])) {
6996
+ this.optionOwnerById.set(optionId, { fieldId: owner.fieldId });
6554
6997
  }
6555
6998
  }
6556
6999
  }
@@ -7498,44 +7941,135 @@ function bumpSuffix(old) {
7498
7941
  return `${stem}${parseInt(m[2], 10) + 1}`;
7499
7942
  }
7500
7943
 
7501
- // src/react/canvas/editor/editor-duplicate.ts
7502
- function duplicate(ctx, ref, opts = {}) {
7503
- const snapBefore = ctx.makeSnapshot("duplicate:before");
7504
- try {
7505
- let newId2 = "";
7506
- ctx.transact("duplicate", () => {
7507
- newId2 = duplicateInPlace(ctx, ref, opts);
7508
- });
7509
- return newId2;
7510
- } catch (err) {
7511
- ctx.loadSnapshot(snapBefore, "undo");
7512
- throw err;
7944
+ // src/react/canvas/editor/editor-utils.ts
7945
+ function ownerOfOption(props, optionId) {
7946
+ var _a;
7947
+ for (const f of (_a = props.fields) != null ? _a : []) {
7948
+ const found = findOptionLocationInField(f, optionId);
7949
+ if (found) return { fieldId: f.id, index: found.index };
7513
7950
  }
7951
+ return null;
7514
7952
  }
7515
- function duplicateMany(ctx, ids, opts = {}) {
7516
- const ordered = Array.from(new Set((ids != null ? ids : []).map((id) => String(id))));
7517
- if (!ordered.length) return [];
7518
- const snapBefore = ctx.makeSnapshot("duplicateMany:before");
7519
- try {
7520
- const created = [];
7521
- ctx.transact("duplicateMany", () => {
7522
- var _a, _b, _c;
7523
- const props = ctx.getProps();
7524
- const selectedFields = /* @__PURE__ */ new Set();
7525
- for (const id of ordered) {
7526
- if (ctx.isFieldId(id) && ((_a = props.fields) != null ? _a : []).some((f) => f.id === id)) {
7527
- selectedFields.add(id);
7528
- }
7529
- }
7530
- for (const id of ordered) {
7531
- if (ctx.isTagId(id)) {
7532
- if (!((_b = ctx.getProps().filters) != null ? _b : []).some((t) => t.id === id)) continue;
7533
- created.push(
7534
- duplicateInPlace(ctx, { kind: "tag", id }, opts)
7535
- );
7536
- continue;
7537
- }
7538
- if (ctx.isFieldId(id)) {
7953
+ function findMutableOption(props, optionId) {
7954
+ var _a;
7955
+ for (const field of (_a = props.fields) != null ? _a : []) {
7956
+ const found = findOptionLocationInField(field, optionId);
7957
+ if (found) return { field, ...found };
7958
+ }
7959
+ return void 0;
7960
+ }
7961
+ function collectFieldOptionIds(field) {
7962
+ const out = [];
7963
+ const visit = (options) => {
7964
+ for (const option of options != null ? options : []) {
7965
+ out.push(String(option.id));
7966
+ visit(option.children);
7967
+ }
7968
+ };
7969
+ visit(field == null ? void 0 : field.options);
7970
+ return out;
7971
+ }
7972
+ function findOptionLocationInField(field, optionId) {
7973
+ const visit = (siblings, parent) => {
7974
+ if (!siblings) return void 0;
7975
+ const index = siblings.findIndex((option) => option.id === optionId);
7976
+ if (index >= 0) {
7977
+ return {
7978
+ option: siblings[index],
7979
+ siblings,
7980
+ index,
7981
+ parent
7982
+ };
7983
+ }
7984
+ for (const option of siblings) {
7985
+ const found = visit(option.children, option);
7986
+ if (found) return found;
7987
+ }
7988
+ return void 0;
7989
+ };
7990
+ return visit(field.options);
7991
+ }
7992
+ function hasFieldOptions(field) {
7993
+ return Array.isArray(field == null ? void 0 : field.options) && field.options.length > 0;
7994
+ }
7995
+ function isActualButtonField(field) {
7996
+ return (field == null ? void 0 : field.button) === true && !hasFieldOptions(field);
7997
+ }
7998
+ function clearFieldButtonReceiverMaps(props, fieldId) {
7999
+ var _a, _b, _c;
8000
+ if ((_a = props.includes_for_buttons) == null ? void 0 : _a[fieldId]) {
8001
+ delete props.includes_for_buttons[fieldId];
8002
+ }
8003
+ if ((_b = props.excludes_for_buttons) == null ? void 0 : _b[fieldId]) {
8004
+ delete props.excludes_for_buttons[fieldId];
8005
+ }
8006
+ if (props.includes_for_buttons && Object.keys(props.includes_for_buttons).length === 0) {
8007
+ delete props.includes_for_buttons;
8008
+ }
8009
+ if (props.excludes_for_buttons && Object.keys(props.excludes_for_buttons).length === 0) {
8010
+ delete props.excludes_for_buttons;
8011
+ }
8012
+ if ((_c = props.option_effects_for_buttons) == null ? void 0 : _c[fieldId]) {
8013
+ delete props.option_effects_for_buttons[fieldId];
8014
+ }
8015
+ if (props.option_effects_for_buttons && Object.keys(props.option_effects_for_buttons).length === 0) {
8016
+ delete props.option_effects_for_buttons;
8017
+ }
8018
+ }
8019
+ function ensureServiceExists(opts, id) {
8020
+ if (typeof opts.serviceExists === "function") {
8021
+ if (!opts.serviceExists(id)) {
8022
+ throw new Error(`service_not_found:${String(id)}`);
8023
+ }
8024
+ return;
8025
+ }
8026
+ if (opts.serviceMap) {
8027
+ if (!Object.prototype.hasOwnProperty.call(opts.serviceMap, id)) {
8028
+ throw new Error(`service_not_found:${String(id)}`);
8029
+ }
8030
+ return;
8031
+ }
8032
+ throw new Error("service_checker_missing");
8033
+ }
8034
+
8035
+ // src/react/canvas/editor/editor-duplicate.ts
8036
+ function duplicate(ctx, ref, opts = {}) {
8037
+ const snapBefore = ctx.makeSnapshot("duplicate:before");
8038
+ try {
8039
+ let newId2 = "";
8040
+ ctx.transact("duplicate", () => {
8041
+ newId2 = duplicateInPlace(ctx, ref, opts);
8042
+ });
8043
+ return newId2;
8044
+ } catch (err) {
8045
+ ctx.loadSnapshot(snapBefore, "undo");
8046
+ throw err;
8047
+ }
8048
+ }
8049
+ function duplicateMany(ctx, ids, opts = {}) {
8050
+ const ordered = Array.from(new Set((ids != null ? ids : []).map((id) => String(id))));
8051
+ if (!ordered.length) return [];
8052
+ const snapBefore = ctx.makeSnapshot("duplicateMany:before");
8053
+ try {
8054
+ const created = [];
8055
+ ctx.transact("duplicateMany", () => {
8056
+ var _a, _b, _c;
8057
+ const props = ctx.getProps();
8058
+ const selectedFields = /* @__PURE__ */ new Set();
8059
+ for (const id of ordered) {
8060
+ if (ctx.isFieldId(id) && ((_a = props.fields) != null ? _a : []).some((f) => f.id === id)) {
8061
+ selectedFields.add(id);
8062
+ }
8063
+ }
8064
+ for (const id of ordered) {
8065
+ if (ctx.isTagId(id)) {
8066
+ if (!((_b = ctx.getProps().filters) != null ? _b : []).some((t) => t.id === id)) continue;
8067
+ created.push(
8068
+ duplicateInPlace(ctx, { kind: "tag", id }, opts)
8069
+ );
8070
+ continue;
8071
+ }
8072
+ if (ctx.isFieldId(id)) {
7539
8073
  if (!((_c = ctx.getProps().fields) != null ? _c : []).some((f) => f.id === id)) continue;
7540
8074
  created.push(
7541
8075
  duplicateInPlace(ctx, { kind: "field", id }, opts)
@@ -7572,14 +8106,66 @@ function duplicateInPlace(ctx, ref, opts = {}) {
7572
8106
  return duplicateOption(ctx, ref.fieldId, ref.id, opts);
7573
8107
  }
7574
8108
  function ownerFieldOfOption(props, optionId) {
7575
- var _a, _b;
8109
+ var _a;
7576
8110
  for (const field of (_a = props.fields) != null ? _a : []) {
7577
- if (((_b = field.options) != null ? _b : []).some((o) => o.id === optionId)) {
8111
+ if (findMutableOption({ ...props, fields: [field] }, optionId)) {
7578
8112
  return { fieldId: field.id };
7579
8113
  }
7580
8114
  }
7581
8115
  return null;
7582
8116
  }
8117
+ function cloneOptionTree(ctx, fieldId, option, opts, optionIdMap) {
8118
+ var _a, _b, _c, _d;
8119
+ const newId2 = ctx.uniqueOptionId(
8120
+ fieldId,
8121
+ ((_a = opts.optionIdStrategy) != null ? _a : defaultOptionIdStrategy)(option.id)
8122
+ );
8123
+ optionIdMap.set(option.id, newId2);
8124
+ const children = (_b = option.children) == null ? void 0 : _b.map(
8125
+ (child) => cloneOptionTree(ctx, fieldId, child, opts, optionIdMap)
8126
+ );
8127
+ return {
8128
+ ...option,
8129
+ id: newId2,
8130
+ label: ((_c = opts.labelStrategy) != null ? _c : nextCopyLabel)((_d = option.label) != null ? _d : option.id),
8131
+ ...(children == null ? void 0 : children.length) ? { children } : {}
8132
+ };
8133
+ }
8134
+ function remapEffect(effect, optionIdMap) {
8135
+ const remapList = (values) => values == null ? void 0 : values.map((value) => {
8136
+ var _a;
8137
+ return (_a = optionIdMap.get(value)) != null ? _a : value;
8138
+ });
8139
+ return {
8140
+ ...effect,
8141
+ ...effect.include ? { include: remapList(effect.include) } : {},
8142
+ ...effect.exclude ? { exclude: remapList(effect.exclude) } : {}
8143
+ };
8144
+ }
8145
+ function copyOptionEffects(props, args) {
8146
+ var _a, _b, _c, _d, _e;
8147
+ const source = props.option_effects_for_buttons;
8148
+ if (!source) return;
8149
+ const next = {
8150
+ ...source
8151
+ };
8152
+ const triggerIdMap = (_a = args.triggerIdMap) != null ? _a : /* @__PURE__ */ new Map();
8153
+ const targetFieldIdMap = (_b = args.targetFieldIdMap) != null ? _b : /* @__PURE__ */ new Map();
8154
+ const optionIdMap = (_c = args.optionIdMap) != null ? _c : /* @__PURE__ */ new Map();
8155
+ for (const [oldTriggerId, targetMap] of Object.entries(source)) {
8156
+ const newTriggerId = triggerIdMap.get(oldTriggerId);
8157
+ if (!newTriggerId) continue;
8158
+ const copiedTargets = {
8159
+ ...(_d = next[newTriggerId]) != null ? _d : {}
8160
+ };
8161
+ for (const [oldTargetFieldId, effect] of Object.entries(targetMap != null ? targetMap : {})) {
8162
+ const newTargetFieldId = (_e = targetFieldIdMap.get(oldTargetFieldId)) != null ? _e : oldTargetFieldId;
8163
+ copiedTargets[newTargetFieldId] = remapEffect(effect, optionIdMap);
8164
+ }
8165
+ next[newTriggerId] = copiedTargets;
8166
+ }
8167
+ props.option_effects_for_buttons = next;
8168
+ }
7583
8169
  function duplicateTag(ctx, tagId, opts) {
7584
8170
  var _a, _b, _c, _d;
7585
8171
  const props = ctx.getProps();
@@ -7635,7 +8221,7 @@ function duplicateTag(ctx, tagId, opts) {
7635
8221
  return id;
7636
8222
  }
7637
8223
  function duplicateField(ctx, fieldId, opts) {
7638
- var _a, _b, _c, _d, _e, _f, _g;
8224
+ var _a, _b, _c, _d, _e, _f;
7639
8225
  const props = ctx.getProps();
7640
8226
  const fields = (_a = props.fields) != null ? _a : [];
7641
8227
  const src = fields.find((f) => f.id === fieldId);
@@ -7643,21 +8229,10 @@ function duplicateField(ctx, fieldId, opts) {
7643
8229
  const id = (_b = opts.id) != null ? _b : ctx.uniqueId(src.id);
7644
8230
  const label = ((_c = opts.labelStrategy) != null ? _c : nextCopyLabel)((_d = src.label) != null ? _d : id);
7645
8231
  const name = opts.nameStrategy ? opts.nameStrategy(src.name) : nextCopyName(src.name);
7646
- const optId = (old) => {
7647
- var _a2;
7648
- return ctx.uniqueOptionId(
7649
- id,
7650
- ((_a2 = opts.optionIdStrategy) != null ? _a2 : defaultOptionIdStrategy)(old)
7651
- );
7652
- };
7653
- const clonedOptions = ((_e = src.options) != null ? _e : []).map((o) => {
7654
- var _a2, _b2;
7655
- return {
7656
- ...o,
7657
- id: optId(o.id),
7658
- label: ((_a2 = opts.labelStrategy) != null ? _a2 : nextCopyLabel)((_b2 = o.label) != null ? _b2 : o.id)
7659
- };
7660
- });
8232
+ const optionIdMap = /* @__PURE__ */ new Map();
8233
+ const clonedOptions = ((_e = src.options) != null ? _e : []).map(
8234
+ (o) => cloneOptionTree(ctx, id, o, opts, optionIdMap)
8235
+ );
7661
8236
  const cloned = {
7662
8237
  ...src,
7663
8238
  id,
@@ -7666,14 +8241,8 @@ function duplicateField(ctx, fieldId, opts) {
7666
8241
  bind_id: ((_f = opts.copyBindings) != null ? _f : true) ? src.bind_id : void 0,
7667
8242
  options: clonedOptions
7668
8243
  };
7669
- const optionIdMap = /* @__PURE__ */ new Map();
7670
- ((_g = src.options) != null ? _g : []).forEach((o, i) => {
7671
- var _a2, _b2;
7672
- const newOptId = (_b2 = (_a2 = clonedOptions[i]) == null ? void 0 : _a2.id) != null ? _b2 : o.id;
7673
- optionIdMap.set(o.id, newOptId);
7674
- });
7675
8244
  ctx.patchProps((p) => {
7676
- var _a2, _b2, _c2, _d2, _e2, _f2, _g2;
8245
+ var _a2, _b2, _c2, _d2, _e2, _f2, _g;
7677
8246
  const arr = (_a2 = p.fields) != null ? _a2 : [];
7678
8247
  const idx = arr.findIndex((f) => f.id === fieldId);
7679
8248
  arr.splice(idx + 1, 0, cloned);
@@ -7711,52 +8280,56 @@ function duplicateField(ctx, fieldId, opts) {
7711
8280
  }
7712
8281
  if (optionIdMap.has(key)) {
7713
8282
  const newKey = optionIdMap.get(key);
7714
- const merged = /* @__PURE__ */ new Set([...(_g2 = nextMap[newKey]) != null ? _g2 : [], ...targets]);
8283
+ const merged = /* @__PURE__ */ new Set([...(_g = nextMap[newKey]) != null ? _g : [], ...targets]);
7715
8284
  nextMap[newKey] = Array.from(merged);
7716
8285
  }
7717
8286
  }
7718
8287
  p[mapKey] = nextMap;
7719
8288
  }
8289
+ copyOptionEffects(p, {
8290
+ triggerIdMap: new Map([
8291
+ [fieldId, id],
8292
+ ...Array.from(optionIdMap.entries())
8293
+ ]),
8294
+ targetFieldIdMap: /* @__PURE__ */ new Map([[fieldId, id]]),
8295
+ optionIdMap
8296
+ });
7720
8297
  }
7721
8298
  });
7722
8299
  return id;
7723
8300
  }
7724
8301
  function duplicateOption(ctx, fieldId, optionId, opts) {
7725
- var _a, _b, _c, _d, _e, _f;
7726
8302
  const props = ctx.getProps();
7727
- const fields = (_a = props.fields) != null ? _a : [];
7728
- const f = fields.find((x) => x.id === fieldId);
7729
- if (!f) throw new Error(`Field not found: ${fieldId}`);
7730
- const optIdx = ((_b = f.options) != null ? _b : []).findIndex((o) => o.id === optionId);
7731
- if (optIdx < 0) {
7732
- throw new Error(`Option not found: ${fieldId}::${optionId}`);
8303
+ const location = findMutableOption(props, optionId);
8304
+ if (!location || location.field.id !== fieldId) {
8305
+ throw new Error(`Option not found: ${fieldId}/${optionId}`);
7733
8306
  }
7734
- const src = ((_c = f.options) != null ? _c : [])[optIdx];
7735
- const newId2 = ctx.uniqueOptionId(
7736
- fieldId,
7737
- ((_d = opts.optionIdStrategy) != null ? _d : defaultOptionIdStrategy)(src.id)
7738
- );
7739
- const newLabel = ((_e = opts.labelStrategy) != null ? _e : nextCopyLabel)((_f = src.label) != null ? _f : src.id);
8307
+ const src = location.option;
8308
+ const optionIdMap = /* @__PURE__ */ new Map();
8309
+ const clone2 = cloneOptionTree(ctx, fieldId, src, opts, optionIdMap);
8310
+ const newId2 = clone2.id;
7740
8311
  ctx.patchProps((p) => {
7741
- var _a2, _b2, _c2;
7742
- const fld = ((_a2 = p.fields) != null ? _a2 : []).find((x) => x.id === fieldId);
7743
- const arr = (_b2 = fld.options) != null ? _b2 : [];
7744
- const clone2 = { ...src, id: newId2, label: newLabel };
7745
- arr.splice(optIdx + 1, 0, clone2);
7746
- fld.options = arr;
8312
+ var _a;
8313
+ const current = findMutableOption(p, optionId);
8314
+ if (!current) return;
8315
+ current.siblings.splice(current.index + 1, 0, clone2);
7747
8316
  if (opts.copyOptionMaps) {
7748
- const oldKey = `${fieldId}::${optionId}`;
7749
- const newKey = `${fieldId}::${newId2}`;
7750
8317
  for (const mapKey of [
7751
8318
  "includes_for_buttons",
7752
8319
  "excludes_for_buttons"
7753
8320
  ]) {
7754
- const m = (_c2 = p[mapKey]) != null ? _c2 : {};
7755
- if (m[oldKey]) {
7756
- m[newKey] = Array.from(new Set(m[oldKey]));
7757
- p[mapKey] = m;
8321
+ const m = (_a = p[mapKey]) != null ? _a : {};
8322
+ for (const [oldKey, newKey] of optionIdMap.entries()) {
8323
+ if (m[oldKey]) {
8324
+ m[newKey] = Array.from(new Set(m[oldKey]));
8325
+ p[mapKey] = m;
8326
+ }
7758
8327
  }
7759
8328
  }
8329
+ copyOptionEffects(p, {
8330
+ triggerIdMap: optionIdMap,
8331
+ optionIdMap
8332
+ });
7760
8333
  }
7761
8334
  });
7762
8335
  return newId2;
@@ -7818,54 +8391,6 @@ function removeNotice(ctx, id) {
7818
8391
 
7819
8392
  // src/react/canvas/editor/editor-nodes.ts
7820
8393
  import { cloneDeep as cloneDeep3 } from "lodash-es";
7821
-
7822
- // src/react/canvas/editor/editor-utils.ts
7823
- function ownerOfOption(props, optionId) {
7824
- var _a, _b;
7825
- for (const f of (_a = props.fields) != null ? _a : []) {
7826
- const idx = ((_b = f.options) != null ? _b : []).findIndex((o) => o.id === optionId);
7827
- if (idx >= 0) return { fieldId: f.id, index: idx };
7828
- }
7829
- return null;
7830
- }
7831
- function hasFieldOptions(field) {
7832
- return Array.isArray(field == null ? void 0 : field.options) && field.options.length > 0;
7833
- }
7834
- function isActualButtonField(field) {
7835
- return (field == null ? void 0 : field.button) === true && !hasFieldOptions(field);
7836
- }
7837
- function clearFieldButtonReceiverMaps(props, fieldId) {
7838
- var _a, _b;
7839
- if ((_a = props.includes_for_buttons) == null ? void 0 : _a[fieldId]) {
7840
- delete props.includes_for_buttons[fieldId];
7841
- }
7842
- if ((_b = props.excludes_for_buttons) == null ? void 0 : _b[fieldId]) {
7843
- delete props.excludes_for_buttons[fieldId];
7844
- }
7845
- if (props.includes_for_buttons && Object.keys(props.includes_for_buttons).length === 0) {
7846
- delete props.includes_for_buttons;
7847
- }
7848
- if (props.excludes_for_buttons && Object.keys(props.excludes_for_buttons).length === 0) {
7849
- delete props.excludes_for_buttons;
7850
- }
7851
- }
7852
- function ensureServiceExists(opts, id) {
7853
- if (typeof opts.serviceExists === "function") {
7854
- if (!opts.serviceExists(id)) {
7855
- throw new Error(`service_not_found:${String(id)}`);
7856
- }
7857
- return;
7858
- }
7859
- if (opts.serviceMap) {
7860
- if (!Object.prototype.hasOwnProperty.call(opts.serviceMap, id)) {
7861
- throw new Error(`service_not_found:${String(id)}`);
7862
- }
7863
- return;
7864
- }
7865
- throw new Error("service_checker_missing");
7866
- }
7867
-
7868
- // src/react/canvas/editor/editor-nodes.ts
7869
8394
  var RELATION_MAP_KEYS = [
7870
8395
  "includes_for_buttons",
7871
8396
  "excludes_for_buttons",
@@ -7925,6 +8450,43 @@ function cleanRelationMapsForDeleted(p, deleted) {
7925
8450
  if (!Object.keys(map).length) delete p[key];
7926
8451
  }
7927
8452
  }
8453
+ function cleanOptionEffectsForDeleted(p, deleted) {
8454
+ var _a, _b;
8455
+ const map = p.option_effects_for_buttons;
8456
+ if (!map) return;
8457
+ for (const triggerId of Object.keys(map)) {
8458
+ if (deleted.has(String(triggerId))) {
8459
+ delete map[triggerId];
8460
+ continue;
8461
+ }
8462
+ const targets = map[triggerId];
8463
+ for (const targetFieldId of Object.keys(targets != null ? targets : {})) {
8464
+ if (deleted.has(String(targetFieldId))) {
8465
+ delete targets[targetFieldId];
8466
+ continue;
8467
+ }
8468
+ const effect = targets[targetFieldId];
8469
+ if (!effect) continue;
8470
+ if (effect.include) {
8471
+ effect.include = effect.include.filter(
8472
+ (optionId) => !deleted.has(String(optionId))
8473
+ );
8474
+ if (!effect.include.length) delete effect.include;
8475
+ }
8476
+ if (effect.exclude) {
8477
+ effect.exclude = effect.exclude.filter(
8478
+ (optionId) => !deleted.has(String(optionId))
8479
+ );
8480
+ if (!effect.exclude.length) delete effect.exclude;
8481
+ }
8482
+ if (effect.forceVisible !== true && !((_a = effect.include) == null ? void 0 : _a.length) && !((_b = effect.exclude) == null ? void 0 : _b.length)) {
8483
+ delete targets[targetFieldId];
8484
+ }
8485
+ }
8486
+ if (!Object.keys(targets != null ? targets : {}).length) delete map[triggerId];
8487
+ }
8488
+ if (!Object.keys(map).length) delete p.option_effects_for_buttons;
8489
+ }
7928
8490
  function cleanOrderForTagsForDeleted(p, deleted) {
7929
8491
  var _a, _b;
7930
8492
  const map = p.order_for_tags;
@@ -7960,28 +8522,37 @@ function applyDeleteCleanup(p, deleted) {
7960
8522
  cleanTagRelationsForDeleted(p, deleted);
7961
8523
  cleanFieldBindsForDeleted(p, deleted);
7962
8524
  cleanRelationMapsForDeleted(p, deleted);
8525
+ cleanOptionEffectsForDeleted(p, deleted);
7963
8526
  cleanOrderForTagsForDeleted(p, deleted);
7964
8527
  cleanNoticesForDeleted(p, deleted);
7965
8528
  }
8529
+ function collectOptionSubtreeIds(option) {
8530
+ var _a;
8531
+ return [
8532
+ String(option.id),
8533
+ ...((_a = option.children) != null ? _a : []).flatMap((child) => collectOptionSubtreeIds(child))
8534
+ ];
8535
+ }
7966
8536
  function removeOptionInPlace(p, optionId) {
7967
8537
  var _a;
7968
- const owner = ownerOfOption(p, optionId);
7969
- if (!owner) return false;
7970
- const f = ((_a = p.fields) != null ? _a : []).find((x) => x.id === owner.fieldId);
7971
- if (!(f == null ? void 0 : f.options)) return false;
7972
- const before = f.options.length;
7973
- f.options = f.options.filter((o) => o.id !== optionId);
7974
- return f.options.length !== before;
8538
+ const found = findMutableOption(p, optionId);
8539
+ if (!found) return [];
8540
+ const deleted = collectOptionSubtreeIds(found.option);
8541
+ found.siblings.splice(found.index, 1);
8542
+ if (found.parent && ((_a = found.parent.children) == null ? void 0 : _a.length) === 0) {
8543
+ delete found.parent.children;
8544
+ }
8545
+ return deleted;
7975
8546
  }
7976
8547
  function removeFieldInPlace(p, fieldId) {
7977
- var _a, _b, _c, _d, _e;
8548
+ var _a, _b, _c, _d;
7978
8549
  const field = ((_a = p.fields) != null ? _a : []).find((f) => f.id === fieldId);
7979
8550
  if (!field) return [];
7980
- const deleted = [fieldId, ...((_b = field.options) != null ? _b : []).map((o) => String(o.id))];
7981
- const before = ((_c = p.fields) != null ? _c : []).length;
7982
- p.fields = ((_d = p.fields) != null ? _d : []).filter((f) => f.id !== fieldId);
8551
+ const deleted = [fieldId, ...collectFieldOptionIds(field)];
8552
+ const before = ((_b = p.fields) != null ? _b : []).length;
8553
+ p.fields = ((_c = p.fields) != null ? _c : []).filter((f) => f.id !== fieldId);
7983
8554
  clearFieldButtonReceiverMaps(p, fieldId);
7984
- return ((_e = p.fields) != null ? _e : []).length !== before ? deleted : [];
8555
+ return ((_d = p.fields) != null ? _d : []).length !== before ? deleted : [];
7985
8556
  }
7986
8557
  function removeTagInPlace(p, tagId) {
7987
8558
  var _a, _b, _c;
@@ -7994,7 +8565,7 @@ function reLabel(ctx, id, nextLabel) {
7994
8565
  ctx.exec({
7995
8566
  name: "reLabel",
7996
8567
  do: () => ctx.patchProps((p) => {
7997
- var _a, _b, _c, _d, _e, _f, _g;
8568
+ var _a, _b, _c, _d, _e, _f;
7998
8569
  if (ctx.isTagId(id)) {
7999
8570
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
8000
8571
  if (!t) return;
@@ -8004,19 +8575,16 @@ function reLabel(ctx, id, nextLabel) {
8004
8575
  return;
8005
8576
  }
8006
8577
  if (ctx.isOptionId(id)) {
8007
- const own = ownerOfOption(p, id);
8008
- if (!own) return;
8009
- const f = ((_c = p.fields) != null ? _c : []).find((x) => x.id === own.fieldId);
8010
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
8578
+ const o = (_c = findMutableOption(p, id)) == null ? void 0 : _c.option;
8011
8579
  if (!o) return;
8012
- if (((_e = o.label) != null ? _e : "") === label) return;
8580
+ if (((_d = o.label) != null ? _d : "") === label) return;
8013
8581
  o.label = label;
8014
8582
  ctx.api.refreshGraph();
8015
8583
  return;
8016
8584
  }
8017
- const fld = ((_f = p.fields) != null ? _f : []).find((x) => x.id === id);
8585
+ const fld = ((_e = p.fields) != null ? _e : []).find((x) => x.id === id);
8018
8586
  if (!fld) return;
8019
- if (((_g = fld.label) != null ? _g : "") === label) return;
8587
+ if (((_f = fld.label) != null ? _f : "") === label) return;
8020
8588
  fld.label = label;
8021
8589
  ctx.api.refreshGraph();
8022
8590
  }),
@@ -8106,11 +8674,7 @@ function updateOption(ctx, optionId, patch) {
8106
8674
  name: "updateOption",
8107
8675
  do: () => ctx.patchProps((p) => {
8108
8676
  var _a;
8109
- const owner = ownerOfOption(p, optionId);
8110
- if (!owner) return;
8111
- const f = ((_a = p.fields) != null ? _a : []).find((x) => x.id === owner.fieldId);
8112
- if (!(f == null ? void 0 : f.options)) return;
8113
- const o = f.options.find((x) => x.id === optionId);
8677
+ const o = (_a = findMutableOption(p, optionId)) == null ? void 0 : _a.option;
8114
8678
  if (o) Object.assign(o, patch);
8115
8679
  }),
8116
8680
  undo: () => ctx.undo()
@@ -8123,9 +8687,9 @@ function removeOption(ctx, optionId) {
8123
8687
  ctx.exec({
8124
8688
  name: "removeOption",
8125
8689
  do: () => ctx.patchProps((p) => {
8126
- const removed = removeOptionInPlace(p, optionId);
8127
- if (!removed) return;
8128
- applyDeleteCleanup(p, /* @__PURE__ */ new Set([optionId]));
8690
+ const removedIds = removeOptionInPlace(p, optionId);
8691
+ if (!removedIds.length) return;
8692
+ applyDeleteCleanup(p, new Set(removedIds));
8129
8693
  }),
8130
8694
  undo: () => ctx.undo()
8131
8695
  });
@@ -8136,7 +8700,7 @@ function editLabel(ctx, id, label) {
8136
8700
  ctx.exec({
8137
8701
  name: "editLabel",
8138
8702
  do: () => ctx.patchProps((p) => {
8139
- var _a, _b, _c, _d;
8703
+ var _a, _b, _c;
8140
8704
  if (ctx.isTagId(id)) {
8141
8705
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
8142
8706
  if (t) t.label = next;
@@ -8148,10 +8712,7 @@ function editLabel(ctx, id, label) {
8148
8712
  return;
8149
8713
  }
8150
8714
  if (ctx.isOptionId(id)) {
8151
- const own = ownerOfOption(p, id);
8152
- if (!own) return;
8153
- const f = ((_c = p.fields) != null ? _c : []).find((x) => x.id === own.fieldId);
8154
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
8715
+ const o = (_c = findMutableOption(p, id)) == null ? void 0 : _c.option;
8155
8716
  if (o) o.label = next;
8156
8717
  return;
8157
8718
  }
@@ -8176,7 +8737,7 @@ function setService(ctx, id, input) {
8176
8737
  ctx.exec({
8177
8738
  name: "setService",
8178
8739
  do: () => ctx.patchProps((p) => {
8179
- var _a, _b, _c, _d, _e, _f;
8740
+ var _a, _b, _c, _d, _e;
8180
8741
  const hasSidKey = Object.prototype.hasOwnProperty.call(
8181
8742
  input,
8182
8743
  "service_id"
@@ -8194,12 +8755,9 @@ function setService(ctx, id, input) {
8194
8755
  return;
8195
8756
  }
8196
8757
  if (ctx.isOptionId(id)) {
8197
- const own = ownerOfOption(p, id);
8198
- if (!own) return;
8199
- const f2 = ((_b = p.fields) != null ? _b : []).find((x) => x.id === own.fieldId);
8200
- const o = (_c = f2 == null ? void 0 : f2.options) == null ? void 0 : _c.find((x) => x.id === id);
8758
+ const o = (_b = findMutableOption(p, id)) == null ? void 0 : _b.option;
8201
8759
  if (!o) return;
8202
- const currentRole = (_d = o.pricing_role) != null ? _d : "base";
8760
+ const currentRole = (_c = o.pricing_role) != null ? _c : "base";
8203
8761
  const role = nextRole != null ? nextRole : currentRole;
8204
8762
  if (role === "utility") {
8205
8763
  if (hasSidKey && sid !== void 0) {
@@ -8220,7 +8778,7 @@ function setService(ctx, id, input) {
8220
8778
  }
8221
8779
  return;
8222
8780
  }
8223
- const f = ((_e = p.fields) != null ? _e : []).find((x) => x.id === id);
8781
+ const f = ((_d = p.fields) != null ? _d : []).find((x) => x.id === id);
8224
8782
  if (!f) {
8225
8783
  throw new Error(
8226
8784
  'setService only supports tag ("t:*"), option ("o:*"), or field ("f:*") ids'
@@ -8231,7 +8789,7 @@ function setService(ctx, id, input) {
8231
8789
  if (nextRole) {
8232
8790
  f.pricing_role = nextRole;
8233
8791
  }
8234
- const effectiveRole = (_f = f.pricing_role) != null ? _f : "base";
8792
+ const effectiveRole = (_e = f.pricing_role) != null ? _e : "base";
8235
8793
  if (isOptionBased) {
8236
8794
  if (hasSidKey) {
8237
8795
  ctx.api.emit("error", {
@@ -8343,13 +8901,15 @@ function updateField(ctx, id, patch) {
8343
8901
  let prev;
8344
8902
  let prevIncludesForButton;
8345
8903
  let prevExcludesForButton;
8904
+ let prevOptionEffectsForButton;
8346
8905
  ctx.exec({
8347
8906
  name: "updateField",
8348
8907
  do: () => ctx.patchProps((p) => {
8349
- var _a, _b, _c, _d, _e, _f, _g;
8908
+ var _a, _b, _c, _d, _e, _f, _g, _h;
8350
8909
  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;
8351
8910
  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;
8352
- p.fields = ((_g = p.fields) != null ? _g : []).map((f) => {
8911
+ prevOptionEffectsForButton = ((_g = p.option_effects_for_buttons) == null ? void 0 : _g[id]) ? cloneDeep3(p.option_effects_for_buttons[id]) : void 0;
8912
+ p.fields = ((_h = p.fields) != null ? _h : []).map((f) => {
8353
8913
  if (f.id !== id) return f;
8354
8914
  prev = cloneDeep3(f);
8355
8915
  const nextField = { ...f, ...patch };
@@ -8360,7 +8920,7 @@ function updateField(ctx, id, patch) {
8360
8920
  });
8361
8921
  }),
8362
8922
  undo: () => ctx.patchProps((p) => {
8363
- var _a, _b, _c;
8923
+ var _a, _b, _c, _d;
8364
8924
  p.fields = ((_a = p.fields) != null ? _a : []).map(
8365
8925
  (f) => f.id === id && prev ? prev : f
8366
8926
  );
@@ -8378,6 +8938,12 @@ function updateField(ctx, id, patch) {
8378
8938
  [id]: [...prevExcludesForButton]
8379
8939
  };
8380
8940
  }
8941
+ if (prevOptionEffectsForButton) {
8942
+ p.option_effects_for_buttons = {
8943
+ ...(_d = p.option_effects_for_buttons) != null ? _d : {},
8944
+ [id]: cloneDeep3(prevOptionEffectsForButton)
8945
+ };
8946
+ }
8381
8947
  })
8382
8948
  });
8383
8949
  }
@@ -8424,9 +8990,9 @@ function remove(ctx, id) {
8424
8990
  ctx.exec({
8425
8991
  name: "removeOption",
8426
8992
  do: () => ctx.patchProps((p) => {
8427
- const removed = removeOptionInPlace(p, key);
8428
- if (!removed) return;
8429
- applyDeleteCleanup(p, /* @__PURE__ */ new Set([key]));
8993
+ const removedIds = removeOptionInPlace(p, key);
8994
+ if (!removedIds.length) return;
8995
+ applyDeleteCleanup(p, new Set(removedIds));
8430
8996
  }),
8431
8997
  undo: () => ctx.undo()
8432
8998
  });
@@ -8443,10 +9009,7 @@ function removeMany(ctx, ids) {
8443
9009
  const existingFieldIds = new Set(((_a = p.fields) != null ? _a : []).map((f) => String(f.id)));
8444
9010
  const existingTagIds = new Set(((_b = p.filters) != null ? _b : []).map((t) => String(t.id)));
8445
9011
  const existingOptionIds = new Set(
8446
- ((_c = p.fields) != null ? _c : []).flatMap((f) => {
8447
- var _a2;
8448
- return ((_a2 = f.options) != null ? _a2 : []).map((o) => String(o.id));
8449
- })
9012
+ ((_c = p.fields) != null ? _c : []).flatMap((f) => collectFieldOptionIds(f))
8450
9013
  );
8451
9014
  const fieldIds = ordered.filter((id) => ctx.isFieldId(id) && existingFieldIds.has(id));
8452
9015
  const fieldIdSet = new Set(fieldIds);
@@ -8459,7 +9022,9 @@ function removeMany(ctx, ids) {
8459
9022
  });
8460
9023
  const deleted = /* @__PURE__ */ new Set();
8461
9024
  for (const optionId of optionIds) {
8462
- if (removeOptionInPlace(p, optionId)) deleted.add(optionId);
9025
+ for (const removedId of removeOptionInPlace(p, optionId)) {
9026
+ deleted.add(removedId);
9027
+ }
8463
9028
  }
8464
9029
  for (const fieldId of fieldIds) {
8465
9030
  const removedIds = removeFieldInPlace(p, fieldId);
@@ -8474,7 +9039,7 @@ function removeMany(ctx, ids) {
8474
9039
  });
8475
9040
  }
8476
9041
  function getNode(ctx, id) {
8477
- var _a, _b, _c, _d;
9042
+ var _a, _b, _c;
8478
9043
  const props = ctx.getProps();
8479
9044
  if (ctx.isTagId(id)) {
8480
9045
  const t = ((_a = props.filters) != null ? _a : []).find((x) => x.id === id);
@@ -8491,8 +9056,7 @@ function getNode(ctx, id) {
8491
9056
  }
8492
9057
  if (ctx.isOptionId(id)) {
8493
9058
  const own = ownerOfOption(props, id);
8494
- const f = own ? ((_c = props.fields) != null ? _c : []).find((x) => x.id === own.fieldId) : void 0;
8495
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
9059
+ const o = (_c = findMutableOption(props, id)) == null ? void 0 : _c.option;
8496
9060
  return {
8497
9061
  kind: "option",
8498
9062
  data: o,
@@ -9029,7 +9593,7 @@ function connect(ctx, kind, fromId, toId2) {
9029
9593
  ctx.exec({
9030
9594
  name: `connect:${kind}`,
9031
9595
  do: () => ctx.patchProps((p) => {
9032
- var _a, _b, _c, _d, _e, _f, _g, _h;
9596
+ var _a, _b, _c, _d, _e, _f, _g;
9033
9597
  if (kind === "bind") {
9034
9598
  if (ctx.isTagId(fromId) && ctx.isTagId(toId2)) {
9035
9599
  if (wouldCreateTagCycle(ctx, p, fromId, toId2)) {
@@ -9109,12 +9673,10 @@ function connect(ctx, kind, fromId, toId2) {
9109
9673
  return;
9110
9674
  }
9111
9675
  if (toId2.startsWith("o:")) {
9112
- for (const f of (_g = p.fields) != null ? _g : []) {
9113
- const o = (_h = f.options) == null ? void 0 : _h.find((x) => x.id === toId2);
9114
- if (o) {
9115
- o.service_id = fromId;
9116
- return;
9117
- }
9676
+ const o = (_g = findMutableOption(p, toId2)) == null ? void 0 : _g.option;
9677
+ if (o) {
9678
+ o.service_id = fromId;
9679
+ return;
9118
9680
  }
9119
9681
  return;
9120
9682
  }
@@ -9131,7 +9693,7 @@ function disconnect(ctx, kind, fromId, toId2) {
9131
9693
  ctx.exec({
9132
9694
  name: `disconnect:${kind}`,
9133
9695
  do: () => ctx.patchProps((p) => {
9134
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
9696
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
9135
9697
  if (kind === "bind") {
9136
9698
  if (ctx.isTagId(fromId) && ctx.isTagId(toId2)) {
9137
9699
  const child = ((_a = p.filters) != null ? _a : []).find(
@@ -9207,12 +9769,10 @@ function disconnect(ctx, kind, fromId, toId2) {
9207
9769
  return;
9208
9770
  }
9209
9771
  if (toId2.startsWith("o:")) {
9210
- for (const f of (_i = p.fields) != null ? _i : []) {
9211
- const o = (_j = f.options) == null ? void 0 : _j.find((x) => x.id === toId2);
9212
- if (o) {
9213
- delete o.service_id;
9214
- return;
9215
- }
9772
+ const o = (_i = findMutableOption(p, toId2)) == null ? void 0 : _i.option;
9773
+ if (o) {
9774
+ delete o.service_id;
9775
+ return;
9216
9776
  }
9217
9777
  return;
9218
9778
  }
@@ -9235,6 +9795,250 @@ function addMappedField(p, mapKey, fromId, toId2) {
9235
9795
  p[mapKey] = maps;
9236
9796
  }
9237
9797
 
9798
+ // src/react/canvas/editor/editor-option-effects.ts
9799
+ function assertCanonicalId(id, label) {
9800
+ if (!id || id.includes("::") || id.includes("/")) {
9801
+ throw new Error(
9802
+ `${label}: expected a raw field or option id, not a composite/path id`
9803
+ );
9804
+ }
9805
+ }
9806
+ function assertTrigger(ctx, triggerId) {
9807
+ assertCanonicalId(triggerId, "option effect trigger");
9808
+ const trigger = ctx.getNode(triggerId);
9809
+ if (trigger.kind === "option" && trigger.data) return;
9810
+ if (trigger.kind === "field" && trigger.data && isActualButtonField(trigger.data)) {
9811
+ return;
9812
+ }
9813
+ throw new Error(
9814
+ "option effect trigger must be an option id or button field id"
9815
+ );
9816
+ }
9817
+ function assertTargetField(props, targetFieldId) {
9818
+ var _a;
9819
+ assertCanonicalId(targetFieldId, "option effect target");
9820
+ const field = ((_a = props.fields) != null ? _a : []).find((item) => item.id === targetFieldId);
9821
+ if (!field) {
9822
+ throw new Error(`option effect target field not found: ${targetFieldId}`);
9823
+ }
9824
+ return field;
9825
+ }
9826
+ function dedupe2(values) {
9827
+ if (!values) return void 0;
9828
+ const out = [];
9829
+ for (const value of values) {
9830
+ const id = String(value);
9831
+ if (!id || out.includes(id)) continue;
9832
+ out.push(id);
9833
+ }
9834
+ return out.length ? out : void 0;
9835
+ }
9836
+ function assertTargetOptions(props, targetFieldId, ids, kind) {
9837
+ if (!(ids == null ? void 0 : ids.length)) return;
9838
+ const field = assertTargetField(props, targetFieldId);
9839
+ const valid = fieldOptionIdSet(field);
9840
+ for (const id of ids) {
9841
+ assertCanonicalId(String(id), `option effect ${kind} option`);
9842
+ if (!valid.has(String(id))) {
9843
+ throw new Error(
9844
+ `option effect ${kind} option not found under ${targetFieldId}: ${String(id)}`
9845
+ );
9846
+ }
9847
+ }
9848
+ }
9849
+ function normalizeEffect(effect) {
9850
+ var _a;
9851
+ if (!effect) return void 0;
9852
+ const exclude2 = dedupe2(effect.exclude);
9853
+ const excluded = new Set(exclude2 != null ? exclude2 : []);
9854
+ const include2 = (_a = dedupe2(effect.include)) == null ? void 0 : _a.filter((id) => !excluded.has(id));
9855
+ const out = {};
9856
+ if (effect.forceVisible === true) out.forceVisible = true;
9857
+ if (include2 == null ? void 0 : include2.length) out.include = include2;
9858
+ if (exclude2 == null ? void 0 : exclude2.length) out.exclude = exclude2;
9859
+ return Object.keys(out).length ? out : void 0;
9860
+ }
9861
+ function ensureTargetMap(props, triggerId) {
9862
+ var _a, _b, _c;
9863
+ (_a = props.option_effects_for_buttons) != null ? _a : props.option_effects_for_buttons = {};
9864
+ (_c = (_b = props.option_effects_for_buttons)[triggerId]) != null ? _c : _b[triggerId] = {};
9865
+ return props.option_effects_for_buttons[triggerId];
9866
+ }
9867
+ function pruneEffectMap(props, triggerId) {
9868
+ const map = props.option_effects_for_buttons;
9869
+ if (!map) return;
9870
+ const keys = triggerId ? [triggerId] : Object.keys(map);
9871
+ for (const key of keys) {
9872
+ const targets = map[key];
9873
+ if (!targets || Object.keys(targets).length === 0) delete map[key];
9874
+ }
9875
+ if (Object.keys(map).length === 0) delete props.option_effects_for_buttons;
9876
+ }
9877
+ function validateEffect(ctx, props, triggerId, targetFieldId, effect) {
9878
+ assertTrigger(ctx, triggerId);
9879
+ assertTargetField(props, targetFieldId);
9880
+ assertTargetOptions(props, targetFieldId, effect == null ? void 0 : effect.include, "include");
9881
+ assertTargetOptions(props, targetFieldId, effect == null ? void 0 : effect.exclude, "exclude");
9882
+ return normalizeEffect(effect);
9883
+ }
9884
+ function setOptionEffect(ctx, triggerId, targetFieldId, effect) {
9885
+ ctx.exec({
9886
+ name: "setOptionEffect",
9887
+ do: () => ctx.patchProps((props) => {
9888
+ var _a;
9889
+ const normalized = validateEffect(
9890
+ ctx,
9891
+ props,
9892
+ triggerId,
9893
+ targetFieldId,
9894
+ effect
9895
+ );
9896
+ if (!normalized) {
9897
+ const map = (_a = props.option_effects_for_buttons) == null ? void 0 : _a[triggerId];
9898
+ if (map) delete map[targetFieldId];
9899
+ pruneEffectMap(props, triggerId);
9900
+ return;
9901
+ }
9902
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
9903
+ }),
9904
+ undo: () => ctx.undo()
9905
+ });
9906
+ }
9907
+ function patchOptionEffect(ctx, triggerId, targetFieldId, patch) {
9908
+ ctx.exec({
9909
+ name: "patchOptionEffect",
9910
+ do: () => ctx.patchProps((props) => {
9911
+ var _a, _b, _c, _d;
9912
+ const current = (_c = (_b = (_a = props.option_effects_for_buttons) == null ? void 0 : _a[triggerId]) == null ? void 0 : _b[targetFieldId]) != null ? _c : {};
9913
+ const merged = {
9914
+ ...current,
9915
+ ...patch
9916
+ };
9917
+ const normalized = validateEffect(
9918
+ ctx,
9919
+ props,
9920
+ triggerId,
9921
+ targetFieldId,
9922
+ merged
9923
+ );
9924
+ if (!normalized) {
9925
+ const map = (_d = props.option_effects_for_buttons) == null ? void 0 : _d[triggerId];
9926
+ if (map) delete map[targetFieldId];
9927
+ pruneEffectMap(props, triggerId);
9928
+ return;
9929
+ }
9930
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
9931
+ }),
9932
+ undo: () => ctx.undo()
9933
+ });
9934
+ }
9935
+ function clearOptionEffect(ctx, triggerId, targetFieldId) {
9936
+ ctx.exec({
9937
+ name: "clearOptionEffect",
9938
+ do: () => ctx.patchProps((props) => {
9939
+ var _a;
9940
+ const map = (_a = props.option_effects_for_buttons) == null ? void 0 : _a[triggerId];
9941
+ if (!map) return;
9942
+ delete map[targetFieldId];
9943
+ pruneEffectMap(props, triggerId);
9944
+ }),
9945
+ undo: () => ctx.undo()
9946
+ });
9947
+ }
9948
+ function clearOptionEffectsForTrigger(ctx, triggerId) {
9949
+ ctx.exec({
9950
+ name: "clearOptionEffectsForTrigger",
9951
+ do: () => ctx.patchProps((props) => {
9952
+ if (!props.option_effects_for_buttons) return;
9953
+ delete props.option_effects_for_buttons[triggerId];
9954
+ pruneEffectMap(props);
9955
+ }),
9956
+ undo: () => ctx.undo()
9957
+ });
9958
+ }
9959
+ function clearOptionEffectsForTarget(ctx, targetFieldId) {
9960
+ ctx.exec({
9961
+ name: "clearOptionEffectsForTarget",
9962
+ do: () => ctx.patchProps((props) => {
9963
+ var _a;
9964
+ const map = props.option_effects_for_buttons;
9965
+ if (!map) return;
9966
+ for (const triggerId of Object.keys(map)) {
9967
+ (_a = map[triggerId]) == null ? true : delete _a[targetFieldId];
9968
+ }
9969
+ pruneEffectMap(props);
9970
+ }),
9971
+ undo: () => ctx.undo()
9972
+ });
9973
+ }
9974
+ function addOptionEffectOptions(ctx, triggerId, targetFieldId, kind, optionIds) {
9975
+ var _a;
9976
+ const additions = (_a = dedupe2(optionIds)) != null ? _a : [];
9977
+ if (!additions.length) return;
9978
+ ctx.exec({
9979
+ name: "addOptionEffectOptions",
9980
+ do: () => ctx.patchProps((props) => {
9981
+ var _a2, _b, _c, _d;
9982
+ const current = (_c = (_b = (_a2 = props.option_effects_for_buttons) == null ? void 0 : _a2[triggerId]) == null ? void 0 : _b[targetFieldId]) != null ? _c : {};
9983
+ const nextValues = dedupe2([
9984
+ ...(_d = current[kind]) != null ? _d : [],
9985
+ ...additions
9986
+ ]);
9987
+ const normalized = validateEffect(
9988
+ ctx,
9989
+ props,
9990
+ triggerId,
9991
+ targetFieldId,
9992
+ {
9993
+ ...current,
9994
+ [kind]: nextValues
9995
+ }
9996
+ );
9997
+ if (!normalized) return;
9998
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
9999
+ }),
10000
+ undo: () => ctx.undo()
10001
+ });
10002
+ }
10003
+ function removeOptionEffectOptions(ctx, triggerId, targetFieldId, kind, optionIds) {
10004
+ var _a;
10005
+ const removals = new Set((_a = dedupe2(optionIds)) != null ? _a : []);
10006
+ if (!removals.size) return;
10007
+ ctx.exec({
10008
+ name: "removeOptionEffectOptions",
10009
+ do: () => ctx.patchProps((props) => {
10010
+ var _a2, _b, _c, _d, _e;
10011
+ const current = (_b = (_a2 = props.option_effects_for_buttons) == null ? void 0 : _a2[triggerId]) == null ? void 0 : _b[targetFieldId];
10012
+ if (!current) return;
10013
+ const next = {
10014
+ ...current,
10015
+ [kind]: ((_c = current[kind]) != null ? _c : []).filter(
10016
+ (optionId) => !removals.has(optionId)
10017
+ )
10018
+ };
10019
+ const normalized = validateEffect(
10020
+ ctx,
10021
+ props,
10022
+ triggerId,
10023
+ targetFieldId,
10024
+ next
10025
+ );
10026
+ if (!normalized) {
10027
+ (_e = (_d = props.option_effects_for_buttons) == null ? void 0 : _d[triggerId]) == null ? true : delete _e[targetFieldId];
10028
+ pruneEffectMap(props, triggerId);
10029
+ return;
10030
+ }
10031
+ ensureTargetMap(props, triggerId)[targetFieldId] = normalized;
10032
+ }),
10033
+ undo: () => ctx.undo()
10034
+ });
10035
+ }
10036
+ function setOptionEffectForceVisible(ctx, triggerId, targetFieldId, forceVisible) {
10037
+ patchOptionEffect(ctx, triggerId, targetFieldId, {
10038
+ forceVisible: forceVisible === true ? true : void 0
10039
+ });
10040
+ }
10041
+
9238
10042
  // src/react/canvas/editor/editor-service-filter.ts
9239
10043
  function filterServicesForVisibleGroup2(ctx, candidates, input) {
9240
10044
  const coreInput = {
@@ -9821,7 +10625,7 @@ var Editor = class {
9821
10625
  if (!ordered.length) return;
9822
10626
  this.transact("clearServiceMany", () => {
9823
10627
  this.patchProps((p) => {
9824
- var _a, _b, _c, _d;
10628
+ var _a, _b;
9825
10629
  for (const id of ordered) {
9826
10630
  if (this.isTagId(id)) {
9827
10631
  const t = ((_a = p.filters) != null ? _a : []).find((x) => x.id === id);
@@ -9834,10 +10638,8 @@ var Editor = class {
9834
10638
  continue;
9835
10639
  }
9836
10640
  if (this.isOptionId(id)) {
9837
- const own = ownerOfOption(p, id);
9838
- if (!own) continue;
9839
- const f = ((_c = p.fields) != null ? _c : []).find((x) => x.id === own.fieldId);
9840
- const o = (_d = f == null ? void 0 : f.options) == null ? void 0 : _d.find((x) => x.id === id);
10641
+ const found = findMutableOption(p, id);
10642
+ const o = found == null ? void 0 : found.option;
9841
10643
  if (o && "service_id" in o) delete o.service_id;
9842
10644
  }
9843
10645
  }
@@ -9886,7 +10688,7 @@ var Editor = class {
9886
10688
  if (!selected.size) return;
9887
10689
  this.transact("clearRelationsMany", () => {
9888
10690
  this.patchProps((p) => {
9889
- var _a, _b, _c;
10691
+ var _a, _b, _c, _d, _e;
9890
10692
  const clearOwned = mode === "owned" || mode === "both";
9891
10693
  const clearIncoming = mode === "incoming" || mode === "both";
9892
10694
  for (const t of (_a = p.filters) != null ? _a : []) {
@@ -9926,6 +10728,44 @@ var Editor = class {
9926
10728
  }
9927
10729
  if (!Object.keys(map).length) delete p[k];
9928
10730
  }
10731
+ const effectMap = p.option_effects_for_buttons;
10732
+ if (effectMap) {
10733
+ for (const triggerId of Object.keys(effectMap)) {
10734
+ if (clearOwned && selected.has(String(triggerId))) {
10735
+ delete effectMap[triggerId];
10736
+ continue;
10737
+ }
10738
+ const targets = effectMap[triggerId];
10739
+ if (!targets || !clearIncoming) continue;
10740
+ for (const targetFieldId of Object.keys(targets)) {
10741
+ if (selected.has(String(targetFieldId))) {
10742
+ delete targets[targetFieldId];
10743
+ continue;
10744
+ }
10745
+ const effect = targets[targetFieldId];
10746
+ if (!effect) continue;
10747
+ if (effect.include) {
10748
+ effect.include = effect.include.filter(
10749
+ (optionId) => !selected.has(String(optionId))
10750
+ );
10751
+ if (!effect.include.length) delete effect.include;
10752
+ }
10753
+ if (effect.exclude) {
10754
+ effect.exclude = effect.exclude.filter(
10755
+ (optionId) => !selected.has(String(optionId))
10756
+ );
10757
+ if (!effect.exclude.length) delete effect.exclude;
10758
+ }
10759
+ if (effect.forceVisible !== true && !((_d = effect.include) == null ? void 0 : _d.length) && !((_e = effect.exclude) == null ? void 0 : _e.length)) {
10760
+ delete targets[targetFieldId];
10761
+ }
10762
+ }
10763
+ if (!Object.keys(targets).length) delete effectMap[triggerId];
10764
+ }
10765
+ if (!Object.keys(effectMap).length) {
10766
+ delete p.option_effects_for_buttons;
10767
+ }
10768
+ }
9929
10769
  });
9930
10770
  });
9931
10771
  }
@@ -9937,7 +10777,7 @@ var Editor = class {
9937
10777
  const suffix = (_b = input.suffix) != null ? _b : "";
9938
10778
  this.transact("renameLabelsMany", () => {
9939
10779
  this.patchProps((p) => {
9940
- var _a2, _b2, _c, _d, _e, _f, _g;
10780
+ var _a2, _b2, _c, _d, _e, _f;
9941
10781
  for (const id of ordered) {
9942
10782
  if (this.isTagId(id)) {
9943
10783
  const t = ((_a2 = p.filters) != null ? _a2 : []).find((x) => x.id === id);
@@ -9950,11 +10790,8 @@ var Editor = class {
9950
10790
  continue;
9951
10791
  }
9952
10792
  if (this.isOptionId(id)) {
9953
- const own = ownerOfOption(p, id);
9954
- if (!own) continue;
9955
- const f = ((_e = p.fields) != null ? _e : []).find((x) => x.id === own.fieldId);
9956
- const o = (_f = f == null ? void 0 : f.options) == null ? void 0 : _f.find((x) => x.id === id);
9957
- if (o) o.label = `${prefix}${(_g = o.label) != null ? _g : ""}${suffix}`.trim();
10793
+ const o = (_e = findMutableOption(p, id)) == null ? void 0 : _e.option;
10794
+ if (o) o.label = `${prefix}${(_f = o.label) != null ? _f : ""}${suffix}`.trim();
9958
10795
  }
9959
10796
  }
9960
10797
  });
@@ -10107,6 +10944,57 @@ var Editor = class {
10107
10944
  exclude(receiverId, idOrIds) {
10108
10945
  return exclude(this.moduleCtx(), receiverId, idOrIds);
10109
10946
  }
10947
+ setOptionEffect(triggerId, targetFieldId, effect) {
10948
+ return setOptionEffect(
10949
+ this.moduleCtx(),
10950
+ triggerId,
10951
+ targetFieldId,
10952
+ effect
10953
+ );
10954
+ }
10955
+ patchOptionEffect(triggerId, targetFieldId, patch) {
10956
+ return patchOptionEffect(
10957
+ this.moduleCtx(),
10958
+ triggerId,
10959
+ targetFieldId,
10960
+ patch
10961
+ );
10962
+ }
10963
+ clearOptionEffect(triggerId, targetFieldId) {
10964
+ return clearOptionEffect(this.moduleCtx(), triggerId, targetFieldId);
10965
+ }
10966
+ clearOptionEffectsForTrigger(triggerId) {
10967
+ return clearOptionEffectsForTrigger(this.moduleCtx(), triggerId);
10968
+ }
10969
+ clearOptionEffectsForTarget(targetFieldId) {
10970
+ return clearOptionEffectsForTarget(this.moduleCtx(), targetFieldId);
10971
+ }
10972
+ addOptionEffectOptions(triggerId, targetFieldId, kind, optionIds) {
10973
+ return addOptionEffectOptions(
10974
+ this.moduleCtx(),
10975
+ triggerId,
10976
+ targetFieldId,
10977
+ kind,
10978
+ optionIds
10979
+ );
10980
+ }
10981
+ removeOptionEffectOptions(triggerId, targetFieldId, kind, optionIds) {
10982
+ return removeOptionEffectOptions(
10983
+ this.moduleCtx(),
10984
+ triggerId,
10985
+ targetFieldId,
10986
+ kind,
10987
+ optionIds
10988
+ );
10989
+ }
10990
+ setOptionEffectForceVisible(triggerId, targetFieldId, forceVisible) {
10991
+ return setOptionEffectForceVisible(
10992
+ this.moduleCtx(),
10993
+ triggerId,
10994
+ targetFieldId,
10995
+ forceVisible
10996
+ );
10997
+ }
10110
10998
  connect(kind, fromId, toId2) {
10111
10999
  return connect(this.moduleCtx(), kind, fromId, toId2);
10112
11000
  }
@@ -10475,11 +11363,10 @@ var Selection = class {
10475
11363
  * What counts as a "button selection" (trigger key):
10476
11364
  * - field key where the field has button === true (e.g. "f:dripfeed")
10477
11365
  * - option key (e.g. "o:fast")
10478
- * - composite key "fieldId::optionId" (e.g. "f:speed::o:fast")
10479
11366
  *
10480
11367
  * Grouping:
10481
11368
  * - button-field trigger groups under its own fieldId
10482
- * - option/composite groups under the option's owning fieldId (from nodeMap)
11369
+ * - option trigger groups under the option's owning fieldId (from nodeMap)
10483
11370
  *
10484
11371
  * Deterministic:
10485
11372
  * - preserves selection insertion order
@@ -10496,15 +11383,6 @@ var Selection = class {
10496
11383
  };
10497
11384
  for (const key of this.set) {
10498
11385
  if (!key) continue;
10499
- const idx = key.indexOf("::");
10500
- if (idx !== -1) {
10501
- const optionId = key.slice(idx + 2);
10502
- const optRef = nodeMap.get(optionId);
10503
- if ((optRef == null ? void 0 : optRef.kind) === "option" && typeof optRef.fieldId === "string") {
10504
- push(optRef.fieldId, key);
10505
- }
10506
- continue;
10507
- }
10508
11386
  const ref = nodeMap.get(key);
10509
11387
  if (!ref) continue;
10510
11388
  if (ref.kind === "option" && typeof ref.fieldId === "string") {
@@ -10525,7 +11403,6 @@ var Selection = class {
10525
11403
  * Returns only selection keys that are valid "trigger buttons":
10526
11404
  * - field keys where field.button === true
10527
11405
  * - option keys
10528
- * - composite keys "fieldId::optionId" (validated by optionId)
10529
11406
  * Excludes tags and non-button fields.
10530
11407
  */
10531
11408
  selectedButtons() {
@@ -10541,13 +11418,6 @@ var Selection = class {
10541
11418
  };
10542
11419
  for (const key of this.set) {
10543
11420
  if (!key) continue;
10544
- const idx = key.indexOf("::");
10545
- if (idx !== -1) {
10546
- const optionId = key.slice(idx + 2);
10547
- const optRef = nodeMap.get(optionId);
10548
- if ((optRef == null ? void 0 : optRef.kind) === "option") push(key);
10549
- continue;
10550
- }
10551
11421
  const ref = nodeMap.get(key);
10552
11422
  if (!ref) continue;
10553
11423
  if (ref.kind === "option") {
@@ -10586,17 +11456,7 @@ var Selection = class {
10586
11456
  const direct = fields.find((x) => x.id === id);
10587
11457
  if (direct) return direct;
10588
11458
  if (this.builder.isOptionId(id)) {
10589
- return fields.find(
10590
- (x) => {
10591
- var _a2;
10592
- return ((_a2 = x.options) != null ? _a2 : []).some((o) => o.id === id);
10593
- }
10594
- );
10595
- }
10596
- if (id.includes("::")) {
10597
- const [fieldId] = id.split("::");
10598
- if (!fieldId) return void 0;
10599
- return fields.find((x) => x.id === fieldId);
11459
+ return findOptionOwnerField(fields, id);
10600
11460
  }
10601
11461
  return void 0;
10602
11462
  };
@@ -10627,18 +11487,7 @@ var Selection = class {
10627
11487
  }
10628
11488
  for (const id of this.set) {
10629
11489
  if (this.builder.isOptionId(id)) {
10630
- const host = fields.find(
10631
- (x) => {
10632
- var _a2;
10633
- return ((_a2 = x.options) != null ? _a2 : []).some((o) => o.id === id);
10634
- }
10635
- );
10636
- if (host == null ? void 0 : host.bind_id)
10637
- return Array.isArray(host.bind_id) ? host.bind_id[0] : host.bind_id;
10638
- }
10639
- if (id.includes("::")) {
10640
- const [fid] = id.split("::");
10641
- const host = fields.find((x) => x.id === fid);
11490
+ const host = findOptionOwnerField(fields, id);
10642
11491
  if (host == null ? void 0 : host.bind_id)
10643
11492
  return Array.isArray(host.bind_id) ? host.bind_id[0] : host.bind_id;
10644
11493
  }
@@ -10652,7 +11501,11 @@ var Selection = class {
10652
11501
  const tagById = new Map(tags.map((t) => [t.id, t]));
10653
11502
  const tag = tagById.get(tagId);
10654
11503
  const selectedTriggerIds = this.selectedButtons();
10655
- const fieldIds = this.builder.visibleFields(tagId, selectedTriggerIds);
11504
+ const visibility = this.builder.resolveVisibility(
11505
+ tagId,
11506
+ selectedTriggerIds
11507
+ );
11508
+ const fieldIds = visibility.fieldIds;
10656
11509
  const fieldById = new Map(fields.map((f) => [f.id, f]));
10657
11510
  const visible = fieldIds.map((id) => fieldById.get(id)).filter(Boolean);
10658
11511
  const parentTags = [];
@@ -10678,6 +11531,14 @@ var Selection = class {
10678
11531
  let baseOverridden = false;
10679
11532
  for (const selId of this.set) {
10680
11533
  const opt = this.findOptionById(fields, selId);
11534
+ if (opt && !this.isSelectedOptionVisible(
11535
+ fields,
11536
+ selId,
11537
+ fieldIds,
11538
+ visibility.optionsByFieldId
11539
+ )) {
11540
+ continue;
11541
+ }
10681
11542
  if ((opt == null ? void 0 : opt.service_id) != null) {
10682
11543
  const role = (_d = opt.pricing_role) != null ? _d : "base";
10683
11544
  const cap = (_e = resolve == null ? void 0 : resolve(opt.service_id)) != null ? _e : { id: opt.service_id };
@@ -10708,6 +11569,8 @@ var Selection = class {
10708
11569
  tag,
10709
11570
  fields: visible,
10710
11571
  fieldIds,
11572
+ optionsByFieldId: visibility.optionsByFieldId,
11573
+ forcedFieldIds: visibility.forcedFieldIds,
10711
11574
  parentTags,
10712
11575
  childrenTags,
10713
11576
  services
@@ -10731,21 +11594,19 @@ var Selection = class {
10731
11594
  return baseOverridden;
10732
11595
  }
10733
11596
  findOptionById(fields, selId) {
10734
- var _a, _b;
10735
11597
  if (this.builder.isOptionId(selId)) {
10736
- for (const f of fields) {
10737
- const o = (_a = f.options) == null ? void 0 : _a.find((x) => x.id === selId);
10738
- if (o) return o;
10739
- }
10740
- }
10741
- if (selId.includes("::")) {
10742
- const [fid, oid] = selId.split("::");
10743
- const f = fields.find((x) => x.id === fid);
10744
- const o = (_b = f == null ? void 0 : f.options) == null ? void 0 : _b.find((x) => x.id === oid || x.id === selId);
10745
- if (o) return o;
11598
+ const field = findOptionOwnerField(fields, selId);
11599
+ return findFieldOption(field, selId);
10746
11600
  }
10747
11601
  return void 0;
10748
11602
  }
11603
+ isSelectedOptionVisible(fields, selId, visibleFieldIds, optionsByFieldId) {
11604
+ const visibleFields = new Set(visibleFieldIds);
11605
+ const field = findOptionOwnerField(fields, selId);
11606
+ if (!field || !visibleFields.has(field.id)) return false;
11607
+ const allowed = optionsByFieldId[field.id];
11608
+ return !allowed || allowed.includes(selId);
11609
+ }
10749
11610
  };
10750
11611
 
10751
11612
  // src/react/canvas/api.ts