@dsh-plus/notify-email 0.1.36 → 0.1.38

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
@@ -4,9 +4,9 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
7
  let react_jsx_runtime = require("react/jsx-runtime");
9
- let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react = require("react");
9
+ require("@deepseek-ai/dsh-client-ui-primitives");
10
10
  //#region ../shared/src/client/card.tsx
11
11
  const IDLE_STATUS = {
12
12
  kind: "idle",
@@ -15,60 +15,68 @@ window.__ModuleLoader__.load({
15
15
  function CardChrome(props) {
16
16
  const p = props.prefix;
17
17
  const badge = props.statusBadge;
18
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
19
- className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
20
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
21
- type: "button",
22
- className: `${p}-header`,
23
- "aria-expanded": props.open,
24
- "aria-label": props.title,
25
- onClick: () => props.onToggle(!props.open),
26
- children: [
27
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
28
- className: `${p}-headText`,
29
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30
- className: `${p}-name`,
18
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
19
+ className: `${p}-section`,
20
+ "aria-label": props.title,
21
+ children: [
22
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
23
+ className: `${p}-sectionHead`,
24
+ children: [
25
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
26
+ className: `${p}-sectionTitle`,
31
27
  children: props.title
32
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
33
- className: `${p}-description`,
34
- children: props.description
35
- })]
36
- }),
37
- badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
38
- className: `${p}-statusBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
39
- children: badge.text
40
- }) : null,
41
- props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
42
- className: `${p}-pending`,
43
- children: props.dirtyLabel
44
- }) : null,
45
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
46
- ]
47
- }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
48
- 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` : ""}`,
28
+ }),
29
+ badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
30
+ className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
31
+ children: badge.text
32
+ }) : null,
33
+ props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34
+ className: `${p}-pending`,
35
+ children: props.dirtyLabel
36
+ }) : null
37
+ ]
38
+ }),
39
+ props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
40
+ className: `${p}-sectionDesc`,
41
+ children: props.description
42
+ }),
43
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
44
+ className: `${p}-sectionBody`,
45
+ children: [
46
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
47
+ className: `${p}-readOnly`,
60
48
  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
- ]
71
- }) : null]
49
+ children: props.readOnlyNotice
50
+ }) : null,
51
+ props.children,
52
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
53
+ className: `${p}-sectionFooter`,
54
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
55
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
56
+ role: "status",
57
+ children: props.status.text
58
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
59
+ type: "button",
60
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
61
+ disabled: action.disabled === true,
62
+ onClick: action.onClick,
63
+ children: action.label
64
+ }, action.key))]
65
+ })
66
+ ]
67
+ })
68
+ ]
69
+ });
70
+ }
71
+ /** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
72
+ function CardLoading(props) {
73
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
74
+ className: `${props.prefix}-section`,
75
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
76
+ className: `${props.prefix}-readOnly`,
77
+ role: "status",
78
+ children: props.text
79
+ })
72
80
  });
73
81
  }
74
82
  //#endregion
@@ -76,19 +84,14 @@ window.__ModuleLoader__.load({
76
84
  /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
77
85
  const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
78
86
  /**
79
- * 把一张配置卡片同时注册进 legacy 与新版三个槽位。
80
- * 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
87
+ * 把一张配置卡片注册进插件页的两个配置槽位。
88
+ * 两个槽位都由 0.2.1-alpha.1 的插件页声明,任一缺席即报错——不做版本探测。
81
89
  */
82
90
  function injectPluginConfigCard(slots, reg) {
83
91
  const base = {
84
92
  locale: reg.ns,
85
93
  inject: reg.inject
86
94
  };
87
- slots.inject("settings.plugin.item", () => slots.register({
88
- name: "settings.plugin.item",
89
- key: reg.ns,
90
- ...base
91
- }, reg.component));
92
95
  slots.inject("plugins.row.config", () => slots.register({
93
96
  name: "plugins.row.config",
94
97
  key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
@@ -101,6 +104,17 @@ window.__ModuleLoader__.load({
101
104
  }, reg.component));
102
105
  }
103
106
  //#endregion
107
+ //#region ../shared/src/client/draft.ts
108
+ /**
109
+ * 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
110
+ * 脏判定、保存(revision fencing + 重新装载)与状态行。
111
+ *
112
+ * 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
113
+ * 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
114
+ * 依赖,必须是模块级稳定函数(不要内联箭头函数)。
115
+ * @module @dsh-plus/shared/client/draft
116
+ */
117
+ //#endregion
104
118
  //#region ../shared/src/client/fields.tsx
105
119
  function TextField(props) {
106
120
  const p = props.prefix ?? "dshp";
@@ -298,33 +312,33 @@ window.__ModuleLoader__.load({
298
312
  //#endregion
299
313
  //#region ../shared/src/client/styles.ts
300
314
  /**
301
- * 配置卡片样式工厂(各插件 client 的公共收编版)。
302
- * 视觉规则与迁移前逐字一致(类名前缀换为各插件自身,DOM 类名不变),
303
- * 另补移动端与可达性增强(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
315
+ * 配置分节样式工厂(各插件 client 的公共收编版)。
316
+ *
317
+ * 目标宿主 dsh 0.2.1-alpha.1:插件页把配置视图放进已有页面级内边距的容器里
318
+ * (`div.detailSections[data-plugin-config]` 或 `section.detailSection[data-plugin-config]`),
319
+ * 外层无边框——故这里输出的是**无边框分节**(小节标题 13px/600、字段 12px 0、
320
+ * 横向零内边距、页脚非 sticky),度量对齐官方设置页
321
+ * (`@deepseek-ai/dsh-client-ui-primitives` 的 settings-form)。
322
+ *
323
+ * 移动端与可达性(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
304
324
  * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
305
- * - footer 按钮等宽换行、状态文字独占一行;
306
- * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
325
+ * - 页脚按钮等宽换行、状态文字独占一行。
307
326
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
308
327
  * @module @dsh-plus/shared/client/styles
309
328
  */
310
- /** 生成全套卡片 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
329
+ /** 生成全套分节 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
311
330
  function cardCss(prefix, extra = "") {
312
331
  return `
313
- .${prefix}-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
314
- .${prefix}-card:hover{border-color:var(--dsw-alias-label-dimmed)}
315
- .${prefix}-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
316
- .${prefix}-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
317
- .${prefix}-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
318
- .${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
319
- .${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
320
- .${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
321
- .${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}
322
- .${prefix}-chevronOpen{transform:rotate(180deg)}
332
+ .${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
333
+ .${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
334
+ .${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}
335
+ .${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
336
+ .${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
337
+ .${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
338
+ .${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
323
339
  .${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
324
- .${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
325
340
  .${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
326
341
  .${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
327
- .${prefix}-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
328
342
  .${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
329
343
  .${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
330
344
  .${prefix}-head{align-items:center;gap:8px;display:flex}
@@ -353,7 +367,6 @@ window.__ModuleLoader__.load({
353
367
  .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
354
368
  .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
355
369
  .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
356
- .${prefix}-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
357
370
  .${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
358
371
  .${prefix}-statusError{color:var(--dsw-alias-label-error)}
359
372
  .${prefix}-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
@@ -363,14 +376,10 @@ window.__ModuleLoader__.load({
363
376
  .${prefix}-btn:disabled{opacity:.4;cursor:default}
364
377
  .${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
365
378
  @media (max-width:767px){
366
- .${prefix}-body{margin:0 10px}
367
- .${prefix}-header{padding:12px 12px}
368
379
  .${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
369
380
  .${prefix}-checkRow{min-height:44px;padding:8px 0}
370
- .${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
371
381
  .${prefix}-status{flex-basis:100%;flex:none}
372
382
  .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
373
- .${prefix}-statusBadge{display:none}
374
383
  }
375
384
  @media (pointer:coarse){
376
385
  .${prefix}-btn{min-height:44px}
@@ -401,8 +410,8 @@ ${extra}
401
410
  * settings 命名空间字面量(纯常量,零依赖,浏览器半可安全引入)。
402
411
  * 服务端直接以字面量注册(0.1.2-alpha.2 起 dsh-settings 对命名空间做编译期
403
412
  * 语法校验,见 config.ts);浏览器半把它作为配置卡片的注册键
404
- * (legacy settings.plugin.item keyed 槽位按「卡片编辑的 settings 命名空间」
405
- * 分发;0.1.6-alpha.2 起改挂插件页 row/bundle config,见 shared/config-slots),
413
+ * (插件页配置卡片挂在 row/bundle config 槽位上,与命名空间解耦,
414
+ * 见 shared/config-slots),
406
415
  * 同一字面量两处共用,防止漂移。
407
416
  * @module notify-email/ns
408
417
  */
@@ -422,20 +431,18 @@ ${extra}
422
431
  }));
423
432
  }
424
433
  //#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
- */
434
+ //#region src/client/draft.ts
435
+ /** 通知触发点(key 同时是 i18n 文案键与 settings 字段名)。 */
436
+ const TRIGGER_KEYS = [
437
+ "onComplete",
438
+ "onError",
439
+ "onAborted",
440
+ "onQuestion",
441
+ "onPlanReview"
442
+ ];
443
+ function isPositiveInt(text) {
444
+ return /^[0-9]+$/.test(text) && Number(text) > 0;
445
+ }
439
446
  function draftFromValue(value) {
440
447
  return {
441
448
  enabled: value.enabled,
@@ -452,9 +459,7 @@ ${extra}
452
459
  dryRun: value.dryRun
453
460
  };
454
461
  }
455
- function isPositiveInt(text) {
456
- return /^[0-9]+$/.test(text) && Number(text) > 0;
457
- }
462
+ /** 草稿 → 提交形状(pass 为空表示「保持已存密钥」,由调用方剔除该键)。 */
458
463
  function toPatch(draft) {
459
464
  return {
460
465
  enabled: draft.enabled,
@@ -473,27 +478,159 @@ ${extra}
473
478
  dryRun: draft.dryRun
474
479
  };
475
480
  }
476
- const TRIGGER_KEYS = [
477
- "onComplete",
478
- "onError",
479
- "onAborted",
480
- "onQuestion",
481
- "onPlanReview"
482
- ];
483
- function NotifyEmailCard(props) {
484
- const { t, scope, api } = props;
485
- const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
486
- const value = snapshot.value;
487
- const [open, setOpen] = (0, react.useState)(props.view === "page");
488
- const [draft, setDraft] = (0, react.useState)(null);
481
+ //#endregion
482
+ //#region src/client/fields.tsx
483
+ /**
484
+ * 「邮件通知」卡片的字段组:SMTP 连接字段与触发/限流字段。
485
+ * 从 card.tsx 拆出,避免单组件过长;控件走 @dsh-plus/shared/client 套件。
486
+ * @module notify-email/client/fields
487
+ */
488
+ /** SMTP 连接字段(密码为只写:留空保持已存密钥)。 */
489
+ function SmtpFields(props) {
490
+ const { t, draft, disabled, onEdit } = props;
491
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
492
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
493
+ prefix: "dne",
494
+ id: "dne-host",
495
+ label: t("host"),
496
+ hint: t("hostHint"),
497
+ value: draft.host,
498
+ disabled,
499
+ onEdit: (v) => onEdit("host", v)
500
+ }),
501
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
502
+ prefix: "dne",
503
+ id: "dne-port",
504
+ label: t("port"),
505
+ hint: t("portHint"),
506
+ value: draft.port,
507
+ numeric: true,
508
+ disabled,
509
+ invalid: !isPositiveInt(draft.port),
510
+ invalidLabel: t("invalidNumber"),
511
+ onEdit: (v) => onEdit("port", v)
512
+ }),
513
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
514
+ prefix: "dne",
515
+ id: "dne-secure",
516
+ label: t("secure"),
517
+ checked: draft.secure,
518
+ disabled,
519
+ onEdit: (v) => onEdit("secure", v)
520
+ }),
521
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
522
+ prefix: "dne",
523
+ id: "dne-user",
524
+ label: t("user"),
525
+ hint: t("userHint"),
526
+ value: draft.user,
527
+ disabled,
528
+ onEdit: (v) => onEdit("user", v)
529
+ }),
530
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
531
+ prefix: "dne",
532
+ id: "dne-pass",
533
+ label: t("pass"),
534
+ hint: t("passHint"),
535
+ value: draft.pass,
536
+ password: true,
537
+ disabled,
538
+ badge: {
539
+ text: props.passConfigured ? t("passSet") : t("passUnset"),
540
+ set: props.passConfigured
541
+ },
542
+ onEdit: (v) => onEdit("pass", v)
543
+ }),
544
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
545
+ prefix: "dne",
546
+ id: "dne-from",
547
+ label: t("from"),
548
+ hint: t("fromHint"),
549
+ value: draft.from,
550
+ disabled,
551
+ onEdit: (v) => onEdit("from", v)
552
+ })
553
+ ] });
554
+ }
555
+ /** 收件人、触发点开关与限流字段。 */
556
+ function TriggerFields(props) {
557
+ const { t, draft, disabled, onEdit } = props;
558
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
560
+ prefix: "dne",
561
+ id: "dne-to",
562
+ label: t("to"),
563
+ hint: t("toHint"),
564
+ value: draft.toText,
565
+ disabled,
566
+ onEdit: (v) => onEdit("toText", v)
567
+ }),
568
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
569
+ className: "dne-groupLabel",
570
+ children: t("triggerGroup")
571
+ }),
572
+ TRIGGER_KEYS.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
573
+ prefix: "dne",
574
+ id: `dne-${key}`,
575
+ label: t(key),
576
+ checked: draft.triggers[key],
577
+ disabled,
578
+ onEdit: (v) => props.onTrigger(key, v)
579
+ }, key)),
580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
581
+ prefix: "dne",
582
+ id: "dne-debounce",
583
+ label: t("idleDebounceMs"),
584
+ hint: t("idleDebounceMsHint"),
585
+ value: draft.idleDebounceMs,
586
+ numeric: true,
587
+ disabled,
588
+ invalid: !isPositiveInt(draft.idleDebounceMs),
589
+ invalidLabel: t("invalidNumber"),
590
+ onEdit: (v) => onEdit("idleDebounceMs", v)
591
+ }),
592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TextField, {
593
+ prefix: "dne",
594
+ id: "dne-maxchars",
595
+ label: t("maxBodyChars"),
596
+ hint: t("maxBodyCharsHint"),
597
+ value: draft.maxBodyChars,
598
+ numeric: true,
599
+ disabled,
600
+ invalid: !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200,
601
+ invalidLabel: t("invalidNumber"),
602
+ onEdit: (v) => onEdit("maxBodyChars", v)
603
+ }),
604
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
605
+ prefix: "dne",
606
+ id: "dne-dryrun",
607
+ label: t("dryRun"),
608
+ checked: draft.dryRun,
609
+ disabled,
610
+ onEdit: (v) => onEdit("dryRun", v)
611
+ })
612
+ ] });
613
+ }
614
+ //#endregion
615
+ //#region src/client/card.tsx
616
+ /**
617
+ * 「邮件通知」配置卡片:经 injectPluginConfigCard 注册(插件页 plugins.row.config /
618
+ * plugins.bundle.config,view 分发 summary/page)。
619
+ * 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
620
+ * 本文件只保留业务字段与保存/测试逻辑。交互对齐官方卡片:折叠/展开、
621
+ * staged draft、未保存标记、保存/放弃;另加「发送测试邮件」。
622
+ * 配置读写经 ctx.remote.settings 直连(0.1.2-alpha.1 起 connection.api.settings
623
+ * 已移除):value 为 schema 解析后的脱敏视图
624
+ * (smtp.pass 不出现,passConfigured 由 describe 的 secrets 探测),
625
+ * 保存经 settings.update 深合并(空 pass 剔除 = 保持不变)。
626
+ * @module notify-email/client/card
627
+ */
628
+ /**
629
+ * passConfigured 探测:scope 快照不含 secrets,经 describe 的 secrets 列表判断;
630
+ * snapshot 变化(保存后 revision 推进)时刻意重探。
631
+ */
632
+ function usePassConfigured(api, snapshot) {
489
633
  const [passConfigured, setPassConfigured] = (0, react.useState)(false);
490
- const [saving, setSaving] = (0, react.useState)(false);
491
- const [testing, setTesting] = (0, react.useState)(false);
492
- const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
493
- (0, react.useEffect)(() => {
494
- if (value === void 0 || draft !== null) return;
495
- setDraft(draftFromValue(value));
496
- }, [value, draft]);
497
634
  (0, react.useEffect)(() => {
498
635
  let alive = true;
499
636
  api.describe().then((view) => {
@@ -505,15 +642,74 @@ ${extra}
505
642
  alive = false;
506
643
  };
507
644
  }, [api, snapshot]);
645
+ return passConfigured;
646
+ }
647
+ /** 卡片动作:发送测试、放弃、保存(保存受无效字段与只读限制)。 */
648
+ function cardActions(options) {
649
+ const { t } = options;
650
+ return [
651
+ {
652
+ key: "test",
653
+ label: t(options.testing ? "testing" : "test"),
654
+ disabled: options.testing,
655
+ onClick: options.onTest
656
+ },
657
+ {
658
+ key: "discard",
659
+ label: t("discard"),
660
+ disabled: !options.dirty || options.saving,
661
+ onClick: options.onDiscard
662
+ },
663
+ {
664
+ key: "save",
665
+ label: t(options.saving ? "saving" : "save"),
666
+ variant: "primary",
667
+ disabled: !options.dirty || options.invalid || options.saving || options.disabled,
668
+ onClick: options.onSave
669
+ }
670
+ ];
671
+ }
672
+ /** 发送测试邮件:端点结果折算成状态行文案(不改草稿);finally 收尾由调用方给。 */
673
+ function sendTestMail(t, onStatus, onSettled) {
674
+ sendTest().then((result) => {
675
+ if (result.ok) {
676
+ onStatus({
677
+ kind: "ok",
678
+ text: result.detail === "dry-run" ? t("testDryRun") : t("testOk")
679
+ });
680
+ return;
681
+ }
682
+ const why = result.detail === "incomplete" ? t("incomplete") : result.detail;
683
+ onStatus({
684
+ kind: "error",
685
+ text: `${t("testFailed")}${why}`
686
+ });
687
+ }).catch((error) => {
688
+ onStatus({
689
+ kind: "error",
690
+ text: `${t("testFailed")}${error instanceof Error ? error.message : ""}`
691
+ });
692
+ }).finally(onSettled);
693
+ }
694
+ function NotifyEmailCard(props) {
695
+ const { t, scope, api } = props;
696
+ const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
697
+ const value = snapshot.value;
698
+ const [draft, setDraft] = (0, react.useState)(null);
699
+ const [saving, setSaving] = (0, react.useState)(false);
700
+ const [testing, setTesting] = (0, react.useState)(false);
701
+ const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
702
+ const passConfigured = usePassConfigured(api, snapshot);
703
+ (0, react.useEffect)(() => {
704
+ if (value === void 0 || draft !== null) return;
705
+ setDraft(draftFromValue(value));
706
+ }, [value, draft]);
508
707
  const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
509
708
  const invalid = (0, react.useMemo)(() => draft !== null && (!isPositiveInt(draft.port) || !isPositiveInt(draft.idleDebounceMs) || !isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200), [draft]);
510
709
  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
- })
710
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
711
+ prefix: "dne",
712
+ text: t("loading")
517
713
  });
518
714
  const edit = (key, editValue) => {
519
715
  setDraft({
@@ -548,32 +744,13 @@ ${extra}
548
744
  };
549
745
  const onTest = () => {
550
746
  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));
747
+ sendTestMail(t, setStatus, () => setTesting(false));
569
748
  };
570
749
  const disabled = !snapshot.writable;
571
750
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
572
751
  prefix: "dne",
573
752
  title: t("title"),
574
753
  description: t("description"),
575
- open,
576
- onToggle: setOpen,
577
754
  statusBadge: {
578
755
  text: t(draft.enabled ? "enabledOn" : "enabledOff"),
579
756
  on: draft.enabled
@@ -582,30 +759,20 @@ ${extra}
582
759
  dirtyLabel: t("unsaved"),
583
760
  readOnlyNotice: disabled ? t("readOnly") : void 0,
584
761
  status,
585
- actions: [
586
- {
587
- key: "test",
588
- label: t(testing ? "testing" : "test"),
589
- disabled: testing,
590
- onClick: onTest
591
- },
592
- {
593
- key: "discard",
594
- label: t("discard"),
595
- disabled: !dirty || saving,
596
- onClick: () => {
597
- setDraft(draftFromValue(value));
598
- setStatus(IDLE_STATUS);
599
- }
762
+ actions: cardActions({
763
+ t,
764
+ dirty,
765
+ invalid,
766
+ saving,
767
+ testing,
768
+ disabled,
769
+ onTest,
770
+ onDiscard: () => {
771
+ setDraft(draftFromValue(value));
772
+ setStatus(IDLE_STATUS);
600
773
  },
601
- {
602
- key: "save",
603
- label: t(saving ? "saving" : "save"),
604
- variant: "primary",
605
- disabled: !dirty || invalid || saving || disabled,
606
- onClick: onSave
607
- }
608
- ],
774
+ onSave
775
+ }),
609
776
  children: [
610
777
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
611
778
  prefix: "dne",
@@ -615,119 +782,19 @@ ${extra}
615
782
  disabled,
616
783
  onEdit: (v) => edit("enabled", v)
617
784
  }),
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,
785
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SmtpFields, {
786
+ t,
787
+ draft,
663
788
  disabled,
664
- badge: {
665
- text: passConfigured ? t("passSet") : t("passUnset"),
666
- set: passConfigured
667
- },
668
- onEdit: (v) => edit("pass", v)
789
+ passConfigured,
790
+ onEdit: edit
669
791
  }),
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,
676
- 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")
691
- }),
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,
792
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TriggerFields, {
793
+ t,
794
+ draft,
729
795
  disabled,
730
- onEdit: (v) => edit("dryRun", v)
796
+ onEdit: edit,
797
+ onTrigger: editTrigger
731
798
  })
732
799
  ]
733
800
  });