@drawbridge/components 0.1.6 → 0.1.8

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.
@@ -30,7 +30,12 @@ __export(form_organization_workflow_exports, {
30
30
  FormOrganizationWorkflow: () => FormOrganizationWorkflow,
31
31
  FormOrganizationWorkflowEdit: () => FormOrganizationWorkflowEdit,
32
32
  campaignFilterId: () => campaignFilterId,
33
- stepRow: () => stepRow
33
+ campaignFilterIds: () => campaignFilterIds,
34
+ filterMissing: () => filterMissing,
35
+ filterRows: () => filterRows,
36
+ filterSeed: () => filterSeed,
37
+ stepRow: () => stepRow,
38
+ workflowSchema: () => workflowSchema
34
39
  });
35
40
  module.exports = __toCommonJS(form_organization_workflow_exports);
36
41
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -46,6 +51,7 @@ var import_fieldset = __toESM(require("../shared/fieldset"));
46
51
  var import_input_group = __toESM(require("../input-group"));
47
52
  var import_error = __toESM(require("../shared/error"));
48
53
  var import_input_text = __toESM(require("../input-text"));
54
+ var import_input_textarea = __toESM(require("../input-textarea"));
49
55
  var import_input_url = __toESM(require("../shared/input-url"));
50
56
  var import_input_resource = __toESM(require("./input-resource"));
51
57
  var import_input_combobox = __toESM(require("./input-combobox"));
@@ -54,6 +60,7 @@ var import_input_array = require("./input-array");
54
60
  var import_badge = __toESM(require("../shared/badge"));
55
61
  var import_card_task = __toESM(require("./card-task"));
56
62
  var import_loader = require("../shared/loader");
63
+ var import_sms = require("./lib/sms");
57
64
  var import_trash = __toESM(require("../icons/trash"));
58
65
  var import_button = __toESM(require("../shared/button"));
59
66
  const emailSteps = ["step.email.send", "step.email.digest"];
@@ -230,32 +237,45 @@ const WorkflowTitleField = ({ control, formState }) => /* @__PURE__ */ (0, impor
230
237
  }
231
238
  );
232
239
  const campaignFilterId = (campaign) => (campaign == null ? void 0 : campaign.id) || campaign || null;
240
+ const campaignFilterIds = (filters) => (Array.isArray(filters == null ? void 0 : filters.campaigns) ? filters.campaigns : [filters == null ? void 0 : filters.campaign]).map(campaignFilterId).filter(Boolean);
241
+ const filterRows = (ids, docs) => ids.map((id) => docs.find((doc) => (doc == null ? void 0 : doc.id) === id) || { id });
242
+ const filterSeed = (seeds, id) => (seeds || []).find((doc) => (doc == null ? void 0 : doc.id) === id);
243
+ const filterMissing = (error) => {
244
+ if ((error == null ? void 0 : error.status) === 404) return { result: null };
245
+ throw error;
246
+ };
233
247
  const WorkflowFilterField = ({
234
248
  collection,
235
249
  control,
236
250
  label,
237
251
  message,
238
252
  model,
253
+ multiple = false,
239
254
  name,
240
255
  organization,
241
- resource
256
+ resource,
257
+ seeds
242
258
  }) => {
243
259
  const queryClient = (0, import_react_query.useQueryClient)();
244
- const id = (0, import_form.useWatch)({
260
+ const value = (0, import_form.useWatch)({
245
261
  control,
246
262
  name
247
263
  });
248
- const selected = (0, import_react_query.useQuery)({
249
- queryKey: ["workflow-filter", model, organization, id],
250
- queryFn: async () => {
251
- const { result } = await (0, import_fetch.request)({
252
- endpoint: "/organization/" + organization + "/" + resource + "/" + id
253
- }).catch(() => ({ result: null }));
254
- return result;
255
- },
256
- enabled: Boolean(id),
257
- staleTime: Infinity
258
- });
264
+ const ids = multiple ? value || [] : [value].filter(Boolean);
265
+ const docs = (0, import_react_query.useQueries)({
266
+ queries: ids.map((id) => ({
267
+ queryKey: ["workflow-filter", model, organization, id],
268
+ queryFn: async () => {
269
+ const { result } = await (0, import_fetch.request)({
270
+ endpoint: "/organization/" + organization + "/" + resource + "/" + id
271
+ }).catch(filterMissing);
272
+ return result;
273
+ },
274
+ initialData: () => filterSeed(seeds, id),
275
+ staleTime: Infinity
276
+ }))
277
+ }).map((query) => query.data);
278
+ const seed = (doc) => (doc == null ? void 0 : doc.id) && queryClient.setQueryData(["workflow-filter", model, organization, doc.id], doc);
259
279
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
260
280
  import_form.Controller,
261
281
  {
@@ -272,11 +292,15 @@ const WorkflowFilterField = ({
272
292
  ...{
273
293
  endpoint: "/organization/" + organization + "/" + collection,
274
294
  model,
275
- onChange: (doc) => {
276
- if (doc == null ? void 0 : doc.id) queryClient.setQueryData(["workflow-filter", model, organization, doc.id], doc);
295
+ multiple,
296
+ onChange: multiple ? (picked) => {
297
+ picked.forEach(seed);
298
+ onChange(picked.map((doc) => doc == null ? void 0 : doc.id).filter(Boolean));
299
+ } : (doc) => {
300
+ seed(doc);
277
301
  onChange((doc == null ? void 0 : doc.id) || null);
278
302
  },
279
- value: selected.data || null
303
+ value: multiple ? filterRows(ids, docs) : docs[0] || null
280
304
  }
281
305
  }
282
306
  ),
@@ -363,7 +387,7 @@ const FormOrganizationWorkflow = ({
363
387
  // Both blank, meaning every campaign and every segment. The
364
388
  // create drawer asks for a trigger and a title only; narrowing
365
389
  // is done on the workflow page afterwards.
366
- campaign: null,
390
+ campaigns: [],
367
391
  segment: null
368
392
  }
369
393
  }
@@ -432,6 +456,48 @@ const FormOrganizationWorkflow = ({
432
456
  }
433
457
  );
434
458
  };
459
+ const workflowSchema = import_yup.default.object({
460
+ title: import_yup.default.string().required().max(100).label("Title"),
461
+ triggerEvent: import_yup.default.string().label("Trigger").required("A trigger is required"),
462
+ steps: import_yup.default.array().of(
463
+ import_yup.default.object({
464
+ type: import_yup.default.string().label("Step").required("A step is required"),
465
+ url: import_yup.default.string().when("type", ([type], schema) => {
466
+ if (type === "step.webhook.send") {
467
+ return schema.url().required("URL is required").label("URL");
468
+ }
469
+ return schema;
470
+ }),
471
+ subject: import_yup.default.string().when("type", ([type], schema) => {
472
+ if (emailSteps.includes(type)) {
473
+ return schema.required("Subject is required").label("Subject");
474
+ }
475
+ return schema;
476
+ }),
477
+ message: import_yup.default.string().when("type", ([type], schema) => {
478
+ if (type === "step.sms.send") {
479
+ return schema.required("Message is required").max(1500).label("Message");
480
+ }
481
+ if (messageSteps.includes(type)) {
482
+ return schema.required("Message is required").label("Message");
483
+ }
484
+ return schema;
485
+ }),
486
+ // No minimum. The organization owner is always a recipient, so a
487
+ // step with no members chosen is complete — requiring one here
488
+ // made the step unsaveable for a solo merchant, who has no
489
+ // member documents to choose and cannot reach the members
490
+ // endpoint at all.
491
+ members: import_yup.default.array().label("Recipients"),
492
+ discount: import_yup.default.object().nullable().when("type", ([type], schema) => {
493
+ if (type === "step.commerce.code.issue") {
494
+ return schema.required("Discount is required").label("Discount");
495
+ }
496
+ return schema;
497
+ })
498
+ })
499
+ )
500
+ });
435
501
  const FormOrganizationWorkflowEdit = ({
436
502
  disabled,
437
503
  onClose,
@@ -439,11 +505,13 @@ const FormOrganizationWorkflowEdit = ({
439
505
  onSuccess,
440
506
  values
441
507
  }) => {
442
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
508
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o;
443
509
  const { catalog, catalogList, triggerOptions } = useWorkflowCatalog(values == null ? void 0 : values.organization, (_a = values == null ? void 0 : values.trigger) == null ? void 0 : _a.event);
444
510
  const { items: memberItems, loading: membersLoading } = useOrganizationMembers(values == null ? void 0 : values.organization);
445
511
  const queryClient = (0, import_react_query.useQueryClient)();
446
- const ownerEmail = queryClient.getQueryData(["organization", values == null ? void 0 : values.organization]).owner.email;
512
+ const organizationData = queryClient.getQueryData(["organization", values == null ? void 0 : values.organization]);
513
+ const ownerEmail = organizationData.owner.email;
514
+ const smsActive = (0, import_sms.smsNumberActive)(organizationData);
447
515
  const commerceConnectionId = ((_b = catalogList.flatMap((item) => item.steps || []).find(
448
516
  (s) => s.value === "step.commerce.customer.insert" || s.value === "step.commerce.code.issue"
449
517
  )) == null ? void 0 : _b.connectionId) || null;
@@ -471,55 +539,15 @@ const FormOrganizationWorkflowEdit = ({
471
539
  const { control, formState, getValues, handleSubmit, setValue } = (0, import_form.useForm)({
472
540
  values: {
473
541
  steps: defaultSteps,
474
- filterCampaign: campaignFilterId((_h = (_g = values == null ? void 0 : values.trigger) == null ? void 0 : _g.filters) == null ? void 0 : _h.campaign),
542
+ filterCampaigns: campaignFilterIds((_g = values == null ? void 0 : values.trigger) == null ? void 0 : _g.filters),
475
543
  // Stored and returned as a plain id — extend.workflow() $lookup-hydrates
476
544
  // only the campaign — but read through the same reducer, so the field
477
545
  // stays right if the segment is ever hydrated too.
478
- filterSegment: campaignFilterId((_j = (_i = values == null ? void 0 : values.trigger) == null ? void 0 : _i.filters) == null ? void 0 : _j.segment),
546
+ filterSegment: campaignFilterId((_i = (_h = values == null ? void 0 : values.trigger) == null ? void 0 : _h.filters) == null ? void 0 : _i.segment),
479
547
  title: (values == null ? void 0 : values.title) || "",
480
- triggerEvent: ((_k = values == null ? void 0 : values.trigger) == null ? void 0 : _k.event) || ""
548
+ triggerEvent: ((_j = values == null ? void 0 : values.trigger) == null ? void 0 : _j.event) || ""
481
549
  },
482
- resolver: (0, import_yup.resolver)(
483
- import_yup.default.object({
484
- title: import_yup.default.string().required().max(100).label("Title"),
485
- triggerEvent: import_yup.default.string().label("Trigger").required("A trigger is required"),
486
- steps: import_yup.default.array().of(
487
- import_yup.default.object({
488
- type: import_yup.default.string().label("Step").required("A step is required"),
489
- url: import_yup.default.string().when("type", ([type], schema) => {
490
- if (type === "step.webhook.send") {
491
- return schema.url().required("URL is required").label("URL");
492
- }
493
- return schema;
494
- }),
495
- subject: import_yup.default.string().when("type", ([type], schema) => {
496
- if (messageSteps.includes(type)) {
497
- return schema.required("Subject is required").label("Subject");
498
- }
499
- return schema;
500
- }),
501
- message: import_yup.default.string().when("type", ([type], schema) => {
502
- if (messageSteps.includes(type)) {
503
- return schema.required("Message is required").label("Message");
504
- }
505
- return schema;
506
- }),
507
- // No minimum. The organization owner is always a recipient, so a
508
- // step with no members chosen is complete — requiring one here
509
- // made the step unsaveable for a solo merchant, who has no
510
- // member documents to choose and cannot reach the members
511
- // endpoint at all.
512
- members: import_yup.default.array().label("Recipients"),
513
- discount: import_yup.default.object().nullable().when("type", ([type], schema) => {
514
- if (type === "step.commerce.code.issue") {
515
- return schema.required("Discount is required").label("Discount");
516
- }
517
- return schema;
518
- })
519
- })
520
- )
521
- })
522
- )
550
+ resolver: (0, import_yup.resolver)(workflowSchema)
523
551
  });
524
552
  const watchedTrigger = (0, import_form.useWatch)({
525
553
  control,
@@ -529,7 +557,7 @@ const FormOrganizationWorkflowEdit = ({
529
557
  control,
530
558
  name: "steps"
531
559
  });
532
- const issueIndex = ((_l = values == null ? void 0 : values.issue) == null ? void 0 : _l.step) ? (watchedSteps || []).findIndex((step) => (step == null ? void 0 : step.type) === values.issue.step) : -1;
560
+ const issueIndex = ((_k = values == null ? void 0 : values.issue) == null ? void 0 : _k.step) ? (watchedSteps || []).findIndex((step) => (step == null ? void 0 : step.type) === values.issue.step) : -1;
533
561
  const selectedTriggerEntry = catalogList.find((item) => item.trigger === watchedTrigger) || null;
534
562
  const getAvailableVariables = (index) => {
535
563
  return watchedSteps.slice(0, index).flatMap((watchedStep) => {
@@ -569,6 +597,22 @@ const FormOrganizationWorkflowEdit = ({
569
597
  href: "/organizations/" + (values == null ? void 0 : values.organization) + "/connections/" + step.slug,
570
598
  onClick: onClose
571
599
  }
600
+ } : {},
601
+ // The same link-shaped option, greyed: it cannot be picked, and
602
+ // clicking it opens Settings, where the number is bought.
603
+ ...step.value === "step.sms.send" && !smsActive ? {
604
+ badge: {
605
+ text: "Settings"
606
+ },
607
+ className: "opacity-50",
608
+ disabled: false,
609
+ element: import_link.default,
610
+ key: step == null ? void 0 : step.key,
611
+ props: {
612
+ "aria-disabled": true,
613
+ href: "/organizations/" + (values == null ? void 0 : values.organization) + "/settings",
614
+ onClick: onClose
615
+ }
572
616
  } : {}
573
617
  }))
574
618
  ];
@@ -615,7 +659,7 @@ const FormOrganizationWorkflowEdit = ({
615
659
  };
616
660
  const onSubmit = async ({
617
661
  steps,
618
- filterCampaign,
662
+ filterCampaigns,
619
663
  filterSegment,
620
664
  title,
621
665
  triggerEvent
@@ -625,10 +669,8 @@ const FormOrganizationWorkflowEdit = ({
625
669
  steps: steps.map(({ connectionId, discount, members, message, score, slug, subject, text, type, url }, index) => {
626
670
  const settings = {};
627
671
  if (type === "step.webhook.send") settings.url = url;
628
- if (messageSteps.includes(type)) {
629
- settings.subject = subject;
630
- settings.message = message;
631
- }
672
+ if (emailSteps.includes(type)) settings.subject = subject;
673
+ if (messageSteps.includes(type)) settings.message = message;
632
674
  if (emailSteps.includes(type)) {
633
675
  if (score && score.subject === subject && score.message === message) settings.score = score;
634
676
  else if (score) setValue("steps." + index + ".score", null);
@@ -651,7 +693,7 @@ const FormOrganizationWorkflowEdit = ({
651
693
  trigger: {
652
694
  event: triggerEvent,
653
695
  filters: {
654
- campaign: filterCampaign || null,
696
+ campaigns: filterCampaigns || [],
655
697
  segment: filterSegment || null
656
698
  }
657
699
  }
@@ -721,12 +763,14 @@ const FormOrganizationWorkflowEdit = ({
721
763
  ...{
722
764
  collection: "campaigns",
723
765
  control,
724
- label: "Campaign",
725
- message: "Only trigger for leads from a specific campaign. Leaving blank will trigger for all campaigns.",
766
+ label: "Campaigns",
767
+ message: "Only runs for leads from the campaigns you add. With none added, it runs for every campaign.",
726
768
  model: "campaign",
727
- name: "filterCampaign",
769
+ multiple: true,
770
+ name: "filterCampaigns",
728
771
  organization: values == null ? void 0 : values.organization,
729
- resource: "campaign"
772
+ resource: "campaign",
773
+ seeds: (_m = (_l = values == null ? void 0 : values.trigger) == null ? void 0 : _l.filters) == null ? void 0 : _m.campaigns
730
774
  }
731
775
  }
732
776
  ),
@@ -749,7 +793,7 @@ const FormOrganizationWorkflowEdit = ({
749
793
  import_input_group.default,
750
794
  {
751
795
  ...{
752
- error: (_n = (_m = formState == null ? void 0 : formState.errors) == null ? void 0 : _m.steps) == null ? void 0 : _n.message,
796
+ error: (_o = (_n = formState == null ? void 0 : formState.errors) == null ? void 0 : _n.steps) == null ? void 0 : _o.message,
753
797
  input: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
754
798
  import_input_array.InputArraySortable,
755
799
  {
@@ -979,36 +1023,6 @@ const FormOrganizationWorkflowEdit = ({
979
1023
  }
980
1024
  }
981
1025
  ),
982
- step.type === "step.sms.send" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
983
- import_form.Controller,
984
- {
985
- ...{
986
- control,
987
- name: `steps.${index}.subject`,
988
- render: ({ field: { value, onChange } }) => {
989
- var _a2, _b2, _c2, _d2;
990
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
991
- import_input_group.default,
992
- {
993
- ...{
994
- error: (_d2 = (_c2 = (_b2 = (_a2 = formState == null ? void 0 : formState.errors) == null ? void 0 : _a2.steps) == null ? void 0 : _b2[index]) == null ? void 0 : _c2.subject) == null ? void 0 : _d2.message,
995
- input: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
996
- import_input_text.default,
997
- {
998
- ...{
999
- onChange,
1000
- value: value || ""
1001
- }
1002
- }
1003
- ),
1004
- label: "Subject"
1005
- }
1006
- }
1007
- );
1008
- }
1009
- }
1010
- }
1011
- ),
1012
1026
  step.type === "step.sms.send" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1013
1027
  import_form.Controller,
1014
1028
  {
@@ -1023,10 +1037,11 @@ const FormOrganizationWorkflowEdit = ({
1023
1037
  ...{
1024
1038
  error: (_d2 = (_c2 = (_b2 = (_a2 = formState == null ? void 0 : formState.errors) == null ? void 0 : _a2.steps) == null ? void 0 : _b2[index]) == null ? void 0 : _c2.message) == null ? void 0 : _d2.message,
1025
1039
  input: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1026
- import_input_text.default,
1040
+ import_input_textarea.default,
1027
1041
  {
1028
1042
  ...{
1029
1043
  onChange,
1044
+ rows: 3,
1030
1045
  value: value || ""
1031
1046
  }
1032
1047
  }
@@ -1141,5 +1156,10 @@ const FormOrganizationWorkflowEdit = ({
1141
1156
  FormOrganizationWorkflow,
1142
1157
  FormOrganizationWorkflowEdit,
1143
1158
  campaignFilterId,
1144
- stepRow
1159
+ campaignFilterIds,
1160
+ filterMissing,
1161
+ filterRows,
1162
+ filterSeed,
1163
+ stepRow,
1164
+ workflowSchema
1145
1165
  });