From 42889d23e5d9cc6e1aac88add59737361ebd4706 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Tue, 28 Apr 2026 16:05:26 +0800 Subject: [PATCH] fix(web): snippet card --- .../components/apps/__tests__/list.spec.tsx | 14 +++-- .../__tests__/snippet-card.spec.tsx | 60 +++++++++++++++++++ .../snippets/components/snippet-card.tsx | 38 +++++++++--- .../hooks/__tests__/use-snippet-init.spec.ts | 6 +- .../snippets/__tests__/index.spec.tsx | 8 ++- .../__tests__/snippet-detail-card.spec.tsx | 13 +++- .../__tests__/snippet-list-item.spec.tsx | 8 +-- .../snippets/snippet-detail-card.tsx | 25 +++++--- .../snippets/snippet-list-item.tsx | 7 +-- web/i18n/en-US/snippet.json | 3 + web/i18n/zh-Hans/snippet.json | 3 + web/types/snippet.ts | 3 +- 12 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 web/app/components/snippets/components/__tests__/snippet-card.spec.tsx diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 932db440da..6fec23cd81 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -160,8 +160,9 @@ const defaultSnippetData = { icon_url: '', }, created_at: 1704067200, - updated_at: '2024-01-02 10:00', - author: '', + created_by: 'user-1', + updated_at: 1704153600, + updated_by: 'user-2', }, ], total: 1, @@ -321,8 +322,9 @@ describe('List', () => { icon_url: '', }, created_at: 1704067200, - updated_at: '2024-01-02 10:00', - author: '', + created_by: 'user-1', + updated_at: 1704153600, + updated_by: 'user-2', }, ] defaultSnippetData.pages[0]!.total = 1 @@ -678,8 +680,8 @@ describe('List', () => { }) it('should reuse the shared empty state when no snippets are available', () => { - defaultSnippetData.pages[0].data = [] - defaultSnippetData.pages[0].total = 0 + defaultSnippetData.pages[0]!.data = [] + defaultSnippetData.pages[0]!.total = 0 renderList({ pageType: 'snippets' }) diff --git a/web/app/components/snippets/components/__tests__/snippet-card.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-card.spec.tsx new file mode 100644 index 0000000000..a5192fecac --- /dev/null +++ b/web/app/components/snippets/components/__tests__/snippet-card.spec.tsx @@ -0,0 +1,60 @@ +import type { SnippetListItem } from '@/types/snippet' +import { render, screen } from '@testing-library/react' +import SnippetCard from '../snippet-card' + +vi.mock('@/service/use-common', () => ({ + useMembers: () => ({ + data: { + accounts: [ + { id: 'creator-id', name: 'Creator', email: 'creator@example.com', avatar: '', avatar_url: null, role: 'editor', last_login_at: '', created_at: '', status: 'active' }, + { id: 'updater-id', name: 'Updater', email: 'updater@example.com', avatar: '', avatar_url: null, role: 'editor', last_login_at: '', created_at: '', status: 'active' }, + ], + }, + }), +})) + +vi.mock('@/utils/time', () => ({ + formatTime: () => 'formatted-time', +})) + +const createSnippet = (overrides: Partial = {}): SnippetListItem => ({ + id: 'snippet-1', + name: 'Tone Rewriter', + description: 'Rewrites rough drafts.', + type: 'node', + is_published: true, + use_count: 19, + icon_info: { + icon_type: 'emoji', + icon: '🪄', + icon_background: '#E0EAFF', + icon_url: '', + }, + created_at: 1_704_067_200, + created_by: 'creator-id', + updated_at: 1_704_153_600, + updated_by: 'updater-id', + ...overrides, +}) + +describe('SnippetCard', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Rendering', () => { + it('should render updater name and updated time from member data', () => { + render() + + expect(screen.getByText('Tone Rewriter')).toBeInTheDocument() + expect(screen.getByText('snippet.updatedBy:{"name":"Updater","time":"formatted-time"}')).toBeInTheDocument() + expect(screen.queryByText('Creator')).not.toBeInTheDocument() + }) + + it('should fall back to creator name when updater is unavailable', () => { + render() + + expect(screen.getByText('snippet.updatedBy:{"name":"Creator","time":"formatted-time"}')).toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/snippets/components/snippet-card.tsx b/web/app/components/snippets/components/snippet-card.tsx index 764dba66f4..3edb16a3e3 100644 --- a/web/app/components/snippets/components/snippet-card.tsx +++ b/web/app/components/snippets/components/snippet-card.tsx @@ -1,9 +1,12 @@ 'use client' import type { SnippetListItem } from '@/types/snippet' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import Link from '@/next/link' +import { useMembers } from '@/service/use-common' +import { formatTime } from '@/utils/time' type Props = { snippet: SnippetListItem @@ -11,16 +14,35 @@ type Props = { const SnippetCard = ({ snippet }: Props) => { const { t } = useTranslation('snippet') + const { data: membersData } = useMembers() + + const memberNameById = useMemo(() => { + return new Map((membersData?.accounts ?? []).map(member => [member.id, member.name])) + }, [membersData?.accounts]) + + const updatedByName = memberNameById.get(snippet.updated_by) + || memberNameById.get(snippet.created_by) + || t('unknownUser') + + const updatedAt = snippet.updated_at || snippet.created_at + const updatedAtText = formatTime({ + date: (updatedAt > 1_000_000_000_000 ? updatedAt : updatedAt * 1000), + dateFormat: `${t('segment.dateTimeFormat', { ns: 'datasetDocuments' })}`, + }) + const updatedText = t('updatedBy', { + name: updatedByName, + time: updatedAtText, + }) return (
{!snippet.is_published && ( -
- Draft +
+ {t('draft')}
)} -
+
{ imageUrl={snippet.icon_info.icon_url} />
-
+
{snippet.name}
@@ -39,11 +61,9 @@ const SnippetCard = ({ snippet }: Props) => { {snippet.description}
-
- {snippet.author} - · - {snippet.updated_at} - {!snippet.is_published && ( +
+ {updatedText} + {snippet.is_published && ( <> · {t('usageCount', { count: snippet.use_count })} diff --git a/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts b/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts index 92d1c8c6a2..b73c11c4ad 100644 --- a/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts +++ b/web/app/components/snippets/hooks/__tests__/use-snippet-init.spec.ts @@ -72,8 +72,9 @@ describe('useSnippetInit', () => { }, input_fields: [], created_at: 1_712_300_000, + created_by: 'user-1', updated_at: 1_712_300_000, - author: 'Evan', + updated_by: 'user-1', }, error: null, isLoading: false, @@ -124,8 +125,9 @@ describe('useSnippetInit', () => { }, ], created_at: 1_712_300_000, + created_by: 'user-1', updated_at: 1_712_300_000, - author: 'Evan', + updated_by: 'user-1', }, error: null, isLoading: false, diff --git a/web/app/components/workflow/block-selector/snippets/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/snippets/__tests__/index.spec.tsx index d0d489c215..3a2cb3abc9 100644 --- a/web/app/components/workflow/block-selector/snippets/__tests__/index.spec.tsx +++ b/web/app/components/workflow/block-selector/snippets/__tests__/index.spec.tsx @@ -74,7 +74,6 @@ describe('Snippets', () => { id: 'snippet-1', name: 'Customer Review', description: 'Snippet description', - author: 'Evan', type: 'group', is_published: true, version: '1.0.0', @@ -87,7 +86,9 @@ describe('Snippets', () => { }, input_fields: [], created_at: 1, + created_by: 'user-1', updated_at: 2, + updated_by: 'user-1', }], }], }, @@ -127,7 +128,6 @@ describe('Snippets', () => { id: 'snippet-1', name: 'Customer Review', description: 'Snippet description', - author: 'Evan', type: 'group', is_published: true, version: '1.0.0', @@ -140,7 +140,9 @@ describe('Snippets', () => { }, input_fields: [], created_at: 1, + created_by: 'user-1', updated_at: 2, + updated_by: 'user-1', }], }], }, @@ -155,7 +157,7 @@ describe('Snippets', () => { fireEvent.click(screen.getByText('Customer Review')) - expect(mockHandleInsertSnippet).toHaveBeenCalledWith('snippet-1') + expect(mockHandleInsertSnippet).toHaveBeenCalledWith('snippet-1', undefined) }) }) }) diff --git a/web/app/components/workflow/block-selector/snippets/__tests__/snippet-detail-card.spec.tsx b/web/app/components/workflow/block-selector/snippets/__tests__/snippet-detail-card.spec.tsx index ade2bd2d61..d5d1520b2b 100644 --- a/web/app/components/workflow/block-selector/snippets/__tests__/snippet-detail-card.spec.tsx +++ b/web/app/components/workflow/block-selector/snippets/__tests__/snippet-detail-card.spec.tsx @@ -8,11 +8,20 @@ vi.mock('@/service/use-snippet-workflows', () => ({ useSnippetPublishedWorkflow: (...args: unknown[]) => mockUseSnippetPublishedWorkflow(...args), })) +vi.mock('@/service/use-common', () => ({ + useMembers: () => ({ + data: { + accounts: [ + { id: 'user-1', name: 'Evan', email: 'evan@example.com', avatar: '', avatar_url: null, role: 'editor', last_login_at: '', created_at: '', status: 'active' }, + ], + }, + }), +})) + const createSnippet = (overrides: Partial = {}): PublishedSnippetListItem => ({ id: 'snippet-1', name: 'Customer Review', description: 'Snippet description', - author: 'Evan', type: 'group', is_published: true, use_count: 3, @@ -23,7 +32,9 @@ const createSnippet = (overrides: Partial = {}): Publi icon_url: '', }, created_at: 1, + created_by: 'user-1', updated_at: 2, + updated_by: 'user-1', ...overrides, }) diff --git a/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx b/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx index 0a1292a68e..fbc8ead0ae 100644 --- a/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx +++ b/web/app/components/workflow/block-selector/snippets/__tests__/snippet-list-item.spec.tsx @@ -6,7 +6,6 @@ const createSnippet = (overrides: Partial = {}): Publi id: 'snippet-1', name: 'Customer Review', description: 'Snippet description', - author: 'Evan', type: 'group', is_published: true, use_count: 3, @@ -17,7 +16,9 @@ const createSnippet = (overrides: Partial = {}): Publi icon_url: '', }, created_at: 1, + created_by: 'user-1', updated_at: 2, + updated_by: 'user-1', ...overrides, }) @@ -38,10 +39,9 @@ describe('SnippetListItem', () => { ) expect(screen.getByText('Customer Review')).toBeInTheDocument() - expect(screen.queryByText('Evan')).not.toBeInTheDocument() }) - it('should render author when hovered', () => { + it('should not render metadata when hovered', () => { render( { />, ) - expect(screen.getByText('Evan')).toBeInTheDocument() + expect(screen.getByText('Customer Review')).toBeInTheDocument() }) }) diff --git a/web/app/components/workflow/block-selector/snippets/snippet-detail-card.tsx b/web/app/components/workflow/block-selector/snippets/snippet-detail-card.tsx index 1a3545a96f..2aea34f6eb 100644 --- a/web/app/components/workflow/block-selector/snippets/snippet-detail-card.tsx +++ b/web/app/components/workflow/block-selector/snippets/snippet-detail-card.tsx @@ -1,7 +1,9 @@ import type { FC } from 'react' import type { SnippetListItem } from '@/types/snippet' import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' +import { useMembers } from '@/service/use-common' import { useSnippetPublishedWorkflow } from '@/service/use-snippet-workflows' import BlockIcon from '../../block-icon' import { BlockEnum } from '../../types' @@ -15,9 +17,16 @@ type SnippetDetailCardProps = { const SnippetDetailCard: FC = ({ snippet, }) => { - const { author, description, icon_info, name } = snippet + const { description, icon_info, name } = snippet + const { t } = useTranslation('snippet') + const { data: membersData } = useMembers() const { data: workflow } = useSnippetPublishedWorkflow(snippet.id) + const authorName = useMemo(() => { + const member = membersData?.accounts?.find(member => member.id === snippet.created_by) + return member?.name || t('unknownUser') + }, [membersData?.accounts, snippet.created_by, t]) + const blockTypes = useMemo(() => { const graph = workflow?.graph if (!graph || typeof graph !== 'object') @@ -51,7 +60,7 @@ const SnippetDetailCard: FC = ({ }, [workflow?.graph]) return ( -
+
= ({ background={icon_info.icon_background} imageUrl={icon_info.icon_url} /> -
{name}
+
{name}
{!!description && ( -
+
{description}
)} @@ -80,11 +89,9 @@ const SnippetDetailCard: FC = ({
)}
- {!!author && ( -
- {author} -
- )} +
+ {authorName} +
) } diff --git a/web/app/components/workflow/block-selector/snippets/snippet-list-item.tsx b/web/app/components/workflow/block-selector/snippets/snippet-list-item.tsx index fa4cd13b3b..7be3bffba9 100644 --- a/web/app/components/workflow/block-selector/snippets/snippet-list-item.tsx +++ b/web/app/components/workflow/block-selector/snippets/snippet-list-item.tsx @@ -36,14 +36,9 @@ const SnippetListItem = ({ background={snippet.icon_info.icon_background} imageUrl={snippet.icon_info.icon_url} /> -
+
{snippet.name}
- {isHovered && snippet.author && ( -
- {snippet.author} -
- )}
) } diff --git a/web/i18n/en-US/snippet.json b/web/i18n/en-US/snippet.json index fda6980f69..30a72ee90e 100644 --- a/web/i18n/en-US/snippet.json +++ b/web/i18n/en-US/snippet.json @@ -7,6 +7,7 @@ "deleteConfirmTitle": "Delete Snippet?", "deleteFailed": "Failed to delete snippet", "deleted": "Snippet deleted", + "draft": "Draft", "editDialogTitle": "Edit Snippet Info", "editDone": "Snippet info updated", "editFailed": "Failed to update snippet info", @@ -32,6 +33,8 @@ "sectionOrchestrate": "Orchestrate", "testRunButton": "Test run", "typeLabel": "Snippet", + "unknownUser": "User", + "updatedBy": "{{name}} updated {{time}}", "usageCount": "Used {{count}} times", "variableInspect": "Variable Inspect" } diff --git a/web/i18n/zh-Hans/snippet.json b/web/i18n/zh-Hans/snippet.json index b8ed686559..d033e76849 100644 --- a/web/i18n/zh-Hans/snippet.json +++ b/web/i18n/zh-Hans/snippet.json @@ -7,6 +7,7 @@ "deleteConfirmTitle": "删除 Snippet?", "deleteFailed": "删除 Snippet 失败", "deleted": "Snippet 已删除", + "draft": "草稿", "editDialogTitle": "编辑 Snippet 信息", "editDone": "Snippet 信息已更新", "editFailed": "更新 Snippet 信息失败", @@ -32,6 +33,8 @@ "sectionOrchestrate": "编排", "testRunButton": "测试运行", "typeLabel": "Snippet", + "unknownUser": "用户", + "updatedBy": "{{name}} 更新于 {{time}}", "usageCount": "已使用 {{count}} 次", "variableInspect": "变量查看" } diff --git a/web/types/snippet.ts b/web/types/snippet.ts index f0651c6ebf..c84012b17a 100644 --- a/web/types/snippet.ts +++ b/web/types/snippet.ts @@ -22,8 +22,9 @@ export type Snippet = { icon_info: SnippetIconInfo input_fields: SnippetInputField[] created_at: number + created_by: string updated_at: number - author: string + updated_by: string } export type SnippetListItem = Omit