Skip to content

feat(notify): test media configs and preview templates without saving - #2232

Merged
jsers merged 3 commits into
mainfrom
optimize-notify-channel
Aug 4, 2026
Merged

feat(notify): test media configs and preview templates without saving#2232
jsers merged 3 commits into
mainfrom
optimize-notify-channel

Conversation

@710leo

@710leo 710leo commented Aug 1, 2026

Copy link
Copy Markdown
Member
image image

Summary by CodeRabbit

  • New Features

    • Test notification channels with mock or historical events, recipients, parameters, validation, and detailed results.
    • Preview templates using mock events or alert history.
    • Search, browse, and copy available template fields.
    • Generate starter template content from channel configuration.
    • Improve template creation, cloning, and empty-state workflows.
    • Safely display unknown channel types with generic labels and icons.
    • Added multilingual support for notification testing and template tools.
  • Bug Fixes

    • Improved filtering, import error handling, and enable/disable feedback.
    • Preserved details when configuration or access is incomplete.
  • Documentation

    • Updated template examples to use automatically populated site domains.

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.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Notification channel workflows

Layer / File(s) Summary
Channel test contracts and execution
src/pages/notificationChannels/services.ts, src/pages/notificationChannels/pages/Form/TestModal/index.tsx, src/pages/notificationChannels/pages/Form/index.tsx, src/pages/notificationChannels/locale/*
Adds typed test requests, a test API, and a modal for historical or mock events.
Channel metadata and unknown-type rendering
src/pages/notificationChannels/constants.ts, src/pages/notificationChannels/pages/Add.tsx, src/pages/notificationChannels/pages/Edit.tsx, src/pages/notificationChannels/pages/Form/index.tsx
Resolves known and unknown channel identifiers and renders logos, labels, fallback icons, and local documentation.
Channel list filtering and updates
src/pages/notificationChannels/pages/ListNG/index.tsx
Combines active filters, validates imports, and fetches current channel data before enable updates.

Notification template workflows

Layer / File(s) Summary
Template field catalog and searchable panel
src/pages/notificationTemplates/constants/eventFields.ts, src/pages/notificationTemplates/components/FieldsPanel/index.tsx, src/pages/notificationTemplates/locale/*, src/pages/notificationTemplates/constants/eventFields.test.ts
Adds grouped event fields, localized search, copy actions, empty states, and catalog invariants.
Template key extraction and starter content
src/pages/notificationTemplates/utils/tplKeys.ts, src/pages/notificationTemplates/utils/tplKeys.test.ts, src/pages/notificationTemplates/pages/List/FormModal.tsx, src/pages/notificationTemplates/pages/List/ItemDetail.tsx
Derives channel template keys, generates starter content, and selects newly created templates while preserving clone content.
History and mock preview flow
src/pages/notificationTemplates/services.ts, src/pages/notificationTemplates/pages/List/Form/PreviewModal/index.tsx
Supports historical and mock preview requests and renders typed field results or template errors.
Template list and documentation layout
src/pages/notificationTemplates/pages/List/index.tsx, public/n9e-docs/notification-template/*.md
Adds loading and empty states, integrates the fields panel, selects created templates, and uses the root $.domain value in documentation examples.
Shared close-button localization
src/locales/common/locale/*
Adds translated close-button labels to the common locales.

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
Loading
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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: testing notification media configurations and previewing templates without saving.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optimize-notify-channel

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
src/pages/notificationChannels/pages/Form/index.tsx (1)

172-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared validation-error handler.

The same error branch appears in the save catch and in onBeforeOpen. 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 value

Show the blocked reason with a Tooltip.

The native title attribute is unreliable on a disabled button and is not reachable by keyboard. Wrap the button in an antd Tooltip so the test.script_blocked reason 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 Tooltip after the button and import Tooltip from antd.

🤖 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 value

Express the height cap with a Tailwind class.

Line 214 sets maxHeight through 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 win

Add a loading state for the preview request.

The effect clears previewData on 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 win

Replace as any in the test fixture with a typed value.

The coding guidelines forbid as any in test data. Type formValues with the channel form type that normalizeFormValues accepts, or validate the literal with satisfies. 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 ChannelItem requires more fields, use Partial<ChannelItem> plus the narrower parameter type instead of any.

As per coding guidelines: "Avoid as any in test data; when data does not match a function signature, prefer satisfies to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0a1d05c and c185cd0.

📒 Files selected for processing (31)
  • public/n9e-docs/notification-template/en_US.md
  • public/n9e-docs/notification-template/ja_JP.md
  • public/n9e-docs/notification-template/zh_CN.md
  • public/n9e-docs/notification-template/zh_HK.md
  • src/pages/notificationChannels/constants.ts
  • src/pages/notificationChannels/locale/en_US.ts
  • src/pages/notificationChannels/locale/ja_JP.ts
  • src/pages/notificationChannels/locale/ru_RU.ts
  • src/pages/notificationChannels/locale/zh_CN.ts
  • src/pages/notificationChannels/locale/zh_HK.ts
  • src/pages/notificationChannels/pages/Add.tsx
  • src/pages/notificationChannels/pages/Edit.tsx
  • src/pages/notificationChannels/pages/Form/TestModal/index.tsx
  • src/pages/notificationChannels/pages/Form/index.tsx
  • src/pages/notificationChannels/pages/ListNG/index.tsx
  • src/pages/notificationChannels/services.ts
  • src/pages/notificationTemplates/components/FieldsPanel/index.tsx
  • src/pages/notificationTemplates/constants/eventFields.test.ts
  • src/pages/notificationTemplates/constants/eventFields.ts
  • src/pages/notificationTemplates/locale/en_US.ts
  • src/pages/notificationTemplates/locale/ja_JP.ts
  • src/pages/notificationTemplates/locale/ru_RU.ts
  • src/pages/notificationTemplates/locale/zh_CN.ts
  • src/pages/notificationTemplates/locale/zh_HK.ts
  • src/pages/notificationTemplates/pages/List/Form/PreviewModal/index.tsx
  • src/pages/notificationTemplates/pages/List/FormModal.tsx
  • src/pages/notificationTemplates/pages/List/ItemDetail.tsx
  • src/pages/notificationTemplates/pages/List/index.tsx
  • src/pages/notificationTemplates/services.ts
  • src/pages/notificationTemplates/utils/tplKeys.test.ts
  • src/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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment on lines +206 to +212
{/* 未知 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`} />}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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*' | sort

Repository: 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 -S

Repository: 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 || true

Repository: 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 || true

Repository: 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>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +195 to +204
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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

Comment on lines +57 to +66
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment on lines +221 to +247
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);
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

710leo added 2 commits August 4, 2026 10:24
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.
@jsers
jsers merged commit 83cc3e5 into main Aug 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants