Skip to content

优化 Topic Feed 管理:分步向导、输入备注与预览#806

Merged
Colin-XKL merged 4 commits into
devfrom
cursor/topic-feed-ux-f70a
Jun 1, 2026
Merged

优化 Topic Feed 管理:分步向导、输入备注与预览#806
Colin-XKL merged 4 commits into
devfrom
cursor/topic-feed-ux-f70a

Conversation

@Colin-XKL
Copy link
Copy Markdown
Owner

@Colin-XKL Colin-XKL commented Jun 1, 2026

概述

优化 Topic Feed 管理后台的创建/编辑体验,使配置流程更清晰、输入源更易识别,并支持在添加子 Feed 时即时预览。子 Feed 支持禁用状态,且数据库最终只保留一个 URI 相关字段:inputs

主要变更

  • 创建/编辑改为 /worktable/topic_feed/create/worktable/topic_feed/:id/edit 分步向导
  • 子 Feed 使用 inputs: [{ uri, description, disabled }]
  • 业务层不再读写 input_uris,运行时通过 EnabledInputURIs() 计算启用 URI
  • 一次性迁移旧 topic_feeds.input_urisinputs 后删除旧列
  • 前端不再提交或读取 input_uris
  • 输入源支持抽屉预览和禁用状态展示

测试

  • go test ./...
  • task fix && task backend-build && task frontend-build
  • API 验证:新 Topic 响应无 input_uris,DB schema 仅包含 inputs
  • 浏览器验证:编辑 Topic、禁用一个输入源、确认页显示启用数量为 1 与“已禁用”标签、保存并删除

topic_feed_disable_edit_success.mp4

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Summary by Sourcery

Refine Topic Feed management by moving creation/editing into a dedicated step-by-step editor, enriching input metadata and preview capabilities, and syncing new input structures across backend and detail views.

New Features:

  • Introduce a multi-step Topic Feed editor page for creating and updating topics with separated basics, inputs, aggregation rules, and review steps.
  • Add per-input metadata including description and disabled state, with UI to label, toggle, and preview individual input sources.
  • Provide a reusable feed preview panel to inspect input feeds directly from the Topic Feed editor.

Bug Fixes:

  • Ensure topic validation requires at least one enabled input and reports validation errors against the new inputs structure rather than raw URI arrays.

Enhancements:

  • Refactor Topic Feed list view to navigate to the new editor routes and share aggregator summary formatting utilities.
  • Extend Topic Feed detail view to display rich input information, show enabled/total input counts, and allow toggling inputs on or off while enforcing at least one enabled source.
  • Add shared frontend utilities and components to manage topic input sources and aggregator configuration more cleanly.

Tests:

  • Add unit tests for TopicFeed.NormalizeInputs to verify correct syncing between inputs and input URIs, including disabled inputs and legacy data upgrade paths.

Replace the topic feed create/edit modal with a multi-step wizard page.
Add per-input description fields persisted via inputs[] on the API.
Extract FeedPreviewPanel for reuse when previewing sub-feeds in the editor.

Co-authored-by: Colin <Colin_XKL@outlook.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
feed-craft-admin Ready Ready Preview, Comment Jun 1, 2026 12:33pm
feed-craft-doc Ready Ready Preview, Comment Jun 1, 2026 12:33pm

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jun 1, 2026

Reviewer's Guide

Refactors Topic Feed management to use a dedicated multi-step editor with reusable input/aggregator utilities, adds backend support for rich input metadata (description, disabled) while keeping input_uris derived and compatible, and exposes input descriptions and enable/disable controls in the detail view along with a reusable feed preview panel for sub-feeds.

Sequence diagram for toggling TopicInput disabled state

sequenceDiagram
  actor Admin
  participant DetailPage as TopicFeedDetailView
  participant TopicAPI as updateTopicFeed
  participant DAO as TopicFeed(NormalizeInputs)
  participant DB

  Admin->>DetailPage: toggleInputDisabled(uri, disabled)
  DetailPage->>DetailPage: build payload (inputs, input_uris)
  DetailPage->>TopicAPI: updateTopicFeed(id, payload)
  TopicAPI->>DAO: bind JSON to TopicFeed
  TopicAPI->>DAO: TopicFeed.NormalizeInputs()
  DAO->>DB: save TopicFeed (Inputs, InputURIs)
  DB-->>DAO: ok
  DAO-->>TopicAPI: updated TopicFeed
  TopicAPI-->>DetailPage: APIResponse(data)
  DetailPage->>DetailPage: fetchDetail() and refresh inputs
Loading

File-Level Changes

Change Details Files
Replace in-table modal editor with routed multi-step Topic Feed editor that reuses shared input and aggregator editing utilities.
  • Remove the create/edit modal form and associated form/validation logic from the TopicFeed list view, keeping it as a pure table with actions
  • Introduce a new TopicFeedEditor page with a 4-step wizard (basic info, inputs with preview, aggregator, review & save) handling create/edit flows via router routes
  • Factor input source and aggregator editing UIs into TopicInputSourcesEditor and TopicAggregatorEditor reusable components
  • Wire list actions to navigate to TopicFeedCreate/TopicFeedEdit routes and use shared aggregator summary helper for display
web/admin/src/views/dashboard/topic_feed/topic_feed.vue
web/admin/src/views/dashboard/topic_feed/editor.vue
web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue
web/admin/src/views/dashboard/topic_feed/components/TopicAggregatorEditor.vue
web/admin/src/router/routes/modules/worktable.ts
web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts
Add reusable feed preview panel and integrate per-input preview into the Topic Feed input editor.
  • Create FeedPreviewPanel component that wraps FeedViewContainer and previewFeed API with loading/error handling and an exposed loadPreview method
  • Add a preview button per input source in TopicInputSourcesEditor that opens a drawer and loads the selected input’s feed via FeedPreviewPanel
web/admin/src/components/feed-preview/FeedPreviewPanel.vue
web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue
Extend backend TopicFeed model and API to support rich inputs (URI, description, disabled) while maintaining derived input_uris and validation.
  • Introduce TopicInput struct and Inputs field on TopicFeed with JSON serialization, keeping InputURIs as a derived field for enabled inputs
  • Implement TopicFeed.NormalizeInputs to sync between Inputs and InputURIs and to upgrade legacy records that only have input_uris
  • Call NormalizeInputs in all relevant controller paths (create, get, list, update, validate, detail) to ensure consistent payloads and validation
  • Update topic validation to operate on Inputs, enforce at least one enabled input, and adjust error field names accordingly
  • Change TopicDetail’s sub-feed health computation to iterate over Inputs instead of InputURIs
  • Add unit tests for NormalizeInputs behavior including disabled inputs and legacy data
internal/dao/topic.go
internal/controller/topic_feed.go
internal/dao/topic_test.go
Expose input descriptions and enable/disable toggles in the Topic Feed detail view while keeping counts accurate for enabled vs total.
  • Compute topicInputs from either inputs or input_uris for backward compatibility and derive enabled/disabled counts
  • Render inputs as a table showing description (fallback to URI), URI, and a disabled switch per row
  • Implement toggleInputDisabled to persist disabled state via updateTopicFeed while ensuring at least one enabled input remains
  • Adjust the input count statistic to show enabled count and, when applicable, total count
web/admin/src/views/dashboard/topic_feed/detail.vue
web/admin/src/api/topic.ts
Add i18n strings and shared frontend utilities for Topic Feed inputs, aggregator summary, and wizard copy.
  • Introduce TopicInput and inputs? field in the TopicFeed API typings and helper functions for converting between form state and API payloads, including aggregator_config normalization and summary formatting
  • Add new locale strings for input descriptions, disabled state messaging, input preview UI, and wizard steps in both English and Chinese
  • Refactor list view to use shared formatAggregatorSummary helper that accepts i18n t function and count inputs via new helper
web/admin/src/api/topic.ts
web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts
web/admin/src/views/dashboard/topic_feed/topic_feed.vue
web/admin/src/locale/en-US/topic.ts
web/admin/src/locale/zh-CN/topic.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Persist optional disabled on each input; runtime uses only enabled URIs.
Detail page supports toggling disable without opening the full editor.

Co-authored-by: Colin <Colin_XKL@outlook.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the Topic Feed management in the admin panel, moving from a modal-based editor to a multi-step wizard editor, and introduces support for input descriptions alongside URIs. On the backend, input normalization has been added to keep the new Inputs field and legacy InputURIs in sync. The review feedback highlights several opportunities to improve robustness, specifically by adding defensive checks against potential TypeErrors when handling potentially null or undefined fields in the frontend, catching errors in asynchronous API calls, and removing redundant input normalization calls in the backend controllers.

Comment on lines +101 to +104
const inputs =
record.inputs && record.inputs.length > 0
? record.inputs
: record.input_uris.map((uri) => ({ uri, description: '' }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

如果 record.input_urisnullundefined(这在历史遗留数据或没有输入源的记录中可能会发生),对其调用 .map() 将会抛出运行时的 TypeError 错误。我们应该通过提供默认的空数组来防御性地避免此问题。

Suggested change
const inputs =
record.inputs && record.inputs.length > 0
? record.inputs
: record.input_uris.map((uri) => ({ uri, description: '' }));
const inputs =
record.inputs && record.inputs.length > 0
? record.inputs
: (record.input_uris || []).map((uri) => ({ uri, description: '' }));

Comment on lines +325 to +330
const topicInputs = computed(() => {
if (!detail.value) return [];
const { topic } = detail.value;
if (topic.inputs?.length) return topic.inputs;
return topic.input_uris.map((uri) => ({ uri, description: '' }));
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

如果 topic.input_urisnullundefined,对其调用 .map() 将会抛出运行时的 TypeError 错误。我们应该使用备用的空数组来防止潜在的页面崩溃。

  const topicInputs = computed(() => {
    if (!detail.value) return [];
    const { topic } = detail.value;
    if (topic.inputs?.length) return topic.inputs;
    return (topic.input_uris || []).map((uri) => ({ uri, description: '' }));
  });

Comment on lines +118 to 121
const inputCount = (record: TopicFeed) => {
if (record.inputs?.length) return record.inputs.length;
return record.input_uris.length;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

如果 record.input_urisnullundefined,访问其 .length 属性将会抛出运行时的 TypeError 错误。我们应该使用可选链(optional chaining)和备用值 0 来确保代码的安全性。

  const inputCount = (record: TopicFeed) => {
    if (record.inputs?.length) return record.inputs.length;
    return record.input_uris?.length || 0;
  };

Comment on lines +215 to +217
previewDrawerTitle.value =
source.description.trim() ||
t('topic.inputPreview.title', { index: idx + 1 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

如果 source.description 在运行时为 undefinednull,对其调用 .trim() 将会抛出 TypeError 错误。我们应该使用可选链来安全地处理可能缺失的描述信息。

    previewDrawerTitle.value =
      source.description?.trim() ||
      t('topic.inputPreview.title', { index: idx + 1 });

Comment on lines +90 to +98
export const sourceToUri = (source: InputSourceItem): string => {
if (source.sourceType === 'recipe') {
return `feedcraft://recipe/${source.resourceId}`;
}
if (source.sourceType === 'topic') {
return `feedcraft://topic/${source.resourceId}`;
}
return source.externalUrl.trim();
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

如果 source.externalUrl 在运行时为 undefinednull,对其调用 .trim() 将会抛出 TypeError 错误。我们应该使用备用的空字符串来防止潜在的崩溃。

Suggested change
export const sourceToUri = (source: InputSourceItem): string => {
if (source.sourceType === 'recipe') {
return `feedcraft://recipe/${source.resourceId}`;
}
if (source.sourceType === 'topic') {
return `feedcraft://topic/${source.resourceId}`;
}
return source.externalUrl.trim();
};
export const sourceToUri = (source: InputSourceItem): string => {
if (source.sourceType === 'recipe') {
return `feedcraft://recipe/${source.resourceId}`;
}
if (source.sourceType === 'topic') {
return `feedcraft://topic/${source.resourceId}`;
}
return (source.externalUrl || '').trim();
};

Comment on lines +154 to +156
id: formData.id.trim(),
title: formData.title.trim(),
description: formData.description.trim(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

如果 formData.idformData.titleformData.description 在运行时为 undefinednull,对其调用 .trim() 将会抛出 TypeError 错误。我们应该使用备用的空字符串来确保代码的健壮性。

Suggested change
id: formData.id.trim(),
title: formData.title.trim(),
description: formData.description.trim(),
id: (formData.id || '').trim(),
title: (formData.title || '').trim(),
description: (formData.description || '').trim(),

Comment on lines +242 to +254
const loadPickerData = async () => {
pickerLoading.value = true;
try {
const [recipesRes, topicsRes] = await Promise.all([
getCustomRecipes(),
listTopicFeeds(),
]);
availableRecipes.value = recipesRes.data ?? [];
availableTopics.value = topicsRes.data ?? [];
} finally {
pickerLoading.value = false;
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

loadPickerData 函数没有捕获异步 API 调用中的错误。如果任何 API 调用失败,Promise 的 rejection 将会是未处理的(unhandled),且用户不会收到任何错误反馈。我们应该添加 catch 块来优雅地处理错误并向用户展示提示。

  const loadPickerData = async () => {
    pickerLoading.value = true;
    try {
      const [recipesRes, topicsRes] = await Promise.all([
        getCustomRecipes(),
        listTopicFeeds(),
      ]);
      availableRecipes.value = recipesRes.data ?? [];
      availableTopics.value = topicsRes.data ?? [];
    } catch (err: any) {
      Message.error(err.message || t('topic.fetchFailed'));
    } finally {
      pickerLoading.value = false;
    }
  };

Comment thread internal/controller/topic_feed.go Outdated
c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()})
return
}
topicData.NormalizeInputs()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

此处对 NormalizeInputs() 的调用是多余的,因为在第 69 行保存之前已经对输入源进行了归一化,且 GORM 的 Create 操作并不会修改它们。移除此调用可以避免不必要的内存分配和处理开销。

Comment thread internal/controller/topic_feed.go Outdated
c.JSON(http.StatusInternalServerError, util.APIResponse[any]{Msg: err.Error()})
return
}
topicData.NormalizeInputs()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

此处对 NormalizeInputs() 的调用是多余的,因为在第 126 行保存之前已经对输入源进行了归一化,且 GORM 的 Save 操作并不会修改它们。移除此调用可以避免不必要的内存分配和处理开销。

Comment thread internal/controller/topic_feed.go Outdated
c.JSON(http.StatusBadRequest, util.APIResponse[any]{Msg: err.Error()})
return
}
topicData.NormalizeInputs()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

此处对 NormalizeInputs() 的调用是多余的,因为 validateTopicConfig(在第 173 行被调用)内部已经在第 288 行对输入源进行了归一化。移除此调用可以避免重复处理。

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Jun 1, 2026

Not up to standards ⛔

🔴 Issues 4 medium · 5 minor

Alerts:
⚠ 4 issues (≤ 0 issues of at least medium severity)

Results:
9 new issues

Category Results
BestPractice 5 minor
Complexity 4 medium

View in Codacy

🟢 Metrics 68 complexity · 0 duplication

Metric Results
Complexity 68
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@Colin-XKL Colin-XKL marked this pull request as ready for review June 1, 2026 10:56
@qodo-code-review
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Refactor Topic Feed management with wizard editor, input descriptions, and preview panel

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Replace modal-based create/edit with dedicated multi-step wizard pages
• Add per-input description and disabled flag with API synchronization
• Extract reusable feed preview component for inline input previews
• Support toggling input disabled state from detail view without full editor
Diagram
flowchart LR
  A["Topic Feed List"] -->|Create/Edit| B["Wizard Editor Page"]
  B -->|Step 1| C["Basic Info"]
  B -->|Step 2| D["Input Sources"]
  D -->|Preview| E["Feed Preview Panel"]
  B -->|Step 3| F["Aggregation Rules"]
  B -->|Step 4| G["Review & Save"]
  G -->|Submit| H["API Normalize Inputs"]
  H -->|Sync| I["Inputs + InputURIs"]
  J["Detail View"] -->|Toggle Disabled| K["Update Input State"]

Loading

Grey Divider

File Changes

1. internal/dao/topic.go ✨ Enhancement +64/-2

Add TopicInput struct and NormalizeInputs method

internal/dao/topic.go


2. internal/dao/topic_test.go 🧪 Tests +66/-0

Unit tests for input normalization logic

internal/dao/topic_test.go


3. internal/controller/topic_feed.go ✨ Enhancement +22/-8

Call NormalizeInputs on all API endpoints

internal/controller/topic_feed.go


View more (11)
4. web/admin/src/api/topic.ts ✨ Enhancement +7/-0

Add TopicInput interface to API types

web/admin/src/api/topic.ts


5. web/admin/src/components/feed-preview/FeedPreviewPanel.vue ✨ Enhancement +101/-0

Reusable feed preview component with loading state

web/admin/src/components/feed-preview/FeedPreviewPanel.vue


6. web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue ✨ Enhancement +326/-0

Input sources editor with preview drawer and disabled toggle

web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue


7. web/admin/src/views/dashboard/topic_feed/components/TopicAggregatorEditor.vue ✨ Enhancement +227/-0

Aggregation rules editor as standalone component

web/admin/src/views/dashboard/topic_feed/components/TopicAggregatorEditor.vue


8. web/admin/src/views/dashboard/topic_feed/editor.vue ✨ Enhancement +407/-0

New multi-step wizard page for create and edit

web/admin/src/views/dashboard/topic_feed/editor.vue


9. web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts ✨ Enhancement +229/-0

Shared utilities for form data and payload normalization

web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts


10. web/admin/src/views/dashboard/topic_feed/topic_feed.vue ✨ Enhancement +16/-712

Simplify list page to navigate to wizard routes

web/admin/src/views/dashboard/topic_feed/topic_feed.vue


11. web/admin/src/views/dashboard/topic_feed/detail.vue ✨ Enhancement +132/-12

Display inputs table with disable toggle and count stats

web/admin/src/views/dashboard/topic_feed/detail.vue


12. web/admin/src/router/routes/modules/worktable.ts ⚙️ Configuration changes +20/-0

Add create and edit wizard routes

web/admin/src/router/routes/modules/worktable.ts


13. web/admin/src/locale/en-US/topic.ts 📝 Documentation +29/-0

Add wizard and input description i18n keys

web/admin/src/locale/en-US/topic.ts


14. web/admin/src/locale/zh-CN/topic.ts 📝 Documentation +25/-0

Add wizard and input description Chinese translations

web/admin/src/locale/zh-CN/topic.ts


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review Bot commented Jun 1, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Inputs not validated on save 🐞 Bug ≡ Correctness
Description
CreateTopicFeed/UpdateTopicFeed 在写入数据库前未强制校验“至少一个启用输入源”,且 InputURIs 不再是 binding required,导致可保存
inputs/input_uris 为空的 TopicFeed,运行时构建 Topic 会得到 0 个输入或直接失败。
Code

internal/dao/topic.go[R24-34]

Evidence
input_uris 不再是必填字段且 Create/Update 直接 Save;与此同时系统自身的校验逻辑与运行时都假设至少有一个启用输入源,否则 Topic 构建将没有 Inputs。

internal/dao/topic.go[17-37]
internal/controller/topic_feed.go[63-79]
internal/controller/topic_feed.go[114-147]
internal/controller/topic_feed.go[308-314]
internal/feedruntime/builder.go[145-181]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Create/Update API can persist a TopicFeed with no enabled inputs because `input_uris` is no longer `binding:"required"` and the controllers don’t validate `len(topicData.InputURIs) > 0` after `NormalizeInputs()`.

## Issue Context
The codebase already defines the business rule in `validateTopicConfig`: at least one enabled input is required. Runtime topic building iterates `topic.InputURIs` to construct providers.

## Fix Focus Areas
- internal/controller/topic_feed.go[63-79]
- internal/controller/topic_feed.go[114-147]
- internal/controller/topic_feed.go[281-351]

## Suggested fix
1. In `CreateTopicFeed` and `UpdateTopicFeed`, after `topicData.NormalizeInputs()`:
  - If `len(topicData.InputURIs) == 0`, return `400 BadRequest` with a clear message (e.g. "At least one enabled input source is required").
2. (Optional but stronger) Reuse/extend `validateTopicConfig` to provide richer errors (runtime build errors) for Create/Update as well, or add a lightweight URI validation step for internal URIs.
3. Ensure responses consistently return `input_uris` as an empty array (not null) when invalid input is rejected.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. loadPickerData() lacks error notice 📘 Rule violation ☼ Reliability
Description
loadPickerData() performs network fetches without a catch, so failures can become unhandled and
no user-visible notification is shown. This violates the requirement to surface failing actions to
users instead of only failing silently/unobservably.
Code

web/admin/src/views/dashboard/topic_feed/editor.vue[R245-257]

Evidence
PR Compliance ID 4 requires UI code handling failing actions to provide user-visible notifications
and not fail silently. The added loadPickerData() only uses try/finally around
Promise.all([...]) and sets arrays to res.data ?? [], but has no catch to show an error
message when the requests fail.

web/admin/src/views/dashboard/topic_feed/editor.vue[245-257]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`loadPickerData()` fetches picker data via `getCustomRecipes()` and `listTopicFeeds()` but does not handle errors with a user-visible notification (and can result in an unhandled rejection).

## Issue Context
This is part of the new Topic Feed wizard page; if these calls fail, users may see an incomplete/broken picker experience without any clear feedback.

## Fix Focus Areas
- web/admin/src/views/dashboard/topic_feed/editor.vue[245-257]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Preview loading can stick 🐞 Bug ☼ Reliability
Description
FeedPreviewPanel 在 resetState()/空 URI 提前返回时不会将 loading 置为 false;若之前一次 preview 请求仍在进行且随后触发
resetState(例如上层 props.inputUri 变化且 autoLoad=false),旧请求又因 requestSeq 变化跳过 finally 清理,可能导致 loading 永久为
true。
Code

web/admin/src/components/feed-preview/FeedPreviewPanel.vue[R41-52]

Evidence
resetState 只更新 requestSeq/feedContent/errorMessage;当 requestSeq 被推进后,旧请求的 finally 会因为
currentSeq!=requestSeq 不再负责关闭 loading,因此必须在 resetState/空输入路径主动关闭 loading。

web/admin/src/components/feed-preview/FeedPreviewPanel.vue[35-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`resetState()` increments `requestSeq` but doesn’t clear `loading`. If `resetState()` is called while a request is in-flight, that request’s `finally` will skip `loading=false` due to sequence mismatch, leaving the spinner stuck.

## Issue Context
This component is intended to be reusable and exposes `resetState()`; it also watches `props.inputUri` and calls `resetState()` when `autoLoad` is false.

## Fix Focus Areas
- web/admin/src/components/feed-preview/FeedPreviewPanel.vue[41-52]
- web/admin/src/components/feed-preview/FeedPreviewPanel.vue[54-74]

## Suggested fix
- In `resetState()`, set `loading.value = false`.
- In the `if (!uri)` early-return path inside `loadPreview`, also ensure `loading.value = false` (or rely on `resetState()` doing it).
- (Optional) Add `watch(..., { immediate: true })` when `autoLoad` is true so initial `inputUri` also loads consistently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Internal URI missing id 🐞 Bug ≡ Correctness
Description
前端 sourceToUri 在 recipe/topic 类型下即使 resourceId 为空也会生成非空 URI(如
feedcraft://recipe/),从而被计入“有效输入源”并可能触发预览/提交;该 URI 在运行时会因为缺少 resource id 被判为非法。
Code

web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts[R96-108]

Evidence
sourceToUri 会把空 resourceId 也拼到内部 URI 中;而运行时对 internal URI 的解析明确要求 resource id 非空,因此该 URI 会在构建/预览时失败。

web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts[96-108]
internal/feedruntime/builder.go[620-632]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`sourceToUri()` builds `feedcraft://recipe/${resourceId}` and `feedcraft://topic/${resourceId}` without trimming or checking emptiness, so blank selections produce `feedcraft://recipe/` which is non-empty and treated as valid.

## Issue Context
Runtime rejects internal URIs with empty resource id.

## Fix Focus Areas
- web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts[96-108]

## Suggested fix
- Update `sourceToUri`:
 - `const id = source.resourceId.trim()`
 - If `sourceType` is `recipe`/`topic` and `id === ''`, return `''`
 - Otherwise return `feedcraft://recipe/${id}` / `feedcraft://topic/${id}`
- This automatically fixes `countEnabledInputs()` and `canPreviewSource()` since both rely on `sourceToUri() !== ''`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In the controller layer you are calling NormalizeInputs multiple times for the same payload (e.g., before and after create/update, and both in ValidateTopicFeed and inside validateTopicConfig), which is redundant; consider centralizing these calls at the API boundary or inside a model hook so each request normalizes only once.
  • In GetTopicFeedDetail, subFeedHealth is now built from topicData.Inputs instead of InputURIs, which means disabled inputs will still be probed for health even though they are excluded from aggregation; consider filtering out disabled inputs here to align behavior with the Disabled flag semantics.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the controller layer you are calling `NormalizeInputs` multiple times for the same payload (e.g., before and after create/update, and both in `ValidateTopicFeed` and inside `validateTopicConfig`), which is redundant; consider centralizing these calls at the API boundary or inside a model hook so each request normalizes only once.
- In `GetTopicFeedDetail`, `subFeedHealth` is now built from `topicData.Inputs` instead of `InputURIs`, which means disabled inputs will still be probed for health even though they are excluded from aggregation; consider filtering out disabled inputs here to align behavior with the `Disabled` flag semantics.

## Individual Comments

### Comment 1
<location path="internal/controller/topic_feed.go" line_range="107-108" />
<code_context>
 		return
 	}

+	for _, topic := range topicList {
+		topic.NormalizeInputs()
+	}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** NormalizeInputs is called on a copy of each TopicFeed, so list responses never expose normalized Inputs

Ranging with `for _, topic := range topicList` passes each `TopicFeed` by value, so `NormalizeInputs` only updates a copy and `topicList` is unchanged. That means the `/topic_feed` list response won’t actually contain normalized `Inputs` that the frontend expects.

Iterate by index instead so you mutate the slice elements:

```go
for i := range topicList {
    topicList[i].NormalizeInputs()
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/controller/topic_feed.go Outdated
Comment on lines +107 to +108
for _, topic := range topicList {
topic.NormalizeInputs()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): NormalizeInputs is called on a copy of each TopicFeed, so list responses never expose normalized Inputs

Ranging with for _, topic := range topicList passes each TopicFeed by value, so NormalizeInputs only updates a copy and topicList is unchanged. That means the /topic_feed list response won’t actually contain normalized Inputs that the frontend expects.

Iterate by index instead so you mutate the slice elements:

for i := range topicList {
    topicList[i].NormalizeInputs()
}

Comment on lines +245 to +257
const loadPickerData = async () => {
pickerLoading.value = true;
try {
const [recipesRes, topicsRes] = await Promise.all([
getCustomRecipes(),
listTopicFeeds(),
]);
availableRecipes.value = recipesRes.data ?? [];
availableTopics.value = topicsRes.data ?? [];
} finally {
pickerLoading.value = false;
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. loadpickerdata() lacks error notice 📘 Rule violation ☼ Reliability

loadPickerData() performs network fetches without a catch, so failures can become unhandled and
no user-visible notification is shown. This violates the requirement to surface failing actions to
users instead of only failing silently/unobservably.
Agent Prompt
## Issue description
`loadPickerData()` fetches picker data via `getCustomRecipes()` and `listTopicFeeds()` but does not handle errors with a user-visible notification (and can result in an unhandled rejection).

## Issue Context
This is part of the new Topic Feed wizard page; if these calls fail, users may see an incomplete/broken picker experience without any clear feedback.

## Fix Focus Areas
- web/admin/src/views/dashboard/topic_feed/editor.vue[245-257]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread internal/dao/topic.go Outdated
Comment on lines 24 to 34
// Inputs carries URI plus optional description for admin display.
Inputs []TopicInput `json:"inputs,omitempty" gorm:"serializer:json"`

// List of URIs representing inputs (derived from Inputs for runtime).
// Uses a custom protocol for internal resources to make routing elegant and standard.
// Examples:
// - "feedcraft://recipe/my-tech-recipe" (Internal RecipeFeed)
// - "feedcraft://topic/sub-topic-id" (Nested internal TopicFeed)
// - "https://external.com/rss.xml" (External raw feed)
InputURIs []string `json:"input_uris" binding:"required" gorm:"serializer:json"`
InputURIs []string `json:"input_uris" gorm:"serializer:json"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Inputs not validated on save 🐞 Bug ≡ Correctness

CreateTopicFeed/UpdateTopicFeed 在写入数据库前未强制校验“至少一个启用输入源”,且 InputURIs 不再是 binding required,导致可保存
inputs/input_uris 为空的 TopicFeed,运行时构建 Topic 会得到 0 个输入或直接失败。
Agent Prompt
## Issue description
Create/Update API can persist a TopicFeed with no enabled inputs because `input_uris` is no longer `binding:"required"` and the controllers don’t validate `len(topicData.InputURIs) > 0` after `NormalizeInputs()`.

## Issue Context
The codebase already defines the business rule in `validateTopicConfig`: at least one enabled input is required. Runtime topic building iterates `topic.InputURIs` to construct providers.

## Fix Focus Areas
- internal/controller/topic_feed.go[63-79]
- internal/controller/topic_feed.go[114-147]
- internal/controller/topic_feed.go[281-351]

## Suggested fix
1. In `CreateTopicFeed` and `UpdateTopicFeed`, after `topicData.NormalizeInputs()`:
   - If `len(topicData.InputURIs) == 0`, return `400 BadRequest` with a clear message (e.g. "At least one enabled input source is required").
2. (Optional but stronger) Reuse/extend `validateTopicConfig` to provide richer errors (runtime build errors) for Create/Update as well, or add a lightweight URI validation step for internal URIs.
3. Ensure responses consistently return `input_uris` as an empty array (not null) when invalid input is rejected.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Replace flat topic input URIs with structured per-input objects (uri/description/disabled), add normalization and migration, update controllers and runtime to use enabled inputs, and implement a multi-step frontend editor, preview component, routes, and list/detail UI changes.

Changes

Topic Feed Input Management Redesign

Layer / File(s) Summary
Backend data model and normalization
internal/dao/topic.go, internal/dao/topic_test.go, internal/dao/migrate.go, .gitignore
Add TopicInput and Inputs []TopicInput on TopicFeed; implement NormalizeInputs() and EnabledInputURIs(); add migration migrateTopicFeedInputs to convert legacy input_uris; tests for normalization and migration; .dev-data/ added to .gitignore.
Controller handling and handler tests
internal/controller/topic_feed.go, internal/controller/feed_viewer_test.go, internal/controller/topic_feed_public_test.go
Normalize inputs in Create/Update/Validate paths, compute SubFeedHealth from Inputs, update validation to inputs[%d].uri, and update tests/payloads to the new inputs shape.
Feed builder runtime
internal/feedruntime/builder.go, internal/feedruntime/builder_test.go
Use TopicFeed.EnabledInputURIs() when building topic providers and update tests to construct Inputs entries.
Frontend API types and utilities
web/admin/src/api/topic.ts, web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts
Add TopicInput TS type and replace input_uris with inputs; add utilities for parse/serialize, form model conversion, normalization, enabled-input counting, and aggregator summary formatting.
Feed preview component
web/admin/src/components/feed-preview/FeedPreviewPanel.vue
New preview panel with loading/error/empty states, request-sequencing to avoid stale responses, auto-load watcher, and exposed load/reset API.
Aggregator and input source editors
web/admin/src/views/dashboard/topic_feed/components/TopicAggregatorEditor.vue, TopicInputSourcesEditor.vue
New components for editing aggregator steps (deduplicate/sort/limit) and input sources (external/recipe/topic) with preview drawer, disabled toggle, add/remove, and emits for model updates.
Editor routing
web/admin/src/router/routes/modules/worktable.ts
Add topic_feed/create and topic_feed/:id/edit child routes that lazy-load the editor and hide from menu.
Topic feed editor wizard
web/admin/src/views/dashboard/topic_feed/editor.vue
4-step wizard for creating/editing topic feeds (Basics, Inputs, Aggregation, Review) with picker loading, server-side validation, and create/update flows enforcing at least one enabled input.
Detail page input table
web/admin/src/views/dashboard/topic_feed/detail.vue
Replace static URI listing with table-backed inputs, show enabled/disabled counts, per-row disabled toggle with "keep at least one enabled" guard, update flow, and minor clipboard error handling change.
List page refactor
web/admin/src/views/dashboard/topic_feed/topic_feed.vue
Remove in-page modal/editor, add inputCount() helper and localized aggregator summaries, change add/edit to navigate to dedicated routes, and remove modal/form logic and styles.

Possibly related PRs

  • Colin-XKL/FeedCraft#782: Re-enables TopicFeed admin routes that this PR extends with create/edit child routes.
  • Colin-XKL/FeedCraft#723: Adds the preview API/client used by the new FeedPreviewPanel and preview integrations.
  • Colin-XKL/FeedCraft#629: Earlier topic feed DAO/engine work that introduced the prior InputURIs model this PR migrates away from.

"I'm a rabbit who trims and tidies feeds with glee,
uris get neat, descriptions sit where they should be.
A toggle hops in, to hide but never stray,
wizard-led steps guide the crafter on their way.
🥕✨ Happy feeds for all — from root to leaf and tree!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly describes the main enhancement: a multi-step wizard for Topic Feed management with input descriptions and preview capability, matching the primary changes in the PR.
Description check ✅ Passed The description thoroughly explains the PR's objectives, main changes, test coverage, and includes a reference to validation video, directly relating to the changeset across backend data models, controller logic, and frontend components.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/topic-feed-ux-f70a

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/controller/topic_feed.go (1)

287-324: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Per-input "Input URI cannot be empty" check is now unreachable.

NormalizeInputs() is invoked at Line 171 (in ValidateTopicFeed) and again at Line 288 here, and it drops any input whose trimmed URI is empty (lines 51-53 in topic.go). By the time the loop at Lines 316-324 runs, topicData.Inputs can no longer contain an empty URI, so the inputs[%d].uri error is never produced. Net effect: a submitted empty URI is silently dropped instead of being reported; if it was the only source, the user gets the generic "At least one enabled input source is required" message instead.

If silent dropping is intended, remove the dead loop; if you want to surface empty-URI errors, validate before normalizing.

♻️ Option A — validate raw inputs before normalization
 func validateTopicConfig(ctx context.Context, db *gorm.DB, topicData *dao.TopicFeed) (*TopicValidationResult, error) {
 	result := &TopicValidationResult{
 		Valid:    true,
 		Errors:   []TopicValidationIssue{},
 		Warnings: []TopicValidationIssue{},
 	}
-	if topicData != nil {
-		topicData.NormalizeInputs()
-	}
 	if topicData == nil {
 		result.Valid = false
 		result.Errors = append(result.Errors, TopicValidationIssue{
 			Field:   "topic",
 			Message: "Topic config is required",
 		})
 		return result, nil
 	}
+
+	for idx, input := range topicData.Inputs {
+		if strings.TrimSpace(input.URI) == "" {
+			result.Valid = false
+			result.Errors = append(result.Errors, TopicValidationIssue{
+				Field:   fmt.Sprintf("inputs[%d].uri", idx),
+				Message: "Input URI cannot be empty",
+			})
+		}
+	}
+	topicData.NormalizeInputs()

Then drop the now-duplicated loop at Lines 316-324.

🤖 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 `@internal/controller/topic_feed.go` around lines 287 - 324, The per-input
"Input URI cannot be empty" check is dead because topicData.NormalizeInputs()
removes empty URIs; move the per-input validation to run before calling
NormalizeInputs() in ValidateTopicFeed (and/or in the current function where
topicData.NormalizeInputs() is called) so empty URIs are reported from
topicData.Inputs (validate strings.TrimSpace(input.URI) == "" and append the
TopicValidationIssue for inputs[%d].uri), then remove the later redundant loop
that checks topicData.Inputs after normalization; keep the existing overall "At
least one enabled input source is required" check that uses topicData.InputURIs.
🤖 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 `@web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts`:
- Around line 96-108: sourceToUri currently returns
feedcraft://recipe/${resourceId} or feedcraft://topic/${resourceId} even when
resourceId is an empty string, causing countEnabledInputs, canPreviewSource and
normalizeTopicPayload to treat empty IDs as valid; update sourceToUri to treat
empty/whitespace resourceId as an empty URI by checking source.resourceId.trim()
=== '' and returning '' in that case for both 'recipe' and 'topic' branches so
client-side gating aligns with backend
parseInternalResourceURI/validateTopicFeed behavior.

---

Outside diff comments:
In `@internal/controller/topic_feed.go`:
- Around line 287-324: The per-input "Input URI cannot be empty" check is dead
because topicData.NormalizeInputs() removes empty URIs; move the per-input
validation to run before calling NormalizeInputs() in ValidateTopicFeed (and/or
in the current function where topicData.NormalizeInputs() is called) so empty
URIs are reported from topicData.Inputs (validate strings.TrimSpace(input.URI)
== "" and append the TopicValidationIssue for inputs[%d].uri), then remove the
later redundant loop that checks topicData.Inputs after normalization; keep the
existing overall "At least one enabled input source is required" check that uses
topicData.InputURIs.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecf26c98-4a76-44ef-831b-e4ce77ab7f2d

📥 Commits

Reviewing files that changed from the base of the PR and between 604e97f and 0e2b9d0.

📒 Files selected for processing (15)
  • .gitignore
  • internal/controller/topic_feed.go
  • internal/dao/topic.go
  • internal/dao/topic_test.go
  • web/admin/src/api/topic.ts
  • web/admin/src/components/feed-preview/FeedPreviewPanel.vue
  • web/admin/src/locale/en-US/topic.ts
  • web/admin/src/locale/zh-CN/topic.ts
  • web/admin/src/router/routes/modules/worktable.ts
  • web/admin/src/views/dashboard/topic_feed/components/TopicAggregatorEditor.vue
  • web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue
  • web/admin/src/views/dashboard/topic_feed/detail.vue
  • web/admin/src/views/dashboard/topic_feed/editor.vue
  • web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts
  • web/admin/src/views/dashboard/topic_feed/topic_feed.vue

Comment on lines +96 to +108
export const sourceToUri = (source: InputSourceItem): string => {
if (source.sourceType === 'recipe') {
return `feedcraft://recipe/${source.resourceId}`;
}
if (source.sourceType === 'topic') {
return `feedcraft://topic/${source.resourceId}`;
}
return source.externalUrl.trim();
};

export const countEnabledInputs = (sources: InputSourceItem[]): number =>
sources.filter((source) => sourceToUri(source) !== '' && !source.disabled)
.length;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect frontend utils around the mentioned snippet and usages
rg -n "topicInputUtils\.ts|sourceToUri|countEnabledInputs|canPreviewSource|normalizeTopicPayload" web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts
sed -n '1,220p' web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts

# Find call sites (editor.vue / topic input editor)
rg -n "countEnabledInputs|canPreviewSource|normalizeTopicPayload|sourceToUri" web/admin/src/views/dashboard/topic_feed
rg -n "configuredInputCount|at least one enabled input|TopicInputSourcesEditor" web/admin/src/views/dashboard/topic_feed -S

# 2) Locate backend validation/normalization (by name first)
rg -n "validateTopicFeed|NormalizeInputs|normalize.*inputs|topic.?feed" -S .

Repository: Colin-XKL/FeedCraft

Length of output: 12694


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect frontend API wrapper
sed -n '70,150p' web/admin/src/api/topic.ts

# Inspect backend controller + DAO normalization
sed -n '1,220p' internal/dao/topic.go
sed -n '220,520p' internal/dao/topic.go

sed -n '1,240p' internal/controller/topic_feed.go
sed -n '240,420p' internal/controller/topic_feed.go
sed -n '420,700p' internal/controller/topic_feed.go

# Inspect tests for NormalizeInputs
sed -n '1,220p' internal/dao/topic_test.go
sed -n '220,520p' internal/dao/topic_test.go

# Search for parsing of feedcraft://recipe/ and topic and how empty ids are handled
rg -n "feedcraft://recipe/|feedcraft://topic/|recipe/\\$|topic/\\$" internal/dao internal/controller -S

Repository: Colin-XKL/FeedCraft

Length of output: 19427


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate runtime URI parsing/building for topic feed inputs
rg -n "BuildTopicProvider|NewBuilder|feedcraft://recipe/|feedcraft://topic/" internal/feedruntime -S
rg -n "recipe/|topic/" internal/feedruntime -S

# Inspect the builder + topic provider implementation(s)
fd -t f -e go internal/feedruntime | head -n 50
# Print the files that likely contain builder/provider logic
ls -R internal/feedruntime | head -n 200

# Search for how recipe/topic IDs are extracted from URIs and whether empty IDs are rejected
rg -n "strings\\.TrimSpace\\(.*\\)|Split\\(|slice\\(|TrimPrefix|feedcraft://recipe/\\'" internal/feedruntime -S
rg -n "resourceId|recipeID|topicID|sub-topic" internal/feedruntime -S

Repository: Colin-XKL/FeedCraft

Length of output: 3519


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls internal/feedruntime
sed -n '1,220p' internal/feedruntime/builder.go
sed -n '220,520p' internal/feedruntime/builder.go

# Search within internal/feedruntime for the actual parsing logic
rg -n "feedcraft://recipe/|feedcraft://topic/" internal/feedruntime -S
rg -n "recipeID|topicID|strings\\.TrimSpace\\(.*\\)|Split\\(|TrimPrefix\\(|slice\\(" internal/feedruntime -S
rg -n "empty|missing|invalid|cannot be empty" internal/feedruntime -S

# Also inspect feed viewer tests that build from URIs
rg -n "Build.*Provider|feedcraft://recipe/|feedcraft://topic/" internal/controller -S
sed -n '1,260p' internal/controller/feed_viewer_test.go

Repository: Colin-XKL/FeedCraft

Length of output: 32532


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Frontend submit/validation gating
sed -n '180,420p' web/admin/src/views/dashboard/topic_feed/editor.vue
sed -n '1,120p' web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue
sed -n '160,330p' web/admin/src/views/dashboard/topic_feed/components/TopicInputSourcesEditor.vue

# Backend internal URI parsing (resource id empty handling)
sed -n '560,720p' internal/feedruntime/builder.go

# Find parseInternalResourceURI definition precisely
rg -n "func parseInternalResourceURI" -n internal/feedruntime/builder.go

Repository: Colin-XKL/FeedCraft

Length of output: 17740


Fix client-side gating for empty recipe/topic resourceId (UX consistency)

  • sourceToUri returns feedcraft://recipe/ / feedcraft://topic/ even when resourceId === '', so countEnabledInputs, canPreviewSource, and normalizeTopicPayload treat it as enabled/previewable.
  • validateTopicFeed ultimately rejects it: backend parseInternalResourceURI errors with missing resource id, and validateTopicConfig runs BuildTopicProvider during validation; the admin editor also blocks create/update when validation fails. Remaining gap is UX/wizard behavior (and preview may fail).
🐛 Proposed fix to treat empty resource id as empty URI
 export const sourceToUri = (source: InputSourceItem): string => {
   if (source.sourceType === 'recipe') {
-    return `feedcraft://recipe/${source.resourceId}`;
+    return source.resourceId ? `feedcraft://recipe/${source.resourceId}` : '';
   }
   if (source.sourceType === 'topic') {
-    return `feedcraft://topic/${source.resourceId}`;
+    return source.resourceId ? `feedcraft://topic/${source.resourceId}` : '';
   }
   return source.externalUrl.trim();
 };
🤖 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 `@web/admin/src/views/dashboard/topic_feed/topicInputUtils.ts` around lines 96
- 108, sourceToUri currently returns feedcraft://recipe/${resourceId} or
feedcraft://topic/${resourceId} even when resourceId is an empty string, causing
countEnabledInputs, canPreviewSource and normalizeTopicPayload to treat empty
IDs as valid; update sourceToUri to treat empty/whitespace resourceId as an
empty URI by checking source.resourceId.trim() === '' and returning '' in that
case for both 'recipe' and 'topic' branches so client-side gating aligns with
backend parseInternalResourceURI/validateTopicFeed behavior.

Co-authored-by: Colin <Colin_XKL@outlook.com>
Remove GORM BeforeSave hook.
Explicitly normalize inputs before Write/Update DB operations in Controller.
Strip redundant read-path normalizations.

Co-authored-by: Colin <Colin_XKL@outlook.com>
@Colin-XKL Colin-XKL merged commit 85838a2 into dev Jun 1, 2026
8 of 10 checks passed
@Colin-XKL Colin-XKL deleted the cursor/topic-feed-ux-f70a branch June 1, 2026 12:34
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