@dsh-plus/notify-email 0.1.35 → 0.1.37

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.
package/lib/client.js CHANGED
@@ -12,9 +12,72 @@ window.__ModuleLoader__.load({
12
12
  kind: "idle",
13
13
  text: ""
14
14
  };
15
+ /** 状态行 + 动作按钮(两种形态共用,类名随形态切换度量)。 */
16
+ function CardFoot(props) {
17
+ const p = props.prefix;
18
+ const section = props.variant === "section";
19
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20
+ className: section ? `${p}-sectionFooter` : `${p}-footer`,
21
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
22
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
23
+ role: "status",
24
+ children: props.status.text
25
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
26
+ type: "button",
27
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
28
+ disabled: action.disabled === true,
29
+ onClick: action.onClick,
30
+ children: action.label
31
+ }, action.key))]
32
+ });
33
+ }
34
+ /** 只读提示 + 字段区 + 页脚(两种形态共用)。 */
35
+ function CardBody(props) {
36
+ const p = props.prefix;
37
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
38
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
39
+ className: `${p}-readOnly`,
40
+ role: "status",
41
+ children: props.readOnlyNotice
42
+ }) : null,
43
+ props.children,
44
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFoot, { ...props })
45
+ ] });
46
+ }
15
47
  function CardChrome(props) {
16
48
  const p = props.prefix;
17
49
  const badge = props.statusBadge;
50
+ if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
51
+ className: `${p}-section`,
52
+ "aria-label": props.title,
53
+ children: [
54
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55
+ className: `${p}-sectionHead`,
56
+ children: [
57
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
58
+ className: `${p}-sectionTitle`,
59
+ children: props.title
60
+ }),
61
+ badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
62
+ className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
63
+ children: badge.text
64
+ }) : null,
65
+ props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
66
+ className: `${p}-pending`,
67
+ children: props.dirtyLabel
68
+ }) : null
69
+ ]
70
+ }),
71
+ props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
72
+ className: `${p}-sectionDesc`,
73
+ children: props.description
74
+ }),
75
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
76
+ className: `${p}-sectionBody`,
77
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
78
+ })
79
+ ]
80
+ });
18
81
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
19
82
  className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
20
83
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -44,38 +107,44 @@ window.__ModuleLoader__.load({
44
107
  }) : null,
45
108
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
46
109
  ]
47
- }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
110
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48
111
  className: `${p}-body`,
49
- children: [
50
- props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
51
- className: `${p}-readOnly`,
52
- role: "status",
53
- children: props.readOnlyNotice
54
- }) : null,
55
- props.children,
56
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57
- className: `${p}-footer`,
58
- children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
59
- className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
60
- role: "status",
61
- children: props.status.text
62
- }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
63
- type: "button",
64
- className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
65
- disabled: action.disabled === true,
66
- onClick: action.onClick,
67
- children: action.label
68
- }, action.key))]
69
- })
70
- ]
112
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
71
113
  }) : null]
72
114
  });
73
115
  }
116
+ /** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
117
+ function CardLoading(props) {
118
+ if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
119
+ className: `${props.prefix}-section`,
120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
121
+ className: `${props.prefix}-readOnly`,
122
+ role: "status",
123
+ children: props.text
124
+ })
125
+ });
126
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
127
+ className: `${props.prefix}-card`,
128
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
129
+ className: `${props.prefix}-readOnly`,
130
+ role: "status",
131
+ children: props.text
132
+ })
133
+ });
134
+ }
74
135
  //#endregion
75
136
  //#region ../shared/src/client/config-slots.ts
76
137
  /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
77
138
  const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
78
139
  /**
140
+ * 视图 → 外壳形态:插件页 page 视图渲染在官方页面容器内(宿主已画标题、图标、
141
+ * 面包屑与页面级内边距),再套卡片会形成卡片套卡片并压缩移动端可用宽度,
142
+ * 故用无边框分节;summary 与 legacy 槽位保持卡片形态。
143
+ */
144
+ function cardVariantFor(view) {
145
+ return view === "page" ? "section" : "card";
146
+ }
147
+ /**
79
148
  * 把一张配置卡片同时注册进 legacy 与新版三个槽位。
80
149
  * 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
81
150
  */
@@ -101,6 +170,17 @@ window.__ModuleLoader__.load({
101
170
  }, reg.component));
102
171
  }
103
172
  //#endregion
173
+ //#region ../shared/src/client/draft.ts
174
+ /**
175
+ * 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
176
+ * 脏判定、保存(revision fencing + 重新装载)与状态行。
177
+ *
178
+ * 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
179
+ * 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
180
+ * 依赖,必须是模块级稳定函数(不要内联箭头函数)。
181
+ * @module @dsh-plus/shared/client/draft
182
+ */
183
+ //#endregion
104
184
  //#region ../shared/src/client/fields.tsx
105
185
  function TextField(props) {
106
186
  const p = props.prefix ?? "dshp";
@@ -304,6 +384,9 @@ window.__ModuleLoader__.load({
304
384
  * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
305
385
  * - footer 按钮等宽换行、状态文字独占一行;
306
386
  * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
387
+ * - 分节形态(-section,插件页 page 视图内嵌宿主页面):无边框/背景、横向零
388
+ * 内边距,页脚不 sticky——官方插件页已提供页面级内边距,再叠一层卡片会
389
+ * 缩窄移动端可用宽度(卡片套卡片)。
307
390
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
308
391
  * @module @dsh-plus/shared/client/styles
309
392
  */
@@ -350,6 +433,15 @@ window.__ModuleLoader__.load({
350
433
  .${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
351
434
  .${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
352
435
  .${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
436
+ /* 宿主内嵌分节形态(插件页 page 视图):官方设置页同构——无边框/背景,
437
+ 横向零内边距(宽度交给宿主页面容器),小节标题 13px/600,页脚非 sticky。 */
438
+ .${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
439
+ .${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
440
+ .${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
441
+ .${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
442
+ .${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
443
+ .${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
444
+ .${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
353
445
  .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
354
446
  .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
355
447
  .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
@@ -371,6 +463,8 @@ window.__ModuleLoader__.load({
371
463
  .${prefix}-status{flex-basis:100%;flex:none}
372
464
  .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
373
465
  .${prefix}-statusBadge{display:none}
466
+ .${prefix}-sectionBadge{display:inline-block}
467
+ .${prefix}-sectionFooter{position:static}
374
468
  }
375
469
  @media (pointer:coarse){
376
470
  .${prefix}-btn{min-height:44px}
@@ -422,20 +516,18 @@ ${extra}
422
516
  }));
423
517
  }
424
518
  //#endregion
425
- //#region src/client/card.tsx
426
- /**
427
- * 「邮件通知」配置卡片:经 injectPluginConfigCard 注册(legacy settings.plugin.item
428
- * 与 0.1.6-alpha.2 插件页 plugins.row.config / plugins.bundle.config,view 分发
429
- * summary/page)。
430
- * 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
431
- * 本文件只保留业务字段与保存/测试逻辑。交互对齐官方卡片:折叠/展开、
432
- * staged draft、未保存标记、保存/放弃;另加「发送测试邮件」。
433
- * 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
434
- * 已移除):value 为 schema 解析后的脱敏视图
435
- * (smtp.pass 不出现,passConfigured 由 describe 的 secrets 探测),
436
- * 保存经 settings.update 深合并(空 pass 剔除 = 保持不变)。
437
- * @module notify-email/client/card
438
- */
519
+ //#region src/client/draft.ts
520
+ /** 通知触发点(key 同时是 i18n 文案键与 settings 字段名)。 */
521
+ const TRIGGER_KEYS = [
522
+ "onComplete",
523
+ "onError",
524
+ "onAborted",
525
+ "onQuestion",
526
+ "onPlanReview"
527
+ ];
528
+ function isPositiveInt(text) {
529
+ return /^[0-9]+$/.test(text) && Number(text) > 0;
530
+ }
439
531
  function draftFromValue(value) {
440
532
  return {
441
533
  enabled: value.enabled,
@@ -452,9 +544,7 @@ ${extra}
452
544
  dryRun: value.dryRun
453
545
  };
454
546
  }
455
- function isPositiveInt(text) {
456
- return /^[0-9]+$/.test(text) && Number(text) > 0;
457
- }
547
+ /** 草稿 → 提交形状(pass 为空表示「保持已存密钥」,由调用方剔除该键)。 */
458
548
  function toPatch(draft) {
459
549
  return {
460
550
  enabled: draft.enabled,
@@ -473,47 +563,242 @@ ${extra}
473
563
  dryRun: draft.dryRun
474
564
  };
475
565
  }
476
- const TRIGGER_KEYS = [
477
- "onComplete",
478
- "onError",
479
- "onAborted",
480
- "onQuestion",
481
- "onPlanReview"
482
- ];
566
+ //#endregion
567
+ //#region src/client/fields.tsx
568
+ /**
569
+ * 「邮件通知」卡片的字段组:SMTP 连接字段与触发/限流字段。
570
+ * 从 card.tsx 拆出,避免单组件过长;控件走 @dsh-plus/shared/client 套件。
571
+ * @module notify-email/client/fields
572
+ */
573
+ /** SMTP 连接字段(密码为只写:留空保持已存密钥)。 */
574
+ function SmtpFields(props) {
575
+ const { t, draft, disabled, onEdit } = props;
576
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
578
+ prefix: "dne",
579
+ id: "dne-host",
580
+ label: t("host"),
581
+ hint: t("hostHint"),
582
+ value: draft.host,
583
+ disabled,
584
+ onEdit: (v) => onEdit("host", v)
585
+ }),
586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
587
+ prefix: "dne",
588
+ id: "dne-port",
589
+ label: t("port"),
590
+ hint: t("portHint"),
591
+ value: draft.port,
592
+ numeric: true,
593
+ disabled,
594
+ invalid: !isPositiveInt(draft.port),
595
+ invalidLabel: t("invalidNumber"),
596
+ onEdit: (v) => onEdit("port", v)
597
+ }),
598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
599
+ prefix: "dne",
600
+ id: "dne-secure",
601
+ label: t("secure"),
602
+ checked: draft.secure,
603
+ disabled,
604
+ onEdit: (v) => onEdit("secure", v)
605
+ }),
606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
607
+ prefix: "dne",
608
+ id: "dne-user",
609
+ label: t("user"),
610
+ hint: t("userHint"),
611
+ value: draft.user,
612
+ disabled,
613
+ onEdit: (v) => onEdit("user", v)
614
+ }),
615
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
616
+ prefix: "dne",
617
+ id: "dne-pass",
618
+ label: t("pass"),
619
+ hint: t("passHint"),
620
+ value: draft.pass,
621
+ password: true,
622
+ disabled,
623
+ badge: {
624
+ text: props.passConfigured ? t("passSet") : t("passUnset"),
625
+ set: props.passConfigured
626
+ },
627
+ onEdit: (v) => onEdit("pass", v)
628
+ }),
629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
630
+ prefix: "dne",
631
+ id: "dne-from",
632
+ label: t("from"),
633
+ hint: t("fromHint"),
634
+ value: draft.from,
635
+ disabled,
636
+ onEdit: (v) => onEdit("from", v)
637
+ })
638
+ ] });
639
+ }
640
+ /** 收件人、触发点开关与限流字段。 */
641
+ function TriggerFields(props) {
642
+ const { t, draft, disabled, onEdit } = props;
643
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
645
+ prefix: "dne",
646
+ id: "dne-to",
647
+ label: t("to"),
648
+ hint: t("toHint"),
649
+ value: draft.toText,
650
+ disabled,
651
+ onEdit: (v) => onEdit("toText", v)
652
+ }),
653
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
654
+ className: "dne-groupLabel",
655
+ children: t("triggerGroup")
656
+ }),
657
+ TRIGGER_KEYS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
658
+ prefix: "dne",
659
+ id: `dne-${key}`,
660
+ label: t(key),
661
+ checked: draft.triggers[key],
662
+ disabled,
663
+ onEdit: (v) => props.onTrigger(key, v)
664
+ }, key)),
665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
666
+ prefix: "dne",
667
+ id: "dne-debounce",
668
+ label: t("idleDebounceMs"),
669
+ hint: t("idleDebounceMsHint"),
670
+ value: draft.idleDebounceMs,
671
+ numeric: true,
672
+ disabled,
673
+ invalid: !isPositiveInt(draft.idleDebounceMs),
674
+ invalidLabel: t("invalidNumber"),
675
+ onEdit: (v) => onEdit("idleDebounceMs", v)
676
+ }),
677
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
678
+ prefix: "dne",
679
+ id: "dne-maxchars",
680
+ label: t("maxBodyChars"),
681
+ hint: t("maxBodyCharsHint"),
682
+ value: draft.maxBodyChars,
683
+ numeric: true,
684
+ disabled,
685
+ invalid: !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200,
686
+ invalidLabel: t("invalidNumber"),
687
+ onEdit: (v) => onEdit("maxBodyChars", v)
688
+ }),
689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
690
+ prefix: "dne",
691
+ id: "dne-dryrun",
692
+ label: t("dryRun"),
693
+ checked: draft.dryRun,
694
+ disabled,
695
+ onEdit: (v) => onEdit("dryRun", v)
696
+ })
697
+ ] });
698
+ }
699
+ //#endregion
700
+ //#region src/client/card.tsx
701
+ /**
702
+ * 「邮件通知」配置卡片:经 injectPluginConfigCard 注册(legacy settings.plugin.item
703
+ * 与 0.1.6-alpha.2 插件页 plugins.row.config / plugins.bundle.config,view 分发
704
+ * summary/page)。
705
+ * 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
706
+ * 本文件只保留业务字段与保存/测试逻辑。交互对齐官方卡片:折叠/展开、
707
+ * staged draft、未保存标记、保存/放弃;另加「发送测试邮件」。
708
+ * 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
709
+ * 已移除):value 为 schema 解析后的脱敏视图
710
+ * (smtp.pass 不出现,passConfigured 由 describe 的 secrets 探测),
711
+ * 保存经 settings.update 深合并(空 pass 剔除 = 保持不变)。
712
+ * @module notify-email/client/card
713
+ */
714
+ /**
715
+ * passConfigured 探测:scope 快照不含 secrets,经 describe 的 secrets 列表判断;
716
+ * snapshot 变化(保存后 revision 推进)时刻意重探。
717
+ */
718
+ function usePassConfigured(api, snapshot) {
719
+ const [passConfigured, setPassConfigured] = (0, react.useState)(false);
720
+ (0, react.useEffect)(() => {
721
+ let alive = true;
722
+ api.describe().then((view) => {
723
+ if (!alive) return;
724
+ const ns = view.namespaces.find((candidate) => candidate.ns === SETTINGS_NS);
725
+ setPassConfigured(ns?.secrets.some((secret) => secret.path.join(".") === "smtp.pass" && secret.set) ?? false);
726
+ }).catch(() => {});
727
+ return () => {
728
+ alive = false;
729
+ };
730
+ }, [api, snapshot]);
731
+ return passConfigured;
732
+ }
733
+ /** 卡片动作:发送测试、放弃、保存(保存受无效字段与只读限制)。 */
734
+ function cardActions(options) {
735
+ const { t } = options;
736
+ return [
737
+ {
738
+ key: "test",
739
+ label: t(options.testing ? "testing" : "test"),
740
+ disabled: options.testing,
741
+ onClick: options.onTest
742
+ },
743
+ {
744
+ key: "discard",
745
+ label: t("discard"),
746
+ disabled: !options.dirty || options.saving,
747
+ onClick: options.onDiscard
748
+ },
749
+ {
750
+ key: "save",
751
+ label: t(options.saving ? "saving" : "save"),
752
+ variant: "primary",
753
+ disabled: !options.dirty || options.invalid || options.saving || options.disabled,
754
+ onClick: options.onSave
755
+ }
756
+ ];
757
+ }
758
+ /** 发送测试邮件:端点结果折算成状态行文案(不改草稿);finally 收尾由调用方给。 */
759
+ function sendTestMail(t, onStatus, onSettled) {
760
+ sendTest().then((result) => {
761
+ if (result.ok) {
762
+ onStatus({
763
+ kind: "ok",
764
+ text: result.detail === "dry-run" ? t("testDryRun") : t("testOk")
765
+ });
766
+ return;
767
+ }
768
+ const why = result.detail === "incomplete" ? t("incomplete") : result.detail;
769
+ onStatus({
770
+ kind: "error",
771
+ text: `${t("testFailed")}${why}`
772
+ });
773
+ }).catch((error) => {
774
+ onStatus({
775
+ kind: "error",
776
+ text: `${t("testFailed")}${error instanceof Error ? error.message : ""}`
777
+ });
778
+ }).finally(onSettled);
779
+ }
483
780
  function NotifyEmailCard(props) {
484
781
  const { t, scope, api } = props;
485
782
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
486
783
  const value = snapshot.value;
487
784
  const [open, setOpen] = (0, react.useState)(props.view === "page");
488
785
  const [draft, setDraft] = (0, react.useState)(null);
489
- const [passConfigured, setPassConfigured] = (0, react.useState)(false);
490
786
  const [saving, setSaving] = (0, react.useState)(false);
491
787
  const [testing, setTesting] = (0, react.useState)(false);
492
788
  const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
789
+ const passConfigured = usePassConfigured(api, snapshot);
493
790
  (0, react.useEffect)(() => {
494
791
  if (value === void 0 || draft !== null) return;
495
792
  setDraft(draftFromValue(value));
496
793
  }, [value, draft]);
497
- (0, react.useEffect)(() => {
498
- let alive = true;
499
- api.describe().then((view) => {
500
- if (!alive) return;
501
- const ns = view.namespaces.find((candidate) => candidate.ns === SETTINGS_NS);
502
- setPassConfigured(ns?.secrets.some((secret) => secret.path.join(".") === "smtp.pass" && secret.set) ?? false);
503
- }).catch(() => {});
504
- return () => {
505
- alive = false;
506
- };
507
- }, [api, snapshot]);
508
794
  const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
509
795
  const invalid = (0, react.useMemo)(() => draft !== null && (!isPositiveInt(draft.port) || !isPositiveInt(draft.idleDebounceMs) || !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200), [draft]);
510
796
  if (props.view === "summary") return t("summaryLine");
511
- if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
512
- className: "dne-card",
513
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
514
- className: "dne-readOnly",
515
- children: t("loading")
516
- })
797
+ const variant = cardVariantFor(props.view);
798
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
799
+ prefix: "dne",
800
+ variant,
801
+ text: t("loading")
517
802
  });
518
803
  const edit = (key, editValue) => {
519
804
  setDraft({
@@ -548,24 +833,7 @@ ${extra}
548
833
  };
549
834
  const onTest = () => {
550
835
  setTesting(true);
551
- sendTest().then((result) => {
552
- if (result.ok) setStatus({
553
- kind: "ok",
554
- text: result.detail === "dry-run" ? t("testDryRun") : t("testOk")
555
- });
556
- else {
557
- const why = result.detail === "incomplete" ? t("incomplete") : result.detail;
558
- setStatus({
559
- kind: "error",
560
- text: `${t("testFailed")}${why}`
561
- });
562
- }
563
- }).catch((error) => {
564
- setStatus({
565
- kind: "error",
566
- text: `${t("testFailed")}${error instanceof Error ? error.message : ""}`
567
- });
568
- }).finally(() => setTesting(false));
836
+ sendTestMail(t, setStatus, () => setTesting(false));
569
837
  };
570
838
  const disabled = !snapshot.writable;
571
839
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
@@ -574,6 +842,7 @@ ${extra}
574
842
  description: t("description"),
575
843
  open,
576
844
  onToggle: setOpen,
845
+ variant,
577
846
  statusBadge: {
578
847
  text: t(draft.enabled ? "enabledOn" : "enabledOff"),
579
848
  on: draft.enabled
@@ -582,30 +851,20 @@ ${extra}
582
851
  dirtyLabel: t("unsaved"),
583
852
  readOnlyNotice: disabled ? t("readOnly") : void 0,
584
853
  status,
585
- actions: [
586
- {
587
- key: "test",
588
- label: t(testing ? "testing" : "test"),
589
- disabled: testing,
590
- onClick: onTest
854
+ actions: cardActions({
855
+ t,
856
+ dirty,
857
+ invalid,
858
+ saving,
859
+ testing,
860
+ disabled,
861
+ onTest,
862
+ onDiscard: () => {
863
+ setDraft(draftFromValue(value));
864
+ setStatus(IDLE_STATUS);
591
865
  },
592
- {
593
- key: "discard",
594
- label: t("discard"),
595
- disabled: !dirty || saving,
596
- onClick: () => {
597
- setDraft(draftFromValue(value));
598
- setStatus(IDLE_STATUS);
599
- }
600
- },
601
- {
602
- key: "save",
603
- label: t(saving ? "saving" : "save"),
604
- variant: "primary",
605
- disabled: !dirty || invalid || saving || disabled,
606
- onClick: onSave
607
- }
608
- ],
866
+ onSave
867
+ }),
609
868
  children: [
610
869
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
611
870
  prefix: "dne",
@@ -615,119 +874,19 @@ ${extra}
615
874
  disabled,
616
875
  onEdit: (v) => edit("enabled", v)
617
876
  }),
618
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
619
- prefix: "dne",
620
- id: "dne-host",
621
- label: t("host"),
622
- hint: t("hostHint"),
623
- value: draft.host,
624
- disabled,
625
- onEdit: (v) => edit("host", v)
626
- }),
627
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
628
- prefix: "dne",
629
- id: "dne-port",
630
- label: t("port"),
631
- hint: t("portHint"),
632
- value: draft.port,
633
- numeric: true,
634
- disabled,
635
- invalid: !isPositiveInt(draft.port),
636
- invalidLabel: t("invalidNumber"),
637
- onEdit: (v) => edit("port", v)
638
- }),
639
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
640
- prefix: "dne",
641
- id: "dne-secure",
642
- label: t("secure"),
643
- checked: draft.secure,
644
- disabled,
645
- onEdit: (v) => edit("secure", v)
646
- }),
647
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
648
- prefix: "dne",
649
- id: "dne-user",
650
- label: t("user"),
651
- hint: t("userHint"),
652
- value: draft.user,
653
- disabled,
654
- onEdit: (v) => edit("user", v)
655
- }),
656
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
657
- prefix: "dne",
658
- id: "dne-pass",
659
- label: t("pass"),
660
- hint: t("passHint"),
661
- value: draft.pass,
662
- password: true,
663
- disabled,
664
- badge: {
665
- text: passConfigured ? t("passSet") : t("passUnset"),
666
- set: passConfigured
667
- },
668
- onEdit: (v) => edit("pass", v)
669
- }),
670
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
671
- prefix: "dne",
672
- id: "dne-from",
673
- label: t("from"),
674
- hint: t("fromHint"),
675
- value: draft.from,
877
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SmtpFields, {
878
+ t,
879
+ draft,
676
880
  disabled,
677
- onEdit: (v) => edit("from", v)
678
- }),
679
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
680
- prefix: "dne",
681
- id: "dne-to",
682
- label: t("to"),
683
- hint: t("toHint"),
684
- value: draft.toText,
685
- disabled,
686
- onEdit: (v) => edit("toText", v)
687
- }),
688
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
689
- className: "dne-groupLabel",
690
- children: t("triggerGroup")
881
+ passConfigured,
882
+ onEdit: edit
691
883
  }),
692
- TRIGGER_KEYS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
693
- prefix: "dne",
694
- id: `dne-${key}`,
695
- label: t(key),
696
- checked: draft.triggers[key],
697
- disabled,
698
- onEdit: (v) => editTrigger(key, v)
699
- }, key)),
700
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
701
- prefix: "dne",
702
- id: "dne-debounce",
703
- label: t("idleDebounceMs"),
704
- hint: t("idleDebounceMsHint"),
705
- value: draft.idleDebounceMs,
706
- numeric: true,
707
- disabled,
708
- invalid: !isPositiveInt(draft.idleDebounceMs),
709
- invalidLabel: t("invalidNumber"),
710
- onEdit: (v) => edit("idleDebounceMs", v)
711
- }),
712
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
713
- prefix: "dne",
714
- id: "dne-maxchars",
715
- label: t("maxBodyChars"),
716
- hint: t("maxBodyCharsHint"),
717
- value: draft.maxBodyChars,
718
- numeric: true,
719
- disabled,
720
- invalid: !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200,
721
- invalidLabel: t("invalidNumber"),
722
- onEdit: (v) => edit("maxBodyChars", v)
723
- }),
724
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
725
- prefix: "dne",
726
- id: "dne-dryrun",
727
- label: t("dryRun"),
728
- checked: draft.dryRun,
884
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriggerFields, {
885
+ t,
886
+ draft,
729
887
  disabled,
730
- onEdit: (v) => edit("dryRun", v)
888
+ onEdit: edit,
889
+ onTrigger: editTrigger
731
890
  })
732
891
  ]
733
892
  });