feat(notify): test media configs and preview templates without saving - #2232
Conversation
Add a test dialog to the notification channel form so a config can be verified before it is saved, driven by either a built-in mock event or a real history event. Script media are excluded, since the backend requires them to be saved first. The expected template fields are derived from the media's outbound request, scanning body, URL, headers and query parameters, because the backend exposes $tpl in all four. Message template preview gains the same mock-event mode, so a fresh install with no alert history can still preview, and renders template errors as an error alert instead of feeding Go's parse error through the markdown/HTML renderers, which stripped parts of it and left a truncated message. Replace the field table in the docs pane with a searchable, click-to-copy field panel backed by structured data, and drop the corresponding section from the four locale docs so the list is no longer maintained in two places. New templates are seeded with starter content keyed to the selected media instead of dropping the user into an empty editor, and the list selects the template that was just created or cloned. Cloning no longer overwrites the copied body for smtp channels. Also fixes on the channel list: the three filters now intersect instead of short-circuiting on the first match, the enable toggle re-reads the record before writing it back so a stale snapshot cannot clobber a concurrent edit, unknown idents no longer borrow the Callback logo and doc page, and a failed import now reports the failure instead of logging it silently.
📝 WalkthroughWalkthroughThe PR adds notification-channel testing with historical and mock events, improves channel metadata and list operations, and adds searchable template fields, starter content, mock previews, empty states, localized UI, and updated template documentation. ChangesNotification channel workflows
Notification template workflows
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ChannelForm
participant TestModal
participant testItem
ChannelForm->>TestModal: validate and open
TestModal->>testItem: submit configuration and event source
testItem-->>TestModal: return test result
TestModal-->>ChannelForm: display success or error
sequenceDiagram
participant PreviewModal
participant preview
participant TemplateFields
PreviewModal->>preview: submit history IDs or mock event
preview-->>PreviewModal: return typed field results
PreviewModal->>TemplateFields: render fields in template order
TemplateFields-->>PreviewModal: display content or template error
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (5)
src/pages/notificationChannels/pages/Form/index.tsx (1)
172-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared validation-error handler.
The same error branch appears in the save
catchand inonBeforeOpen. Extract one helper and use it in both places.♻️ Proposed refactor
+ // 校验失败带 errorFields,由 antd 自行标红;其余异常才是真错误 + const handleValidateError = (err: any) => { + if (!err?.errorFields) { + console.error(err); + } + scrollToFirstError(); + };- .catch((err) => { - // 校验失败带 errorFields,由 antd 自行标红;其余异常才是真错误, - // 一并 console.error 会让真异常淹没在日常的校验失败里 - if (!err?.errorFields) { - console.error(err); - } - scrollToFirstError(); - }); + .catch(handleValidateError);onBeforeOpen={() => - form.validateFields().catch((err) => { - if (!err?.errorFields) { - console.error(err); - } - scrollToFirstError(); - throw err; - }) + form.validateFields().catch((err) => { + handleValidateError(err); + throw err; + }) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/notificationChannels/pages/Form/index.tsx` around lines 172 - 196, Extract the duplicated validation-error handling from the save promise catch and TestModal’s onBeforeOpen into a shared helper near the surrounding component logic. The helper should conditionally console.error non-validation errors, call scrollToFirstError, and be reused by both handlers while preserving the existing rethrow behavior in onBeforeOpen.src/pages/notificationChannels/pages/Form/TestModal/index.tsx (1)
140-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueShow the blocked reason with a
Tooltip.The native
titleattribute is unreliable on a disabled button and is not reachable by keyboard. Wrap the button in an antdTooltipso thetest.script_blockedreason is always visible.♻️ Proposed change
- <Button - ghost - type='primary' - icon={<ExperimentOutlined />} - disabled={scriptBlocked} - title={scriptBlocked ? t('test.script_blocked') : undefined} - onClick={() => { + <Tooltip title={scriptBlocked ? t('test.script_blocked') : undefined}> + <Button + ghost + type='primary' + icon={<ExperimentOutlined />} + disabled={scriptBlocked} + onClick={() => {Close the
Tooltipafter the button and importTooltipfromantd.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/notificationChannels/pages/Form/TestModal/index.tsx` around lines 140 - 157, Update the test button rendering in TestModal to import and wrap the disabled Button with an antd Tooltip, using t('test.script_blocked') as its content when scriptBlocked. Remove the native title attribute and ensure the Tooltip remains available for the blocked-state reason, while preserving the existing click and disabled behavior.src/pages/notificationTemplates/pages/List/index.tsx (1)
208-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExpress the height cap with a Tailwind class.
Line 214 sets
maxHeightthrough an inline style while the same element controls layout with Tailwind classes. The repository convention is to express layout, spacing, and sizing with Tailwind and to avoid splitting one visual property between systems.♻️ Proposed change
- <div className='flex min-h-0 flex-none flex-col' style={{ maxHeight: '62%' }}> + <div className='flex min-h-0 max-h-[62%] flex-none flex-col'> <FieldsPanel /> </div>As per coding guidelines: "能直接用 Tailwind 表达的布局、间距、排版、圆角和常见交互态应使用 Tailwind。"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/notificationTemplates/pages/List/index.tsx` around lines 208 - 222, Replace the inline maxHeight styling on the FieldsPanel container with the equivalent Tailwind arbitrary-value class, keeping the existing flex layout and 62% height cap unchanged.Source: Coding guidelines
src/pages/notificationTemplates/pages/List/Form/PreviewModal/index.tsx (1)
40-59: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a loading state for the preview request.
The effect clears
previewDataon close and does not track the request state. Between opening the result modal and the response, every field renders with an empty string. The user sees empty editors that later fill in.Track a loading flag and render a spinner in the result modal body.
♻️ Proposed change
+ const [previewLoading, setPreviewLoading] = useState(false); + useEffect(() => { if (!resultModalVisible || !content) return; if (mode === 'history' && _.isEmpty(selectedEventIds)) return; + setPreviewLoading(true); preview({ @@ .catch((err) => { console.error(err); setPreviewData(undefined); - }); + }) + .finally(() => { + setPreviewLoading(false); + }); }, [resultModalVisible, mode, _.join(selectedEventIds), mockEvent.severity, mockEvent.isRecovered]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/notificationTemplates/pages/List/Form/PreviewModal/index.tsx` around lines 40 - 59, Update the preview request flow in the useEffect to track a loading state, setting it before preview starts and clearing it after success or failure; reset preview data when the modal closes as needed. Pass this state into the result modal body and render a spinner while the request is pending instead of showing empty editors.src/pages/notificationTemplates/utils/tplKeys.test.ts (1)
159-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReplace
as anyin the test fixture with a typed value.The coding guidelines forbid
as anyin test data. TypeformValueswith the channel form type thatnormalizeFormValuesaccepts, or validate the literal withsatisfies. This keeps the fixture aligned with the source contract if the channel type changes.♻️ Proposed change
- const formValues = { + const formValues: ChannelItem = { name: 'dingtalk', ident: 'dingtalk', @@ - const normalized = normalizeFormValues(formValues as any); + const normalized = normalizeFormValues(formValues);Add the import:
import { ChannelItem } from '`@/pages/notificationChannels/services`';If
ChannelItemrequires more fields, usePartial<ChannelItem>plus the narrower parameter type instead ofany.As per coding guidelines: "Avoid
as anyin test data; when data does not match a function signature, prefersatisfiesto validate its structure."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/notificationTemplates/utils/tplKeys.test.ts` around lines 159 - 181, Replace the `formValues as any` cast in the test fixture with a typed value accepted by `normalizeFormValues`. Import and use `ChannelItem`, or apply `satisfies` with the appropriate channel form type, preserving the fixture’s existing fields and ensuring it remains aligned with the function’s contract.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/pages/notificationChannels/pages/Edit.tsx`:
- Line 25: Update the getChannelTypeMeta call in the Edit component to pass
data?.ident instead of the fallback ident value, while retaining ident for image
alt text or using typeMeta.label there.
In `@src/pages/notificationChannels/pages/Form/index.tsx`:
- Around line 206-212: Update the unknown-channel documentation flow in the Form
component so dingtalkapp, wecomapp, and feishuapp resolve to available local
documentation instead of missing
`/n9e-docs/notification-channel/${requestType}-request` pages. Add the
corresponding local docs or map these requestType values to an existing fallback
path while preserving current handling for known types and existing documented
request types.
In `@src/pages/notificationChannels/pages/ListNG/index.tsx`:
- Line 62: Replace the single togglingId state used by the notification channel
list and its toggle handlers with serialized updates or a collection of
in-flight IDs so concurrent toggles cannot clear another row’s loading state.
Ensure each update reads current data and merges its change without overwriting
another request’s successful update; alternatively disable all switches while
any update is pending. Update the handlers around the channel toggle mutation
and preserve correct loading behavior for every affected row.
- Around line 195-204: Update the JSON import flow in the `try` block to parse
into `unknown`, then validate the result with `Array.isArray` before calling
`postItems`. For non-array values, show the existing
`message.error(t('common:error.import'))` notification and return; keep the
existing parse-error handling unchanged and pass only the narrowed array to
`postItems`.
In `@src/pages/notificationTemplates/components/FieldsPanel/index.tsx`:
- Around line 57-66: Update the group-header toggle and field-copy row
interactive divs to include role="button", tabIndex={0}, and onKeyDown handlers
that invoke their existing onClick behavior for Enter and Space, while
preventing default Space scrolling. Also include focus in the field row Tooltip
trigger so keyboard focus reveals the description.
In `@src/pages/notificationTemplates/pages/List/FormModal.tsx`:
- Around line 221-247: Add a submitting state for the form save flow and set it
for the entire asynchronous validateFields handler, including starter-content
lookup and postItems/putItem requests; clear it in a finally path so failures
also re-enable saving. Bind this state to the submit button’s loading and/or
disabled props to prevent repeated clicks while the operation is pending,
preserving existing add, clone, and edit behavior.
---
Nitpick comments:
In `@src/pages/notificationChannels/pages/Form/index.tsx`:
- Around line 172-196: Extract the duplicated validation-error handling from the
save promise catch and TestModal’s onBeforeOpen into a shared helper near the
surrounding component logic. The helper should conditionally console.error
non-validation errors, call scrollToFirstError, and be reused by both handlers
while preserving the existing rethrow behavior in onBeforeOpen.
In `@src/pages/notificationChannels/pages/Form/TestModal/index.tsx`:
- Around line 140-157: Update the test button rendering in TestModal to import
and wrap the disabled Button with an antd Tooltip, using
t('test.script_blocked') as its content when scriptBlocked. Remove the native
title attribute and ensure the Tooltip remains available for the blocked-state
reason, while preserving the existing click and disabled behavior.
In `@src/pages/notificationTemplates/pages/List/Form/PreviewModal/index.tsx`:
- Around line 40-59: Update the preview request flow in the useEffect to track a
loading state, setting it before preview starts and clearing it after success or
failure; reset preview data when the modal closes as needed. Pass this state
into the result modal body and render a spinner while the request is pending
instead of showing empty editors.
In `@src/pages/notificationTemplates/pages/List/index.tsx`:
- Around line 208-222: Replace the inline maxHeight styling on the FieldsPanel
container with the equivalent Tailwind arbitrary-value class, keeping the
existing flex layout and 62% height cap unchanged.
In `@src/pages/notificationTemplates/utils/tplKeys.test.ts`:
- Around line 159-181: Replace the `formValues as any` cast in the test fixture
with a typed value accepted by `normalizeFormValues`. Import and use
`ChannelItem`, or apply `satisfies` with the appropriate channel form type,
preserving the fixture’s existing fields and ensuring it remains aligned with
the function’s contract.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3ce66e84-23c9-4764-af04-f5dd743ac0c5
📒 Files selected for processing (31)
public/n9e-docs/notification-template/en_US.mdpublic/n9e-docs/notification-template/ja_JP.mdpublic/n9e-docs/notification-template/zh_CN.mdpublic/n9e-docs/notification-template/zh_HK.mdsrc/pages/notificationChannels/constants.tssrc/pages/notificationChannels/locale/en_US.tssrc/pages/notificationChannels/locale/ja_JP.tssrc/pages/notificationChannels/locale/ru_RU.tssrc/pages/notificationChannels/locale/zh_CN.tssrc/pages/notificationChannels/locale/zh_HK.tssrc/pages/notificationChannels/pages/Add.tsxsrc/pages/notificationChannels/pages/Edit.tsxsrc/pages/notificationChannels/pages/Form/TestModal/index.tsxsrc/pages/notificationChannels/pages/Form/index.tsxsrc/pages/notificationChannels/pages/ListNG/index.tsxsrc/pages/notificationChannels/services.tssrc/pages/notificationTemplates/components/FieldsPanel/index.tsxsrc/pages/notificationTemplates/constants/eventFields.test.tssrc/pages/notificationTemplates/constants/eventFields.tssrc/pages/notificationTemplates/locale/en_US.tssrc/pages/notificationTemplates/locale/ja_JP.tssrc/pages/notificationTemplates/locale/ru_RU.tssrc/pages/notificationTemplates/locale/zh_CN.tssrc/pages/notificationTemplates/locale/zh_HK.tssrc/pages/notificationTemplates/pages/List/Form/PreviewModal/index.tsxsrc/pages/notificationTemplates/pages/List/FormModal.tsxsrc/pages/notificationTemplates/pages/List/ItemDetail.tsxsrc/pages/notificationTemplates/pages/List/index.tsxsrc/pages/notificationTemplates/services.tssrc/pages/notificationTemplates/utils/tplKeys.test.tssrc/pages/notificationTemplates/utils/tplKeys.ts
💤 Files with no reviewable changes (4)
- public/n9e-docs/notification-template/en_US.md
- public/n9e-docs/notification-template/zh_CN.md
- public/n9e-docs/notification-template/ja_JP.md
- public/n9e-docs/notification-template/zh_HK.md
| const ident = (data?.ident as string) || 'callback'; | ||
| const channelTypes = getNotificationChannelTypes(); | ||
| const identConfig = channelTypes[ident] ? channelTypes[ident] : channelTypes['callback']; | ||
| const typeMeta = getChannelTypeMeta(ident); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not default the ident to callback for the title.
ident falls back to 'callback' while data is still loading and for records without an ident. The title then shows the Callback logo and the Callback label. That is the mislabeling this change removes elsewhere.
Pass data?.ident to getChannelTypeMeta so an unresolved ident renders the neutral ApiOutlined icon and the - label.
🐛 Proposed fix
- const typeMeta = getChannelTypeMeta(ident);
+ const typeMeta = getChannelTypeMeta(data?.ident);Keep ident for the image alt text, or use typeMeta.label there.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const typeMeta = getChannelTypeMeta(ident); | |
| const typeMeta = getChannelTypeMeta(data?.ident); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/notificationChannels/pages/Edit.tsx` at line 25, Update the
getChannelTypeMeta call in the Edit component to pass data?.ident instead of the
fallback ident value, while retaining ident for image alt text or using
typeMeta.label there.
| {/* 未知 ident(历史数据、自建 script 媒介)在官网上没有对应文档页, | ||
| 直接拼 URL 会让整个右栏渲染一张带营销 banner 的 404 页。 | ||
| 这类回落到仓库内按 request_type 组织的本地文档。 */} | ||
| {requestType && isKnownType && ( | ||
| <Document type='iframe' documentPath={`https://flashcat.cloud/docs/content/flashcat-monitor/nightingale-v9/usage/alert-notify/notify-channel/${ident}/`} /> | ||
| )} | ||
| {requestType && !isKnownType && <Document documentPath={`/n9e-docs/notification-channel/${requestType}-request`} />} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: List available local notification-channel documents and compare with request types.
set -euo pipefail
fd -t d 'notification-channel' | head
fd . -t f --full-path '*notification-channel*' | sortRepository: n9e/fe
Length of output: 292
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file excerpt =="
sed -n '170,225p' src/pages/notificationChannels/pages/Form/index.tsx
echo
echo "== local notification-channel docs =="
find public/n9e-docs/notification-channel -maxdepth 1 -type f -printf '%f\n' 2>/dev/null | sort || true
echo
echo "== request type references in target area =="
rg -n "request[ _]?type|isKnownType|requestType|flashduty|pagerduty|dingtalkapp|wecomapp|feishuapp|smtp|script|http" src/pages/notificationChannels/src src/pages/notificationChannels/pages/Form -SRepository: n9e/fe
Length of output: 20050
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== notificationChannels files =="
git ls-files 'src/pages/notificationChannels/**' | sort
echo
echo "== request_type values across repo =="
rg -n "request_type|requestType|getChannelTypeMeta|known" src pages public 2>/dev/null | head -n 200 || true
echo
echo "== n9e-docs structure under public =="
find public/n9e-docs -maxdepth 3 -type d 2>/dev/null | sort | sed -n '1,200p' || true
find public/n9e-docs -maxdepth 4 -type f 2>/dev/null | sort | sed -n '1,200p' || true
echo
echo "== explicit /n9e-docs paths in repo =="
rg -n "/n9e-docs|notification-channel" . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' 2>/dev/null | head -n 120 || trueRepository: n9e/fe
Length of output: 50363
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target metadata definitions =="
rg -n "getChannelTypeMeta|known|request_type|requestTypes|http|smtp|script|flashduty|pagerduty|dingtalkapp|wecomapp|feishuapp" src/pages/notificationChannels/pages/Form -S --glob '*.ts' --glob '*.tsx' | head -n 200
echo
echo "== public/n9e-docs top-level =="
find public/n9e-docs -maxdepth 2 \( -type f -o -type d \) 2>/dev/null | sort | sed -n '1,200p' || true
echo
echo "== precise public docs filenames =="
find public/n9e-docs -maxdepth 3 \( -type f -o -type d \) 2>/dev/null | sort | sed -n '1,200p' || true
echo
echo "== route/static aliases for notification-channel-docs paths =="
rg -n "n9e-docs|notification-channel|flashcat.cloud" . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' --glob '!package-lock.json' 2>/dev/null | head -n 200 || true
echo
echo "== request type occurrences outside minified/generated parsing files =="
rg -n "request_type|requestType" src public --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' | head -n 250 || trueRepository: n9e/fe
Length of output: 50362
Add local docs for the unknown request_type values.
The right panel uses /n9e-docs/notification-channel/${requestType}-request, but that path currently only has http, smtp, script, flashduty, and pagerduty docs. Add local docs for dingtalkapp, wecomapp, and feishuapp, or handle those unknown channel types with an existing fallback path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/notificationChannels/pages/Form/index.tsx` around lines 206 - 212,
Update the unknown-channel documentation flow in the Form component so
dingtalkapp, wecomapp, and feishuapp resolve to available local documentation
instead of missing `/n9e-docs/notification-channel/${requestType}-request`
pages. Add the corresponding local docs or map these requestType values to an
existing fallback path while preserving current handling for known types and
existing documented request types.
| window.sessionStorage.setItem(FILTER_SESSION_STORAGE_KEY, JSON.stringify({ ...newFilter, current: 1 })); | ||
| }; | ||
| const [selectedRows, setSelectedRows] = useState<ChannelItem[]>([]); | ||
| const [togglingId, setTogglingId] = useState<number>(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Prevent concurrent toggle requests from overwriting row state.
togglingId stores only one ID. If a user toggles channel A and then channel B, B replaces the loading state for A. When either request settles, it clears the loading state for both rows.
The two handlers also capture the same data snapshot. Their separate mutate(newData) calls can overwrite the other row’s successful local update. Disable all switches during an update, or track all in-flight IDs and refresh or merge against current data safely.
Proposed fix: serialize enable updates
checked={val}
size='small'
loading={togglingId === record.id}
+ disabled={togglingId !== undefined}
onChange={(checked) => {Also applies to: 288-312
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/notificationChannels/pages/ListNG/index.tsx` at line 62, Replace
the single togglingId state used by the notification channel list and its toggle
handlers with serialized updates or a collection of in-flight IDs so concurrent
toggles cannot clear another row’s loading state. Ensure each update reads
current data and merges its change without overwriting another request’s
successful update; alternatively disable all switches while any update is
pending. Update the handlers around the channel toggle mutation and preserve
correct loading behavior for every affected row.
| let newData: ChannelItem[]; | ||
| try { | ||
| const newData = JSON.parse(data); | ||
| postItems(newData).then(() => { | ||
| run(); | ||
| message.success(t('common:success.import')); | ||
| }); | ||
| newData = JSON.parse(data); | ||
| } catch (e) { | ||
| // JSON 解析失败是纯前端错误,不经过全局 errorHandler,需自行提示 | ||
| console.error(e); | ||
| message.error(t('common:error.import')); | ||
| return; | ||
| } | ||
| postItems(newData) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Reject non-array JSON before calling postItems.
newData: ChannelItem[] is a TypeScript annotation. It does not validate the parsed value at runtime. JSON such as null or {} reaches postItems, although its input contract requires an array.
Parse into unknown and reject values that fail Array.isArray with the existing import error.
Proposed fix
- let newData: ChannelItem[];
+ let newData: ChannelItem[];
try {
- newData = JSON.parse(data);
+ const parsed: unknown = JSON.parse(data);
+ if (!Array.isArray(parsed)) {
+ throw new Error('Imported payload must be an array');
+ }
+ newData = parsed as ChannelItem[];As per coding guidelines, “For type narrowing in JavaScript and TypeScript code, prefer native checks such as Array.isArray.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let newData: ChannelItem[]; | |
| try { | |
| const newData = JSON.parse(data); | |
| postItems(newData).then(() => { | |
| run(); | |
| message.success(t('common:success.import')); | |
| }); | |
| newData = JSON.parse(data); | |
| } catch (e) { | |
| // JSON 解析失败是纯前端错误,不经过全局 errorHandler,需自行提示 | |
| console.error(e); | |
| message.error(t('common:error.import')); | |
| return; | |
| } | |
| postItems(newData) | |
| let newData: ChannelItem[]; | |
| try { | |
| const parsed: unknown = JSON.parse(data); | |
| if (!Array.isArray(parsed)) { | |
| throw new Error('Imported payload must be an array'); | |
| } | |
| newData = parsed as ChannelItem[]; | |
| } catch (e) { | |
| // JSON 解析失败是纯前端错误,不经过全局 errorHandler,需自行提示 | |
| console.error(e); | |
| message.error(t('common:error.import')); | |
| return; | |
| } | |
| postItems(newData) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/notificationChannels/pages/ListNG/index.tsx` around lines 195 -
204, Update the JSON import flow in the `try` block to parse into `unknown`,
then validate the result with `Array.isArray` before calling `postItems`. For
non-array values, show the existing `message.error(t('common:error.import'))`
notification and return; keep the existing parse-error handling unchanged and
pass only the narrowed array to `postItems`.
Source: Coding guidelines
| <div | ||
| className='flex cursor-pointer select-none items-center gap-1 py-1 text-[12px] text-soft' | ||
| onClick={() => { | ||
| setCollapsed((prev) => ({ ...prev, [group.key]: !prev[group.key] })); | ||
| }} | ||
| > | ||
| <DownOutlined className='text-[10px] transition-transform duration-200' style={{ transform: isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)' }} /> | ||
| {t(`fields_panel.groups.${group.key}`)} | ||
| <span className='opacity-60'>({group.fields.length})</span> | ||
| </div> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add keyboard support to the two interactive div elements.
The group-header toggle (Line 57-66) and the field-copy row (Line 87-102) rely on onClick only. Neither element has role, tabIndex, or an onKeyDown handler. A keyboard-only user cannot expand or collapse a group, and cannot copy a field reference, which is the panel's primary action.
Add role="button", tabIndex={0}, and an onKeyDown handler that responds to Enter and Space on both elements. Consider adding 'focus' to the Tooltip trigger prop so keyboard focus also reveals the field description.
♿ Proposed fix for keyboard accessibility
<div
className='flex cursor-pointer select-none items-center gap-1 py-1 text-[12px] text-soft'
+ role='button'
+ tabIndex={0}
onClick={() => {
setCollapsed((prev) => ({ ...prev, [group.key]: !prev[group.key] }));
}}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setCollapsed((prev) => ({ ...prev, [group.key]: !prev[group.key] }));
+ }
+ }}
> <Tooltip
key={field.ref}
mouseEnterDelay={0.5}
+ trigger={['hover', 'focus']}
title={
...
}
>
<div
className='group mb-1 cursor-pointer rounded border border-antd bg-fc-100 px-2 py-1 transition hover:border-primary'
+ role='button'
+ tabIndex={0}
onClick={() => {
copyToClipBoard(field.ref);
}}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ copyToClipBoard(field.ref);
+ }
+ }}
>Also applies to: 87-102
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/notificationTemplates/components/FieldsPanel/index.tsx` around
lines 57 - 66, Update the group-header toggle and field-copy row interactive
divs to include role="button", tabIndex={0}, and onKeyDown handlers that invoke
their existing onClick behavior for Enter and Space, while preventing default
Space scrolling. Also include focus in the field row Tooltip trigger so keyboard
focus reveals the description.
| form | ||
| .validateFields() | ||
| .then(async (values) => { | ||
| if (mode === 'add' || mode === 'clone') { | ||
| values.ident = uuidv4(); // 2025-06-06 Generate a new unique identifier for the template | ||
| // 新建时按媒介期望的字段名种一份起步内容,避免落到空白编辑器从零手写 Go template; | ||
| // 克隆必须保留被克隆的正文——此前这里对 smtp 无条件覆写,把内容静默清空了 | ||
| if (mode === 'add') { | ||
| values.content = await buildStarterContentByIdent(values.notify_channel_ident); | ||
| } | ||
| await postItems([values]); | ||
| message.success(t('common:success.add')); | ||
| onOk(); | ||
| }); | ||
| } else if (mode === 'edit') { | ||
| putItem(values).then(() => { | ||
| // 回传 ident 让列表页选中刚建好的这条:中间栏若仍为空, | ||
| // 用户点侧栏新条目是没有反应的(ItemDetail 未挂载,ref 为 null) | ||
| onOk(values.ident); | ||
| } else if (mode === 'edit') { | ||
| await putItem(values); | ||
| message.success(t('common:success.edit')); | ||
| onOk(); | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| // 校验失败由 antd 自行标红,其余异常不能跟着一起静默掉 | ||
| if (!err?.errorFields) { | ||
| console.error(err); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Guard the save action against repeated submits.
The submit handler is now asynchronous and can await two requests: the channel lookup and postItems. The button stays enabled for that whole period. A second click creates a second template, because postItems is not idempotent and each click generates a fresh uuidv4() ident.
Add a submitting flag and bind it to the button.
🐛 Proposed fix
+ const [submitting, setSubmitting] = useState(false); <Button
type='primary'
htmlType='submit'
+ loading={submitting}
+ disabled={submitting}
onClick={() => {
+ setSubmitting(true);
form
.validateFields()
.then(async (values) => {
@@
.catch((err) => {
// 校验失败由 antd 自行标红,其余异常不能跟着一起静默掉
if (!err?.errorFields) {
console.error(err);
}
- });
+ })
+ .finally(() => {
+ setSubmitting(false);
+ });
}}
>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/notificationTemplates/pages/List/FormModal.tsx` around lines 221 -
247, Add a submitting state for the form save flow and set it for the entire
asynchronous validateFields handler, including starter-content lookup and
postItems/putItem requests; clear it in a finally path so failures also
re-enable saving. Bind this state to the submit button’s loading and/or disabled
props to prevent repeated clicks while the operation is pending, preserving
existing add, clone, and edit behavior.
Use {{$.domain}} instead of {{$domain}} in the generated starter
template and in the field panel, and update the four notification
template docs. $domain is not a variable this template system provides:
the site URL lives in the render data, and $domain only appears in the
legacy built-in templates where each one declares it itself. Prefer $.
over a bare dot because dot is rebound inside range/with, and field
panel entries are meant to be copied into any position.
Add the missing common:btn.close key to all five locales. The channel
test result view rendered the raw string "btn.close" because no locale
defined it.
Let PagerDuty channels be tested at all. The provider requires at least
one routing key, but nothing in the test modal could supply
pagerduty_integration_keys, so the test failed every time no matter how
the API key was configured, with an error naming a concept absent from
that page. Add an integration key input for request_type pagerduty (tags
select, so the value reaches the backend as the string array it decodes)
and disable the submit button until it is filled.
Also closes an unterminated code fence in the ja_JP template doc that
was rendering the rest of the page as a single code block.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation