@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 +349 -282
- package/package.json +2 -2
- package/src/client/card.tsx +124 -264
- package/src/client/client.ts +2 -2
- package/src/client/draft.ts +99 -0
- package/src/client/fields.tsx +153 -0
- package/src/ns.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/notify-email",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 任务结束/等待决策/出错时向指定邮箱发送邮件通知",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"nodemailer": "9.0.6",
|
|
35
|
-
"@dsh-plus/shared": "0.1.
|
|
35
|
+
"@dsh-plus/shared": "0.1.32"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.4",
|
package/src/client/card.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 「邮件通知」配置卡片:经 injectPluginConfigCard
|
|
3
|
-
*
|
|
4
|
-
* summary/page)。
|
|
2
|
+
* 「邮件通知」配置卡片:经 injectPluginConfigCard 注册(插件页 plugins.row.config /
|
|
3
|
+
* plugins.bundle.config,view 分发 summary/page)。
|
|
5
4
|
* 外壳与基础控件走 @dsh-plus/shared/client 套件(CardChrome/TextField/CheckRow),
|
|
6
5
|
* 本文件只保留业务字段与保存/测试逻辑。交互对齐官方卡片:折叠/展开、
|
|
7
6
|
* staged draft、未保存标记、保存/放弃;另加「发送测试邮件」。
|
|
@@ -13,18 +12,28 @@
|
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import {
|
|
15
|
+
type CardAction,
|
|
16
16
|
CardChrome,
|
|
17
|
+
CardLoading,
|
|
17
18
|
type CardStatusState,
|
|
18
19
|
CheckRow,
|
|
19
20
|
IDLE_STATUS,
|
|
20
21
|
type NamespaceSettingsApi,
|
|
21
22
|
type PluginConfigViewProps,
|
|
22
23
|
type Scope,
|
|
23
|
-
TextField,
|
|
24
24
|
} from '@dsh-plus/shared/client'
|
|
25
25
|
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
26
26
|
import { SETTINGS_NS } from '../ns.ts'
|
|
27
27
|
import { sendTest } from './api.ts'
|
|
28
|
+
import {
|
|
29
|
+
type ConfigValue,
|
|
30
|
+
type Draft,
|
|
31
|
+
draftFromValue,
|
|
32
|
+
isPositiveInt,
|
|
33
|
+
type TriggerKey,
|
|
34
|
+
toPatch,
|
|
35
|
+
} from './draft.ts'
|
|
36
|
+
import { SmtpFields, TriggerFields } from './fields.tsx'
|
|
28
37
|
import type { Translate } from './i18n.ts'
|
|
29
38
|
|
|
30
39
|
export interface CardProps extends PluginConfigViewProps {
|
|
@@ -33,88 +42,95 @@ export interface CardProps extends PluginConfigViewProps {
|
|
|
33
42
|
api: NamespaceSettingsApi
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
host: string
|
|
62
|
-
port: string
|
|
63
|
-
secure: boolean
|
|
64
|
-
user: string
|
|
65
|
-
pass: string
|
|
66
|
-
from: string
|
|
67
|
-
toText: string
|
|
68
|
-
triggers: ConfigValue['triggers']
|
|
69
|
-
idleDebounceMs: string
|
|
70
|
-
maxBodyChars: string
|
|
71
|
-
dryRun: boolean
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function draftFromValue(value: ConfigValue): Draft {
|
|
75
|
-
return {
|
|
76
|
-
enabled: value.enabled,
|
|
77
|
-
host: value.smtp.host,
|
|
78
|
-
port: String(value.smtp.port),
|
|
79
|
-
secure: value.smtp.secure,
|
|
80
|
-
user: value.smtp.user,
|
|
81
|
-
pass: '',
|
|
82
|
-
from: value.smtp.from,
|
|
83
|
-
toText: value.to.join(', '),
|
|
84
|
-
triggers: { ...value.triggers },
|
|
85
|
-
idleDebounceMs: String(value.idleDebounceMs),
|
|
86
|
-
maxBodyChars: String(value.maxBodyChars),
|
|
87
|
-
dryRun: value.dryRun,
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function isPositiveInt(text: string): boolean {
|
|
92
|
-
return /^[0-9]+$/.test(text) && Number(text) > 0
|
|
45
|
+
/**
|
|
46
|
+
* passConfigured 探测:scope 快照不含 secrets,经 describe 的 secrets 列表判断;
|
|
47
|
+
* snapshot 变化(保存后 revision 推进)时刻意重探。
|
|
48
|
+
*/
|
|
49
|
+
function usePassConfigured(api: NamespaceSettingsApi, snapshot: unknown): boolean {
|
|
50
|
+
const [passConfigured, setPassConfigured] = useState(false)
|
|
51
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: snapshot 变化(保存后 revision 推进)时刻意重探 secrets 状态
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
let alive = true
|
|
54
|
+
api
|
|
55
|
+
.describe()
|
|
56
|
+
.then((view) => {
|
|
57
|
+
if (!alive) return
|
|
58
|
+
const ns = view.namespaces.find((candidate) => candidate.ns === SETTINGS_NS)
|
|
59
|
+
setPassConfigured(
|
|
60
|
+
ns?.secrets.some((secret) => secret.path.join('.') === 'smtp.pass' && secret.set) ??
|
|
61
|
+
false,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
.catch(() => {})
|
|
65
|
+
return () => {
|
|
66
|
+
alive = false
|
|
67
|
+
}
|
|
68
|
+
}, [api, snapshot])
|
|
69
|
+
return passConfigured
|
|
93
70
|
}
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
72
|
+
/** 卡片动作:发送测试、放弃、保存(保存受无效字段与只读限制)。 */
|
|
73
|
+
function cardActions(options: {
|
|
74
|
+
t: Translate
|
|
75
|
+
dirty: boolean
|
|
76
|
+
invalid: boolean
|
|
77
|
+
saving: boolean
|
|
78
|
+
testing: boolean
|
|
79
|
+
disabled: boolean
|
|
80
|
+
onTest(): void
|
|
81
|
+
onDiscard(): void
|
|
82
|
+
onSave(): void
|
|
83
|
+
}): CardAction[] {
|
|
84
|
+
const { t } = options
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
key: 'test',
|
|
88
|
+
label: t(options.testing ? 'testing' : 'test'),
|
|
89
|
+
disabled: options.testing,
|
|
90
|
+
onClick: options.onTest,
|
|
105
91
|
},
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
{
|
|
93
|
+
key: 'discard',
|
|
94
|
+
label: t('discard'),
|
|
95
|
+
disabled: !options.dirty || options.saving,
|
|
96
|
+
onClick: options.onDiscard,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: 'save',
|
|
100
|
+
label: t(options.saving ? 'saving' : 'save'),
|
|
101
|
+
variant: 'primary',
|
|
102
|
+
disabled: !options.dirty || options.invalid || options.saving || options.disabled,
|
|
103
|
+
onClick: options.onSave,
|
|
104
|
+
},
|
|
105
|
+
]
|
|
115
106
|
}
|
|
116
107
|
|
|
117
|
-
|
|
108
|
+
/** 发送测试邮件:端点结果折算成状态行文案(不改草稿);finally 收尾由调用方给。 */
|
|
109
|
+
function sendTestMail(
|
|
110
|
+
t: Translate,
|
|
111
|
+
onStatus: (status: CardStatusState) => void,
|
|
112
|
+
onSettled: () => void,
|
|
113
|
+
): void {
|
|
114
|
+
sendTest()
|
|
115
|
+
.then((result) => {
|
|
116
|
+
if (result.ok) {
|
|
117
|
+
onStatus({
|
|
118
|
+
kind: 'ok',
|
|
119
|
+
text: result.detail === 'dry-run' ? t('testDryRun') : t('testOk'),
|
|
120
|
+
})
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
const why = result.detail === 'incomplete' ? t('incomplete') : result.detail
|
|
124
|
+
onStatus({ kind: 'error', text: `${t('testFailed')}${why}` })
|
|
125
|
+
})
|
|
126
|
+
.catch((error: unknown) => {
|
|
127
|
+
onStatus({
|
|
128
|
+
kind: 'error',
|
|
129
|
+
text: `${t('testFailed')}${error instanceof Error ? error.message : ''}`,
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
.finally(onSettled)
|
|
133
|
+
}
|
|
118
134
|
|
|
119
135
|
export function NotifyEmailCard(props: CardProps): ReactElement | string | null {
|
|
120
136
|
const { t, scope, api } = props
|
|
@@ -123,13 +139,11 @@ export function NotifyEmailCard(props: CardProps): ReactElement | string | null
|
|
|
123
139
|
() => scope.getSnapshot(),
|
|
124
140
|
)
|
|
125
141
|
const value = snapshot.value as ConfigValue | undefined
|
|
126
|
-
// 0.1.6-alpha.2 插件页 page 视图为表单落地页,默认展开;旧槽位无 view,保持折叠。
|
|
127
|
-
const [open, setOpen] = useState(props.view === 'page')
|
|
128
142
|
const [draft, setDraft] = useState<Draft | null>(null)
|
|
129
|
-
const [passConfigured, setPassConfigured] = useState(false)
|
|
130
143
|
const [saving, setSaving] = useState(false)
|
|
131
144
|
const [testing, setTesting] = useState(false)
|
|
132
145
|
const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
|
|
146
|
+
const passConfigured = usePassConfigured(api, snapshot)
|
|
133
147
|
|
|
134
148
|
// 首次拿到解析值后播种草稿;后续 Host 更新不覆盖在途编辑(与官方 staged 表单一致)。
|
|
135
149
|
useEffect(() => {
|
|
@@ -137,26 +151,6 @@ export function NotifyEmailCard(props: CardProps): ReactElement | string | null
|
|
|
137
151
|
setDraft(draftFromValue(value))
|
|
138
152
|
}, [value, draft])
|
|
139
153
|
|
|
140
|
-
// passConfigured 探测:scope 快照不含 secrets,经 describe 的 secrets 列表判断。
|
|
141
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: snapshot 变化(保存后 revision 推进)时刻意重探 secrets 状态
|
|
142
|
-
useEffect(() => {
|
|
143
|
-
let alive = true
|
|
144
|
-
api
|
|
145
|
-
.describe()
|
|
146
|
-
.then((view) => {
|
|
147
|
-
if (!alive) return
|
|
148
|
-
const ns = view.namespaces.find((candidate) => candidate.ns === SETTINGS_NS)
|
|
149
|
-
setPassConfigured(
|
|
150
|
-
ns?.secrets.some((secret) => secret.path.join('.') === 'smtp.pass' && secret.set) ??
|
|
151
|
-
false,
|
|
152
|
-
)
|
|
153
|
-
})
|
|
154
|
-
.catch(() => {})
|
|
155
|
-
return () => {
|
|
156
|
-
alive = false
|
|
157
|
-
}
|
|
158
|
-
}, [api, snapshot])
|
|
159
|
-
|
|
160
154
|
const dirty = useMemo(
|
|
161
155
|
() =>
|
|
162
156
|
value !== undefined &&
|
|
@@ -178,17 +172,13 @@ export function NotifyEmailCard(props: CardProps): ReactElement | string | null
|
|
|
178
172
|
if (props.view === 'summary') return t('summaryLine')
|
|
179
173
|
|
|
180
174
|
if (value === undefined || draft === null) {
|
|
181
|
-
return (
|
|
182
|
-
<li className="dne-card">
|
|
183
|
-
<p className="dne-readOnly">{t('loading')}</p>
|
|
184
|
-
</li>
|
|
185
|
-
)
|
|
175
|
+
return <CardLoading prefix="dne" text={t('loading')} />
|
|
186
176
|
}
|
|
187
177
|
const edit = <K extends keyof Draft>(key: K, editValue: Draft[K]): void => {
|
|
188
178
|
setDraft({ ...draft, [key]: editValue })
|
|
189
179
|
setStatus(IDLE_STATUS)
|
|
190
180
|
}
|
|
191
|
-
const editTrigger = (key:
|
|
181
|
+
const editTrigger = (key: TriggerKey, checked: boolean): void => {
|
|
192
182
|
edit('triggers', { ...draft.triggers, [key]: checked })
|
|
193
183
|
}
|
|
194
184
|
const onSave = (): void => {
|
|
@@ -214,25 +204,7 @@ export function NotifyEmailCard(props: CardProps): ReactElement | string | null
|
|
|
214
204
|
}
|
|
215
205
|
const onTest = (): void => {
|
|
216
206
|
setTesting(true)
|
|
217
|
-
|
|
218
|
-
.then((result) => {
|
|
219
|
-
if (result.ok) {
|
|
220
|
-
setStatus({
|
|
221
|
-
kind: 'ok',
|
|
222
|
-
text: result.detail === 'dry-run' ? t('testDryRun') : t('testOk'),
|
|
223
|
-
})
|
|
224
|
-
} else {
|
|
225
|
-
const why = result.detail === 'incomplete' ? t('incomplete') : result.detail
|
|
226
|
-
setStatus({ kind: 'error', text: `${t('testFailed')}${why}` })
|
|
227
|
-
}
|
|
228
|
-
})
|
|
229
|
-
.catch((error: unknown) => {
|
|
230
|
-
setStatus({
|
|
231
|
-
kind: 'error',
|
|
232
|
-
text: `${t('testFailed')}${error instanceof Error ? error.message : ''}`,
|
|
233
|
-
})
|
|
234
|
-
})
|
|
235
|
-
.finally(() => setTesting(false))
|
|
207
|
+
sendTestMail(t, setStatus, () => setTesting(false))
|
|
236
208
|
}
|
|
237
209
|
|
|
238
210
|
const disabled = !snapshot.writable
|
|
@@ -241,37 +213,25 @@ export function NotifyEmailCard(props: CardProps): ReactElement | string | null
|
|
|
241
213
|
prefix="dne"
|
|
242
214
|
title={t('title')}
|
|
243
215
|
description={t('description')}
|
|
244
|
-
open={open}
|
|
245
|
-
onToggle={setOpen}
|
|
246
216
|
statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
|
|
247
217
|
dirty={dirty}
|
|
248
218
|
dirtyLabel={t('unsaved')}
|
|
249
219
|
readOnlyNotice={disabled ? t('readOnly') : undefined}
|
|
250
220
|
status={status}
|
|
251
|
-
actions={
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
onClick: () => {
|
|
263
|
-
setDraft(draftFromValue(value))
|
|
264
|
-
setStatus(IDLE_STATUS)
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
key: 'save',
|
|
269
|
-
label: t(saving ? 'saving' : 'save'),
|
|
270
|
-
variant: 'primary',
|
|
271
|
-
disabled: !dirty || invalid || saving || disabled,
|
|
272
|
-
onClick: onSave,
|
|
221
|
+
actions={cardActions({
|
|
222
|
+
t,
|
|
223
|
+
dirty,
|
|
224
|
+
invalid,
|
|
225
|
+
saving,
|
|
226
|
+
testing,
|
|
227
|
+
disabled,
|
|
228
|
+
onTest,
|
|
229
|
+
onDiscard: () => {
|
|
230
|
+
setDraft(draftFromValue(value))
|
|
231
|
+
setStatus(IDLE_STATUS)
|
|
273
232
|
},
|
|
274
|
-
|
|
233
|
+
onSave,
|
|
234
|
+
})}
|
|
275
235
|
>
|
|
276
236
|
<CheckRow
|
|
277
237
|
prefix="dne"
|
|
@@ -281,119 +241,19 @@ export function NotifyEmailCard(props: CardProps): ReactElement | string | null
|
|
|
281
241
|
disabled={disabled}
|
|
282
242
|
onEdit={(v) => edit('enabled', v)}
|
|
283
243
|
/>
|
|
284
|
-
<
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
label={t('host')}
|
|
288
|
-
hint={t('hostHint')}
|
|
289
|
-
value={draft.host}
|
|
290
|
-
disabled={disabled}
|
|
291
|
-
onEdit={(v) => edit('host', v)}
|
|
292
|
-
/>
|
|
293
|
-
<TextField
|
|
294
|
-
prefix="dne"
|
|
295
|
-
id="dne-port"
|
|
296
|
-
label={t('port')}
|
|
297
|
-
hint={t('portHint')}
|
|
298
|
-
value={draft.port}
|
|
299
|
-
numeric
|
|
300
|
-
disabled={disabled}
|
|
301
|
-
invalid={!isPositiveInt(draft.port)}
|
|
302
|
-
invalidLabel={t('invalidNumber')}
|
|
303
|
-
onEdit={(v) => edit('port', v)}
|
|
304
|
-
/>
|
|
305
|
-
<CheckRow
|
|
306
|
-
prefix="dne"
|
|
307
|
-
id="dne-secure"
|
|
308
|
-
label={t('secure')}
|
|
309
|
-
checked={draft.secure}
|
|
310
|
-
disabled={disabled}
|
|
311
|
-
onEdit={(v) => edit('secure', v)}
|
|
312
|
-
/>
|
|
313
|
-
<TextField
|
|
314
|
-
prefix="dne"
|
|
315
|
-
id="dne-user"
|
|
316
|
-
label={t('user')}
|
|
317
|
-
hint={t('userHint')}
|
|
318
|
-
value={draft.user}
|
|
319
|
-
disabled={disabled}
|
|
320
|
-
onEdit={(v) => edit('user', v)}
|
|
321
|
-
/>
|
|
322
|
-
<TextField
|
|
323
|
-
prefix="dne"
|
|
324
|
-
id="dne-pass"
|
|
325
|
-
label={t('pass')}
|
|
326
|
-
hint={t('passHint')}
|
|
327
|
-
value={draft.pass}
|
|
328
|
-
password
|
|
329
|
-
disabled={disabled}
|
|
330
|
-
badge={{
|
|
331
|
-
text: passConfigured ? t('passSet') : t('passUnset'),
|
|
332
|
-
set: passConfigured,
|
|
333
|
-
}}
|
|
334
|
-
onEdit={(v) => edit('pass', v)}
|
|
335
|
-
/>
|
|
336
|
-
<TextField
|
|
337
|
-
prefix="dne"
|
|
338
|
-
id="dne-from"
|
|
339
|
-
label={t('from')}
|
|
340
|
-
hint={t('fromHint')}
|
|
341
|
-
value={draft.from}
|
|
244
|
+
<SmtpFields
|
|
245
|
+
t={t}
|
|
246
|
+
draft={draft}
|
|
342
247
|
disabled={disabled}
|
|
343
|
-
|
|
248
|
+
passConfigured={passConfigured}
|
|
249
|
+
onEdit={edit}
|
|
344
250
|
/>
|
|
345
|
-
<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
label={t('to')}
|
|
349
|
-
hint={t('toHint')}
|
|
350
|
-
value={draft.toText}
|
|
351
|
-
disabled={disabled}
|
|
352
|
-
onEdit={(v) => edit('toText', v)}
|
|
353
|
-
/>
|
|
354
|
-
<p className="dne-groupLabel">{t('triggerGroup')}</p>
|
|
355
|
-
{TRIGGER_KEYS.map((key) => (
|
|
356
|
-
<CheckRow
|
|
357
|
-
prefix="dne"
|
|
358
|
-
key={key}
|
|
359
|
-
id={`dne-${key}`}
|
|
360
|
-
label={t(key)}
|
|
361
|
-
checked={draft.triggers[key]}
|
|
362
|
-
disabled={disabled}
|
|
363
|
-
onEdit={(v) => editTrigger(key, v)}
|
|
364
|
-
/>
|
|
365
|
-
))}
|
|
366
|
-
<TextField
|
|
367
|
-
prefix="dne"
|
|
368
|
-
id="dne-debounce"
|
|
369
|
-
label={t('idleDebounceMs')}
|
|
370
|
-
hint={t('idleDebounceMsHint')}
|
|
371
|
-
value={draft.idleDebounceMs}
|
|
372
|
-
numeric
|
|
373
|
-
disabled={disabled}
|
|
374
|
-
invalid={!isPositiveInt(draft.idleDebounceMs)}
|
|
375
|
-
invalidLabel={t('invalidNumber')}
|
|
376
|
-
onEdit={(v) => edit('idleDebounceMs', v)}
|
|
377
|
-
/>
|
|
378
|
-
<TextField
|
|
379
|
-
prefix="dne"
|
|
380
|
-
id="dne-maxchars"
|
|
381
|
-
label={t('maxBodyChars')}
|
|
382
|
-
hint={t('maxBodyCharsHint')}
|
|
383
|
-
value={draft.maxBodyChars}
|
|
384
|
-
numeric
|
|
385
|
-
disabled={disabled}
|
|
386
|
-
invalid={!isPositiveInt(draft.maxBodyChars) || Number(draft.maxBodyChars) < 200}
|
|
387
|
-
invalidLabel={t('invalidNumber')}
|
|
388
|
-
onEdit={(v) => edit('maxBodyChars', v)}
|
|
389
|
-
/>
|
|
390
|
-
<CheckRow
|
|
391
|
-
prefix="dne"
|
|
392
|
-
id="dne-dryrun"
|
|
393
|
-
label={t('dryRun')}
|
|
394
|
-
checked={draft.dryRun}
|
|
251
|
+
<TriggerFields
|
|
252
|
+
t={t}
|
|
253
|
+
draft={draft}
|
|
395
254
|
disabled={disabled}
|
|
396
|
-
onEdit={
|
|
255
|
+
onEdit={edit}
|
|
256
|
+
onTrigger={editTrigger}
|
|
397
257
|
/>
|
|
398
258
|
</CardChrome>
|
|
399
259
|
)
|
package/src/client/client.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 浏览器半入口:注册 locale 字典 + 注册「邮件通知」卡片
|
|
3
|
-
* (injectPluginConfigCard
|
|
4
|
-
*
|
|
3
|
+
* (injectPluginConfigCard 两槽位:插件页 plugins.row.config /
|
|
4
|
+
* plugins.bundle.config)。
|
|
5
5
|
* 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
|
|
6
6
|
* (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
|
|
7
7
|
*
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 卡片草稿模型与纯校验(无 React/JSX,供 node --test 直接导入):
|
|
3
|
+
* 脱敏解析值 → 草稿、草稿 → 提交形状、触发键表与正整数判定。
|
|
4
|
+
* @module notify-email/client/draft
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** settings 命名空间的脱敏解析值(smtp.pass 被脱敏剥除)。 */
|
|
8
|
+
export interface ConfigValue {
|
|
9
|
+
enabled: boolean
|
|
10
|
+
smtp: {
|
|
11
|
+
host: string
|
|
12
|
+
port: number
|
|
13
|
+
secure: boolean
|
|
14
|
+
user: string
|
|
15
|
+
from: string
|
|
16
|
+
}
|
|
17
|
+
to: string[]
|
|
18
|
+
triggers: {
|
|
19
|
+
onComplete: boolean
|
|
20
|
+
onError: boolean
|
|
21
|
+
onAborted: boolean
|
|
22
|
+
onQuestion: boolean
|
|
23
|
+
onPlanReview: boolean
|
|
24
|
+
}
|
|
25
|
+
idleDebounceMs: number
|
|
26
|
+
maxBodyChars: number
|
|
27
|
+
dryRun: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 编辑草稿:数值字段以文本承载(保存时校验并折算)。 */
|
|
31
|
+
export interface Draft {
|
|
32
|
+
enabled: boolean
|
|
33
|
+
host: string
|
|
34
|
+
port: string
|
|
35
|
+
secure: boolean
|
|
36
|
+
user: string
|
|
37
|
+
pass: string
|
|
38
|
+
from: string
|
|
39
|
+
toText: string
|
|
40
|
+
triggers: ConfigValue['triggers']
|
|
41
|
+
idleDebounceMs: string
|
|
42
|
+
maxBodyChars: string
|
|
43
|
+
dryRun: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 通知触发点(key 同时是 i18n 文案键与 settings 字段名)。 */
|
|
47
|
+
export const TRIGGER_KEYS = [
|
|
48
|
+
'onComplete',
|
|
49
|
+
'onError',
|
|
50
|
+
'onAborted',
|
|
51
|
+
'onQuestion',
|
|
52
|
+
'onPlanReview',
|
|
53
|
+
] as const
|
|
54
|
+
|
|
55
|
+
export type TriggerKey = (typeof TRIGGER_KEYS)[number]
|
|
56
|
+
|
|
57
|
+
export function isPositiveInt(text: string): boolean {
|
|
58
|
+
return /^[0-9]+$/.test(text) && Number(text) > 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function draftFromValue(value: ConfigValue): Draft {
|
|
62
|
+
return {
|
|
63
|
+
enabled: value.enabled,
|
|
64
|
+
host: value.smtp.host,
|
|
65
|
+
port: String(value.smtp.port),
|
|
66
|
+
secure: value.smtp.secure,
|
|
67
|
+
user: value.smtp.user,
|
|
68
|
+
pass: '',
|
|
69
|
+
from: value.smtp.from,
|
|
70
|
+
toText: value.to.join(', '),
|
|
71
|
+
triggers: { ...value.triggers },
|
|
72
|
+
idleDebounceMs: String(value.idleDebounceMs),
|
|
73
|
+
maxBodyChars: String(value.maxBodyChars),
|
|
74
|
+
dryRun: value.dryRun,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 草稿 → 提交形状(pass 为空表示「保持已存密钥」,由调用方剔除该键)。 */
|
|
79
|
+
export function toPatch(draft: Draft): Record<string, unknown> {
|
|
80
|
+
return {
|
|
81
|
+
enabled: draft.enabled,
|
|
82
|
+
smtp: {
|
|
83
|
+
host: draft.host.trim(),
|
|
84
|
+
port: Number(draft.port),
|
|
85
|
+
secure: draft.secure,
|
|
86
|
+
user: draft.user.trim(),
|
|
87
|
+
pass: draft.pass,
|
|
88
|
+
from: draft.from.trim(),
|
|
89
|
+
},
|
|
90
|
+
to: draft.toText
|
|
91
|
+
.split(',')
|
|
92
|
+
.map((s) => s.trim())
|
|
93
|
+
.filter((s) => s.length > 0),
|
|
94
|
+
triggers: { ...draft.triggers },
|
|
95
|
+
idleDebounceMs: Number(draft.idleDebounceMs),
|
|
96
|
+
maxBodyChars: Number(draft.maxBodyChars),
|
|
97
|
+
dryRun: draft.dryRun,
|
|
98
|
+
}
|
|
99
|
+
}
|