diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 4adca38aa0..46277d3349 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -246,11 +246,6 @@ "count": 1 } }, - "web/app/components/app/app-access-control/add-member-or-group-pop.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/app-publisher/features-wrapper.tsx": { "ts/no-explicit-any": { "count": 4 diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md index 9524394214..6eadd200f0 100644 --- a/packages/dify-ui/AGENTS.md +++ b/packages/dify-ui/AGENTS.md @@ -9,6 +9,7 @@ Shared design tokens, the `cn()` utility, CSS-first Tailwind styles, and headles - No imports from `web/`. No dependencies on next / i18next / ky / jotai / zustand. - One component per folder: `src//index.tsx`, optional `index.stories.tsx` and `__tests__/index.spec.tsx`. Add a matching `./` subpath to `package.json#exports`. - Props pattern: `Omit & VariantProps & { /* custom */ }`. +- Use plain `Omit<...>` only for non-union Base UI props. When a prop changes the valid shape of related props (for example `value` / `defaultValue`, `multiple` / `value`, or `clearable` / `onChange`), model that relationship with an explicit discriminated union or a distributive helper instead of flattening the props. - When a component accepts a prop typed from a shared internal module, `export type` it from that component so consumers import it from the component subpath. ## Overlay Primitive Selection: Tooltip vs PreviewCard vs Popover diff --git a/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx b/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx index a3c63f5a0c..52c2a0dd54 100644 --- a/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx +++ b/web/app/components/app/app-access-control/__tests__/access-control.spec.tsx @@ -254,9 +254,7 @@ describe('AddMemberOrGroupDialog', () => { await user.click(expandButton) expect(useAccessControlStore.getState().selectedGroupsForBreadcrumb).toEqual([baseGroup]) - const memberLabel = screen.getByText(baseMember.name) - const memberCheckbox = memberLabel.parentElement?.previousElementSibling as HTMLElement - fireEvent.click(memberCheckbox) + await user.click(screen.getByRole('option', { name: /Member One/ })) expect(useAccessControlStore.getState().specificMembers).toEqual([baseMember]) }) @@ -277,13 +275,13 @@ describe('AddMemberOrGroupDialog', () => { await user.type(screen.getByPlaceholderText('app.accessControlDialog.operateGroupAndMember.searchPlaceholder'), 'Group') expect(document.querySelector('.spin-animation')).toBeInTheDocument() - const groupCheckbox = screen.getByText(baseGroup.name).closest('div')?.previousElementSibling as HTMLElement - fireEvent.click(groupCheckbox) - fireEvent.click(groupCheckbox) + const groupOption = screen.getByRole('option', { name: /Group One/ }) + fireEvent.click(groupOption) + fireEvent.click(groupOption) - const memberCheckbox = screen.getByText(baseMember.name).parentElement?.previousElementSibling as HTMLElement - fireEvent.click(memberCheckbox) - fireEvent.click(memberCheckbox) + const memberOption = screen.getByRole('option', { name: /Member One/ }) + fireEvent.click(memberOption) + fireEvent.click(memberOption) fireEvent.click(screen.getByText('app.accessControlDialog.operateGroupAndMember.expand')) fireEvent.click(screen.getByText('app.accessControlDialog.operateGroupAndMember.allMembers')) @@ -307,7 +305,7 @@ describe('AddMemberOrGroupDialog', () => { await user.click(screen.getByText('common.operation.add')) - expect(screen.getByText('app.accessControlDialog.operateGroupAndMember.noResult')).toBeInTheDocument() + expect(screen.getByRole('status')).toHaveTextContent('app.accessControlDialog.operateGroupAndMember.noResult') }) }) diff --git a/web/app/components/app/app-access-control/__tests__/add-member-or-group-pop.spec.tsx b/web/app/components/app/app-access-control/__tests__/add-member-or-group-pop.spec.tsx index 725b121d30..d34756e85e 100644 --- a/web/app/components/app/app-access-control/__tests__/add-member-or-group-pop.spec.tsx +++ b/web/app/components/app/app-access-control/__tests__/add-member-or-group-pop.spec.tsx @@ -1,5 +1,5 @@ import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control' -import { fireEvent, render, screen } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import useAccessControlStore from '@/context/access-control-store' import { SubjectType } from '@/models/access-control' @@ -106,8 +106,7 @@ describe('AddMemberOrGroupDialog', () => { expect(useAccessControlStore.getState().selectedGroupsForBreadcrumb).toEqual([baseGroup]) - const memberCheckbox = screen.getByText(baseMember.name).parentElement?.previousElementSibling as HTMLElement - fireEvent.click(memberCheckbox) + await user.click(screen.getByRole('option', { name: /Member One/ })) expect(useAccessControlStore.getState().specificMembers).toEqual([baseMember]) }) @@ -125,6 +124,31 @@ describe('AddMemberOrGroupDialog', () => { await user.click(screen.getByText('common.operation.add')) - expect(screen.getByText('app.accessControlDialog.operateGroupAndMember.noResult')).toBeInTheDocument() + expect(screen.getByRole('status')).toHaveTextContent('app.accessControlDialog.operateGroupAndMember.noResult') + }) + + it('should keep breadcrumbs visible when the current group has no candidates', async () => { + useAccessControlStore.setState({ + selectedGroupsForBreadcrumb: [baseGroup], + }) + mockUseSearchForWhiteListCandidates.mockReturnValue({ + isLoading: false, + isFetchingNextPage: false, + fetchNextPage: vi.fn(), + data: { pages: [{ currPage: 1, subjects: [], hasMore: false }] }, + }) + + const user = userEvent.setup() + render() + + await user.click(screen.getByText('common.operation.add')) + + expect(screen.getByRole('button', { name: 'app.accessControlDialog.operateGroupAndMember.allMembers' })).toBeInTheDocument() + expect(screen.getByText(baseGroup.name)).toBeInTheDocument() + expect(screen.getByRole('status')).toHaveTextContent('app.accessControlDialog.operateGroupAndMember.noResult') + + await user.click(screen.getByRole('button', { name: 'app.accessControlDialog.operateGroupAndMember.allMembers' })) + + expect(useAccessControlStore.getState().selectedGroupsForBreadcrumb).toEqual([]) }) }) diff --git a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx index 8d9bf19ea3..1e3a992136 100644 --- a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx +++ b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx @@ -1,110 +1,207 @@ 'use client' +import type { ComboboxRootChangeEventDetails } from '@langgenius/dify-ui/combobox' import type { AccessControlAccount, AccessControlGroup, Subject, SubjectAccount, SubjectGroup } from '@/models/access-control' -import { FloatingOverlay } from '@floating-ui/react' import { Avatar } from '@langgenius/dify-ui/avatar' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxInputGroup, + ComboboxItem, + ComboboxItemText, + ComboboxList, + ComboboxStatus, + ComboboxTrigger, +} from '@langgenius/dify-ui/combobox' import { RiAddCircleFill, RiArrowRightSLine, RiOrganizationChart } from '@remixicon/react' import { useDebounce } from 'ahooks' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from '@/context/app-context' import { SubjectType } from '@/models/access-control' import { useSearchForWhiteListCandidates } from '@/service/access-control' import useAccessControlStore from '../../../../context/access-control-store' -import Checkbox from '../../base/checkbox' -import Input from '../../base/input' import Loading from '../../base/loading' export default function AddMemberOrGroupDialog() { const { t } = useTranslation() const [open, setOpen] = useState(false) const [keyword, setKeyword] = useState('') + const scrollRootRef = useRef(null) + const anchorRef = useRef(null) + const specificGroups = useAccessControlStore(s => s.specificGroups) + const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups) + const specificMembers = useAccessControlStore(s => s.specificMembers) + const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers) const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb) const debouncedKeyword = useDebounce(keyword, { wait: 500 }) const lastAvailableGroup = selectedGroupsForBreadcrumb[selectedGroupsForBreadcrumb.length - 1] const { isLoading, isFetchingNextPage, fetchNextPage, data } = useSearchForWhiteListCandidates({ keyword: debouncedKeyword, groupId: lastAvailableGroup?.id, resultsPerPage: 10 }, open) - const handleKeywordChange = (e: React.ChangeEvent) => { - setKeyword(e.target.value) - } + const pages = data?.pages ?? [] + const subjects = pages.flatMap(page => page.subjects ?? []) + const selectedSubjects = [ + ...specificGroups.map(groupToSubject), + ...specificMembers.map(memberToSubject), + ] + const hasResults = pages.length > 0 && subjects.length > 0 + const shouldShowBreadcrumb = hasResults || selectedGroupsForBreadcrumb.length > 0 + const hasMore = pages[pages.length - 1]?.hasMore ?? false - const anchorRef = useRef(null) useEffect(() => { - const hasMore = data?.pages?.[0]?.hasMore ?? false let observer: IntersectionObserver | undefined if (anchorRef.current) { observer = new IntersectionObserver((entries) => { if (entries[0]!.isIntersecting && !isLoading && hasMore) fetchNextPage() - }, { rootMargin: '20px' }) + }, { root: scrollRootRef.current, rootMargin: '20px' }) observer.observe(anchorRef.current) } return () => observer?.disconnect() - }, [isLoading, fetchNextPage, anchorRef, data]) + }, [isLoading, fetchNextPage, hasMore]) + + const handleOpenChange = (nextOpen: boolean) => { + if (!nextOpen) + setKeyword('') + + setOpen(nextOpen) + } + + const handleInputValueChange = (inputValue: string, details: ComboboxRootChangeEventDetails) => { + if (details.reason !== 'item-press') + setKeyword(inputValue) + } + + const handleValueChange = (nextSubjects: Subject[]) => { + const nextGroups: AccessControlGroup[] = [] + const nextMembers: AccessControlAccount[] = [] + + for (const subject of nextSubjects) { + if (subject.subjectType === SubjectType.GROUP) + nextGroups.push((subject as SubjectGroup).groupData) + else + nextMembers.push((subject as SubjectAccount).accountData) + } + + setSpecificGroups(nextGroups) + setSpecificMembers(nextMembers) + } return ( - - - - {t('operation.add', { ns: 'common' })} - - )} - /> - {open && } - + multiple + open={open} + value={selectedSubjects} + inputValue={keyword} + items={subjects} + itemToStringLabel={getSubjectLabel} + itemToStringValue={getSubjectValue} + isItemEqualToValue={isSameSubject} + filter={null} + onOpenChange={handleOpenChange} + onInputValueChange={handleInputValueChange} + onValueChange={handleValueChange} + > + + + -
+
- + +
- { - isLoading - ?
- : (data?.pages?.length ?? 0) > 0 - ? ( - <> -
- -
-
- {renderGroupOrMember(data?.pages ?? [])} + {isLoading + ? ( + + + + ) + : ( + <> + {shouldShowBreadcrumb && ( +
+ +
+ )} + {hasResults + ? ( + <> + + {(subject: Subject) => } + {isFetchingNextPage && } -
-
- - ) - : ( -
- {t('accessControlDialog.operateGroupAndMember.noResult', { ns: 'app' })} -
- ) - } +
+ + ) + : ( + + {t('accessControlDialog.operateGroupAndMember.noResult', { ns: 'app' })} + + )} + + )}
- - + + ) } -type GroupOrMemberData = { subjects: Subject[], currPage: number }[] -function renderGroupOrMember(data: GroupOrMemberData) { - return data?.map((page) => { - return ( -
- {page.subjects?.map((item, index) => { - if (item.subjectType === SubjectType.GROUP) - return - return - })} -
- ) - }) ?? null +function groupToSubject(group: AccessControlGroup): SubjectGroup { + return { + subjectId: group.id, + subjectType: SubjectType.GROUP, + groupData: group, + } +} + +function memberToSubject(member: AccessControlAccount): SubjectAccount { + return { + subjectId: member.id, + subjectType: SubjectType.ACCOUNT, + accountData: member, + } +} + +function getSubjectLabel(subject: Subject) { + if (subject.subjectType === SubjectType.GROUP) + return (subject as SubjectGroup).groupData.name + + return (subject as SubjectAccount).accountData.name +} + +function getSubjectValue(subject: Subject) { + return `${subject.subjectType}:${subject.subjectId}` +} + +function isSameSubject(item: Subject, value: Subject) { + return item.subjectId === value.subjectId && item.subjectType === value.subjectType +} + +function SubjectItem({ subject }: { subject: Subject }) { + if (subject.subjectType === SubjectType.GROUP) + return + + return } function SelectedGroupsBreadCrumb() { @@ -112,13 +209,13 @@ function SelectedGroupsBreadCrumb() { const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb) const { t } = useTranslation() - const handleBreadCrumbClick = useCallback((index: number) => { + const handleBreadCrumbClick = (index: number) => { const newGroups = selectedGroupsForBreadcrumb.slice(0, index + 1) setSelectedGroupsForBreadcrumb(newGroups) - }, [setSelectedGroupsForBreadcrumb, selectedGroupsForBreadcrumb]) - const handleReset = useCallback(() => { + } + const handleReset = () => { setSelectedGroupsForBreadcrumb([]) - }, [setSelectedGroupsForBreadcrumb]) + } const hasBreadcrumb = selectedGroupsForBreadcrumb.length > 0 return ( @@ -162,104 +259,111 @@ function SelectedGroupsBreadCrumb() { type GroupItemProps = { group: AccessControlGroup + subject: Subject } -function GroupItem({ group }: GroupItemProps) { +function GroupItem({ group, subject }: GroupItemProps) { const { t } = useTranslation() const specificGroups = useAccessControlStore(s => s.specificGroups) - const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups) const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb) const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb) const isChecked = specificGroups.some(g => g.id === group.id) - const handleCheckChange = useCallback(() => { - if (!isChecked) { - const newGroups = [...specificGroups, group] - setSpecificGroups(newGroups) - } - else { - const newGroups = specificGroups.filter(g => g.id !== group.id) - setSpecificGroups(newGroups) - } - }, [specificGroups, setSpecificGroups, group, isChecked]) - const handleExpandClick = useCallback(() => { + const handleExpandClick = () => { setSelectedGroupsForBreadcrumb([...selectedGroupsForBreadcrumb, group]) - }, [selectedGroupsForBreadcrumb, setSelectedGroupsForBreadcrumb, group]) + } + return ( - - -
-
-
- +
+ + + +
+
+
-
-

{group.name}

-

{group.groupSize}

-
+ {group.name} + {group.groupSize} + + - +
) } type MemberItemProps = { member: AccessControlAccount + subject: Subject } -function MemberItem({ member }: MemberItemProps) { +function MemberItem({ member, subject }: MemberItemProps) { const currentUser = useSelector(s => s.userProfile) const { t } = useTranslation() const specificMembers = useAccessControlStore(s => s.specificMembers) - const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers) const isChecked = specificMembers.some(m => m.id === member.id) - const handleCheckChange = useCallback(() => { - if (!isChecked) { - const newMembers = [...specificMembers, member] - setSpecificMembers(newMembers) - } - else { - const newMembers = specificMembers.filter(m => m.id !== member.id) - setSpecificMembers(newMembers) - } - }, [specificMembers, setSpecificMembers, member, isChecked]) return ( - - -
+ + +
-

{member.name}

+ {member.name} {currentUser.email === member.email && ( -

+ ( {t('you', { ns: 'common' })} ) -

+ )} -
-

{member.email}

+ + {member.email}
) } type BaseItemProps = { className?: string + subject: Subject children: React.ReactNode } -function BaseItem({ children, className }: BaseItemProps) { +function BaseItem({ children, className, subject }: BaseItemProps) { return ( -
+ {children} -
+ + ) +} + +function SelectionBox({ checked }: { checked: boolean }) { + return ( +
+ - + + {(document: SimpleDocumentDetail | null) => ( + + )} + + + -
- - {documentsList - ? ( - ({ - id: d.id, - name: d.name, - extension: d.data_source_detail_dict?.upload_file?.extension || '', - }))} - onChange={handleChange} - /> - ) - : ( -
- -
- )} -
- - + + + {data + ? ( + documentsList.length > 0 + ? ( + + ) + : ( + + {t('noData', { ns: 'common' })} + + ) + ) + : ( + + + + )} +
+ ) } -export default React.memo(DocumentPicker) diff --git a/web/app/components/datasets/common/document-picker/preview-document-picker.tsx b/web/app/components/datasets/common/document-picker/preview-document-picker.tsx index 597ceda9a5..fb90bf57f7 100644 --- a/web/app/components/datasets/common/document-picker/preview-document-picker.tsx +++ b/web/app/components/datasets/common/document-picker/preview-document-picker.tsx @@ -14,7 +14,6 @@ import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import FileIcon from '../document-file-icon' -import DocumentList from './document-list' type Props = { className?: string @@ -74,7 +73,7 @@ const PreviewDocumentPicker: FC = ({ {files?.length > 1 &&
{t('preprocessDocument', { ns: 'dataset', num: files.length })}
} {files?.length > 0 ? ( - @@ -90,3 +89,27 @@ const PreviewDocumentPicker: FC = ({ ) } export default React.memo(PreviewDocumentPicker) + +function PreviewDocumentList({ + list, + onChange, +}: { + list: DocumentItem[] + onChange: (value: DocumentItem) => void +}) { + return ( +
+ {list.map(item => ( + + ))} +
+ ) +} diff --git a/web/app/components/datasets/documents/detail/__tests__/document-title.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/document-title.spec.tsx index 3eb1017b8d..b48575d209 100644 --- a/web/app/components/datasets/documents/detail/__tests__/document-title.spec.tsx +++ b/web/app/components/datasets/documents/detail/__tests__/document-title.spec.tsx @@ -1,6 +1,7 @@ +import type { SimpleDocumentDetail } from '@/models/datasets' import { render } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { ChunkingMode } from '@/models/datasets' +import { ChunkingMode, DataSourceType } from '@/models/datasets' import { DocumentTitle } from '../document-title' @@ -11,13 +12,23 @@ vi.mock('@/next/navigation', () => ({ }), })) -// Mock DocumentPicker vi.mock('../../../common/document-picker', () => ({ - default: ({ datasetId, value, onChange }: { datasetId: string, value: unknown, onChange: (doc: { id: string }) => void }) => ( + DocumentPicker: ({ + datasetId, + value, + parentMode, + onChange, + }: { + datasetId: string + value?: SimpleDocumentDetail | null + parentMode?: string + onChange: (doc: { id: string }) => void + }) => (
onChange({ id: 'new-doc-id' })} > Document Picker @@ -25,6 +36,42 @@ vi.mock('../../../common/document-picker', () => ({ ), })) +const createDocument = (overrides: Partial = {}): SimpleDocumentDetail => ({ + id: 'doc-1', + batch: 'batch-1', + position: 1, + dataset_id: 'dataset-1', + data_source_type: DataSourceType.FILE, + data_source_info: { + upload_file: { + id: 'file-1', + name: 'document.pdf', + size: 1024, + extension: 'pdf', + mime_type: 'application/pdf', + created_by: 'user-1', + created_at: Date.now(), + }, + job_id: 'job-1', + url: '', + }, + dataset_process_rule_id: 'rule-1', + name: 'Document 1', + created_from: 'web', + created_by: 'user-1', + created_at: Date.now(), + indexing_status: 'completed', + display_status: 'enabled', + doc_form: ChunkingMode.text, + doc_language: 'en', + enabled: true, + word_count: 1000, + archived: false, + updated_at: Date.now(), + hit_count: 0, + ...overrides, +}) + describe('DocumentTitle', () => { beforeEach(() => { vi.clearAllMocks() @@ -69,31 +116,26 @@ describe('DocumentTitle', () => { expect(getByTestId('document-picker').getAttribute('data-dataset-id')).toBe('test-dataset-id') }) - it('should pass value props to DocumentPicker', () => { + it('should pass the selected document to DocumentPicker', () => { + const document = createDocument({ id: 'doc-current' }) const { getByTestId } = render( , ) - const value = JSON.parse(getByTestId('document-picker').getAttribute('data-value') || '{}') - expect(value.name).toBe('test-document') - expect(value.extension).toBe('pdf') - expect(value.chunkingMode).toBe(ChunkingMode.text) - expect(value.parentMode).toBe('paragraph') + expect(getByTestId('document-picker')).toHaveAttribute('data-value-id', 'doc-current') + expect(getByTestId('document-picker')).toHaveAttribute('data-parent-mode', 'paragraph') }) - it('should default parentMode to paragraph when parent_mode is undefined', () => { + it('should pass no parent mode when it is undefined', () => { const { getByTestId } = render( , ) - const value = JSON.parse(getByTestId('document-picker').getAttribute('data-value') || '{}') - expect(value.parentMode).toBe('paragraph') + expect(getByTestId('document-picker')).toHaveAttribute('data-parent-mode', '') }) it('should apply custom wrapperCls', () => { @@ -119,24 +161,23 @@ describe('DocumentTitle', () => { }) describe('Edge Cases', () => { - it('should handle undefined optional props', () => { + it('should handle an empty document value', () => { const { getByTestId } = render( , ) - const value = JSON.parse(getByTestId('document-picker').getAttribute('data-value') || '{}') - expect(value.name).toBeUndefined() - expect(value.extension).toBeUndefined() + expect(getByTestId('document-picker')).toHaveAttribute('data-value-id', '') }) it('should maintain structure when rerendered', () => { const { rerender, getByTestId } = render( - , + , ) - rerender() + rerender() expect(getByTestId('document-picker').getAttribute('data-dataset-id')).toBe('dataset-2') + expect(getByTestId('document-picker').getAttribute('data-value-id')).toBe('doc-2') }) }) }) diff --git a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx index e717475b38..e8946ce584 100644 --- a/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/__tests__/index.spec.tsx @@ -114,9 +114,20 @@ vi.mock('../batch-modal', () => ({ })) vi.mock('../document-title', () => ({ - DocumentTitle: ({ name, extension }: { name?: string, extension?: string }) => ( -
{name}
- ), + DocumentTitle: ({ + document, + }: { + document?: { + name?: string + data_source_detail_dict?: { upload_file?: { extension?: string } } + data_source_info?: { upload_file?: { extension?: string } } + } | null + }) => { + const extension = document?.data_source_detail_dict?.upload_file?.extension + ?? document?.data_source_info?.upload_file?.extension + + return
{document?.name}
+ }, })) vi.mock('../segment-add', () => ({ diff --git a/web/app/components/datasets/documents/detail/document-title.tsx b/web/app/components/datasets/documents/detail/document-title.tsx index d5bf5345ae..0a1cfbf61a 100644 --- a/web/app/components/datasets/documents/detail/document-title.tsx +++ b/web/app/components/datasets/documents/detail/document-title.tsx @@ -1,39 +1,29 @@ -import type { FC } from 'react' -import type { ChunkingMode, ParentMode } from '@/models/datasets' +import type { ParentMode, SimpleDocumentDetail } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' import { useRouter } from '@/next/navigation' -import DocumentPicker from '../../common/document-picker' +import { DocumentPicker } from '../../common/document-picker' type DocumentTitleProps = { datasetId: string - extension?: string - name?: string - chunkingMode?: ChunkingMode - parent_mode?: ParentMode - iconCls?: string - textCls?: string + document?: SimpleDocumentDetail | null + parentMode?: ParentMode wrapperCls?: string } -export const DocumentTitle: FC = ({ +export function DocumentTitle({ datasetId, - extension, - name, - chunkingMode, - parent_mode, + document, + parentMode, wrapperCls, -}) => { +}: DocumentTitleProps) { const router = useRouter() + return (
{ router.push(`/datasets/${datasetId}/documents/${doc.id}`) }} diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index 732d7ffb28..190cf8edf7 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/web/app/components/datasets/documents/detail/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import type { DataSourceInfo, DocumentDisplayStatus, FileItem, FullDocumentDetail, LegacyDataSourceInfo } from '@/models/datasets' +import type { DocumentDisplayStatus, FileItem, FullDocumentDetail } from '@/models/datasets' import type { SegmentImportStatus } from '@/types/dataset' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' @@ -38,10 +38,6 @@ const NON_TERMINAL_DISPLAY_STATUSES = new Set( DisplayStatusList.filter(s => s === 'queuing' || s === 'indexing' || s === 'paused'), ) -const isLegacyDataSourceInfo = (info?: DataSourceInfo): info is LegacyDataSourceInfo => { - return !!info && 'upload_file' in info -} - const DocumentDetail: FC = ({ datasetId, documentId }) => { const router = useRouter() const searchParams = useSearchParams() @@ -123,14 +119,6 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { const embedding = NON_TERMINAL_DISPLAY_STATUSES.has(documentDetail?.display_status as DocumentDisplayStatus) - const documentUploadFile = useMemo(() => { - if (!documentDetail?.data_source_info) - return undefined - if (isLegacyDataSourceInfo(documentDetail.data_source_info)) - return documentDetail.data_source_info.upload_file - return undefined - }, [documentDetail?.data_source_info]) - const invalidChunkList = useInvalid(useSegmentListKey) const invalidChildChunkList = useInvalid(useChildSegmentListKey) const invalidDocumentList = useInvalidDocumentList(datasetId) @@ -212,11 +200,9 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => {
{embeddingAvailable && documentDetail && !documentDetail.archived && !isFullDocMode && (