@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 +375 -216
- package/package.json +2 -2
- package/src/client/card.tsx +127 -257
- package/src/client/draft.ts +99 -0
- package/src/client/fields.tsx +153 -0
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.
|
|
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/
|
|
426
|
-
/**
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
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)(
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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:
|
|
888
|
+
onEdit: edit,
|
|
889
|
+
onTrigger: editTrigger
|
|
731
890
|
})
|
|
732
891
|
]
|
|
733
892
|
});
|