From e7be86f710cf980dfc203147aa06dc942dddbe00 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 19 May 2026 00:33:42 +0530 Subject: [PATCH 01/17] Context center folder crud, integrate search api and bug fixes --- .../src/components/application/tree/tree.tsx | 321 ++++++++++++++ .../main/resources/ui/src/components/index.ts | 1 + .../resources/ui/src/stories/Tree.stories.tsx | 406 ++++++++++++++++++ .../e2e/Features/ContextCenter.spec.ts | 146 ++++++- .../KnowledgeCenterReviewerWorkflow.spec.ts | 135 ++++++ .../ui/playwright/utils/userWorkflowUtils.ts | 276 ++++++++++++ .../ContextCenterHeader.component.tsx | 23 +- .../ContextCenterHeader.interface.ts | 3 + .../CreateFolderModal.component.tsx | 120 ++++++ .../CreateFolderModal.test.tsx | 257 +++++++++++ .../DocumentFolderView.component.tsx | 241 +++++++++++ .../DocumentsView/DocumentFolderView.test.tsx | 405 +++++++++++++++++ .../DocumentsView/DocumentsView.component.tsx | 56 +-- .../DocumentsView/DocumentsView.interface.ts | 3 + .../MoveToFolderModal.component.tsx | 162 +++++++ .../MoveToFolderModal.test.tsx | 300 +++++++++++++ .../UploadDocumentModal.component.tsx | 38 +- .../UploadDocumentModal.interface.ts | 6 +- .../KnowledgeCard/KnowledgeCard.tsx | 3 +- .../KnowledgePageDetailComponent.tsx | 10 + .../KnowledgePageListComponent.tsx | 72 +++- .../KnowledgePagesHierarchy.tsx | 2 +- .../resources/ui/src/enums/search.enum.ts | 1 + .../ui/src/interface/search.interface.ts | 5 + .../ui/src/locale/languages/ar-sa.json | 4 + .../ui/src/locale/languages/de-de.json | 4 + .../ui/src/locale/languages/en-us.json | 4 + .../ui/src/locale/languages/es-es.json | 4 + .../ui/src/locale/languages/fr-fr.json | 4 + .../ui/src/locale/languages/gl-es.json | 4 + .../ui/src/locale/languages/he-he.json | 4 + .../ui/src/locale/languages/ja-jp.json | 4 + .../ui/src/locale/languages/ko-kr.json | 4 + .../ui/src/locale/languages/mr-in.json | 4 + .../ui/src/locale/languages/nl-nl.json | 4 + .../ui/src/locale/languages/pr-pr.json | 4 + .../ui/src/locale/languages/pt-br.json | 4 + .../ui/src/locale/languages/pt-pt.json | 4 + .../ui/src/locale/languages/ru-ru.json | 4 + .../ui/src/locale/languages/th-th.json | 4 + .../ui/src/locale/languages/tr-tr.json | 4 + .../ui/src/locale/languages/zh-cn.json | 4 + .../ui/src/locale/languages/zh-tw.json | 4 + .../ContextCenterArticlesPage.tsx | 12 + .../ContextCenterDashboardPage.tsx | 34 +- .../ContextCenterDocumentsPage.tsx | 146 ++++++- .../main/resources/ui/src/rest/assetAPI.ts | 97 ++++- .../ui/src/rest/knowledgeCenterAPI.ts | 5 +- .../ui/src/utils/ContextCenterUtils.tsx | 56 ++- .../ui/src/utils/DatabaseServiceUtils.tsx | 4 +- .../resources/ui/src/utils/TagClassBase.ts | 1 + 51 files changed, 3293 insertions(+), 130 deletions(-) create mode 100644 openmetadata-ui-core-components/src/main/resources/ui/src/components/application/tree/tree.tsx create mode 100644 openmetadata-ui-core-components/src/main/resources/ui/src/stories/Tree.stories.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/tree/tree.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/tree/tree.tsx new file mode 100644 index 000000000000..9d799eb3eee2 --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/application/tree/tree.tsx @@ -0,0 +1,321 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { cx } from '@/utils/cx'; +import { ChevronRight, RefreshCw01 } from '@untitledui/icons'; +import type { + ComponentPropsWithRef, + ComponentType, + HTMLAttributes, + ReactNode, +} from 'react'; +import type { + TreeItemProps as AriaTreeItemProps, + TreeLoadMoreItemProps as AriaTreeLoadMoreItemProps, + TreeProps as AriaTreeProps, + Key, + Selection, + TreeItemContentRenderProps, +} from 'react-aria-components'; +import { + Button as AriaButton, + Tree as AriaTree, + TreeHeader as AriaTreeHeader, + TreeItem as AriaTreeItem, + TreeItemContent as AriaTreeItemContent, + TreeLoadMoreItem as AriaTreeLoadMoreItem, + TreeSection as AriaTreeSection, +} from 'react-aria-components'; + +export type { Key, Selection }; + +// ---- Tree --------------------------------------------------------------- + +export interface TreeProps + extends Omit, 'className' | 'children'> { + /** Additional CSS class name for the tree container. */ + className?: string; + /** The contents of the tree. */ + children?: ReactNode | ((item: T) => ReactNode); +} + +const TreeRoot = ({ + className, + children, + ...props +}: TreeProps) => { + return ( + + {children as AriaTreeProps['children']} + + ); +}; + +// ---- TreeItem ----------------------------------------------------------- + +export interface TreeItemProps + extends Omit, 'children'> { + /** The content of this tree item. Typically `Tree.ItemContent`. */ + children: ReactNode; + /** Additional CSS class name applied to the item row. */ + className?: string | AriaTreeItemProps['className']; +} + +const TreeItemComponent = ({ + className, + children, + ...props +}: TreeItemProps) => { + return ( + + cx( + 'tw:group/tree-item tw:outline-none tw:rounded-md', + 'tw:cursor-pointer tw:select-none', + state.isDisabled && 'tw:opacity-50 tw:cursor-not-allowed', + state.isFocusVisible && 'tw:ring-2 tw:ring-inset tw:ring-brand-300', + typeof className === 'function' ? className(state) : className + ) + }> + {children} + + ); +}; + +// ---- TreeItemContent ---------------------------------------------------- + +export interface TreeItemContentProps { + /** + * The content to render. Can be static ReactNode or a render function + * receiving `TreeItemContentRenderProps`. + */ + children: + | ReactNode + | ((renderProps: TreeItemContentRenderProps) => ReactNode); + /** Additional CSS class name for the content wrapper. */ + className?: string; + /** + * Optional icon component rendered between the chevron and the label. + * Accepts any `@untitledui/icons`-compatible component. + */ + icon?: ComponentType>; + /** Additional CSS class name applied to the icon. */ + iconClassName?: string; + /** + * When `true`, an animated expand/collapse chevron is rendered automatically. + * Set to `false` to render your own expand indicator. Defaults to `true`. + */ + showExpandIcon?: boolean; +} + +const TreeItemContentComponent = ({ + className, + children, + icon: Icon, + iconClassName, + showExpandIcon = true, +}: TreeItemContentProps) => { + return ( + + {(renderProps: TreeItemContentRenderProps) => { + const { isExpanded, hasChildItems, level } = renderProps; + + return ( +
+ {showExpandIcon && ( +
+ ); + }} +
+ ); +}; + +// ---- TreeLoadMoreItem --------------------------------------------------- + +export type TreeLoadMoreItemProps = Omit< + AriaTreeLoadMoreItemProps, + 'className' | 'children' +> & { + /** Additional CSS class name for the load-more row. */ + className?: string; + /** Content shown inside the row. Defaults to a spinner + "Loading…" / "Load more". */ + children?: ReactNode; +}; + +const TreeLoadMoreItemComponent = ({ + isLoading, + children, + className, + ...props +}: TreeLoadMoreItemProps) => { + return ( + + {isLoading ? ( + + + ) : ( + children ?? 'Load more' + )} + + ); +}; + +// ---- TreeSection -------------------------------------------------------- + +export type TreeSectionProps = ComponentPropsWithRef< + typeof AriaTreeSection +>; + +const TreeSectionComponent = (props: TreeSectionProps) => ( + +); + +// ---- TreeHeader --------------------------------------------------------- + +export interface TreeHeaderProps { + children?: ReactNode; + className?: string; +} + +const TreeHeaderComponent = ({ className, children }: TreeHeaderProps) => { + return ( + + {children} + + ); +}; + +// ---- TreeExpandButton --------------------------------------------------- + +export interface TreeExpandButtonProps + extends Omit, 'className'> { + /** Additional CSS class name. */ + className?: string; +} + +/** + * An accessible expand/collapse button for use inside `Tree.ItemContent`. + * Render with slot="chevron" so React Aria's Tree handles keyboard & ARIA. + */ +const TreeExpandButton = ({ className, ...props }: TreeExpandButtonProps) => { + return ( + + cx( + 'tw:flex tw:items-center tw:justify-center tw:w-4 tw:h-4 tw:shrink-0', + 'tw:rounded tw:outline-none tw:text-fg-quaternary', + 'tw:transition-transform tw:duration-200 tw:ease-in-out', + state.isFocusVisible && 'tw:ring-2 tw:ring-brand-300', + className + ) + } + slot="chevron"> + + ); +}; + +// ---- Compound export ---------------------------------------------------- + +/** + * Tree renders a hierarchical list of items with keyboard navigation, + * selection, and expand/collapse support. Built on React Aria's Tree + * primitives for full accessibility. + * + * @example + * ```tsx + * + * + * Databases + * + * Postgres + * + * + * + * ``` + */ +const _Tree = TreeRoot as typeof TreeRoot & { + Item: typeof TreeItemComponent; + ItemContent: typeof TreeItemContentComponent; + LoadMoreItem: typeof TreeLoadMoreItemComponent; + Section: typeof TreeSectionComponent; + Header: typeof TreeHeaderComponent; + ExpandButton: typeof TreeExpandButton; +}; + +_Tree.Item = TreeItemComponent; +_Tree.ItemContent = TreeItemContentComponent; +_Tree.LoadMoreItem = TreeLoadMoreItemComponent; +_Tree.Section = TreeSectionComponent; +_Tree.Header = TreeHeaderComponent; +_Tree.ExpandButton = TreeExpandButton; + +export { + _Tree as Tree, + TreeExpandButton, + TreeHeaderComponent as TreeHeader, + TreeItemComponent as TreeItem, + TreeItemContentComponent as TreeItemContent, + TreeLoadMoreItemComponent as TreeLoadMoreItem, + TreeSectionComponent as TreeSection, +}; diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts b/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts index 0e25cee0d5d4..e01fd5b9fb27 100644 --- a/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/components/index.ts @@ -93,6 +93,7 @@ export { getField, } from './application/form-field/form-field'; export * from './application/accordion/accordion'; +export * from './application/tree/tree'; export { MobileNavigationHeader } from './application/app-navigation/base-components/mobile-header'; export { NavAccountCard, diff --git a/openmetadata-ui-core-components/src/main/resources/ui/src/stories/Tree.stories.tsx b/openmetadata-ui-core-components/src/main/resources/ui/src/stories/Tree.stories.tsx new file mode 100644 index 000000000000..c99447b161dc --- /dev/null +++ b/openmetadata-ui-core-components/src/main/resources/ui/src/stories/Tree.stories.tsx @@ -0,0 +1,406 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import type { Key, Selection } from 'react-aria-components'; +import { Tree } from '../components/application/tree/tree'; + +const meta = { + title: 'Components/Tree', + component: Tree, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( +
+ + + src/ + + components/ + + + Button.tsx + + + + + Input.tsx + + + + + utils/ + + cx.ts + + + + + public/ + + + index.html + + + + +
+ ), +}; + +export const DefaultExpanded: StoryObj = { + render: () => ( +
+ + + public + + Tables + + orders + + + + customers + + + + + Views + + + order_summary + + + + + +
+ ), +}; + +export const SingleSelection: StoryObj = { + render: () => { + const [selected, setSelected] = useState(new Set(['orders'])); + + return ( +
+

+ Selected:{' '} + {selected === 'all' + ? 'all' + : [...(selected as Set)].join(', ') || 'none'} +

+ + + Tables + + orders + + + + customers + + + + + products + + + + + Views + + + order_summary + + + + +
+ ); + }, +}; + +export const MultipleSelection: StoryObj = { + render: () => { + const [selected, setSelected] = useState( + new Set(['orders', 'customers']) + ); + + return ( +
+

+ Selected:{' '} + {selected === 'all' + ? 'all' + : [...(selected as Set)].join(', ') || 'none'} +

+ + + Databases + + orders + + + + customers + + + + + products + + + + +
+ ); + }, +}; + +export const WithDisabledItems: StoryObj = { + render: () => ( +
+ + + Tables + + orders + + + + customers (disabled) + + + + products + + + +
+ ), +}; + +export const WithAction: StoryObj = { + render: () => { + const [lastAction, setLastAction] = useState(''); + + return ( +
+

+ Last action: {lastAction || 'none'} +

+ setLastAction(String(key))}> + + Pipeline + + + extract + + + + + transform + + + + load + + + +
+ ); + }, +}; + +export const EmptyState: StoryObj = { + render: () => ( +
+ ( +
+ No items found +
+ )}> + {[]} +
+
+ ), +}; + +export const WithLoadMore: StoryObj = { + render: () => { + const [isLoading, setIsLoading] = useState(false); + const [items, setItems] = useState(['orders', 'customers', 'products']); + + const handleLoadMore = () => { + setIsLoading(true); + setTimeout(() => { + setItems((prev) => [...prev, 'invoices', 'shipments']); + setIsLoading(false); + }, 1500); + }; + + return ( +
+ + + Tables + {items.map((item) => ( + + + {item} + + + ))} + + + +
+ ); + }, +}; + +export const DeeplyNested: StoryObj = { + render: () => ( +
+ + + Collate + + Data Engineering + + Pipelines + + ETL + + + daily_orders + + + + + hourly_events + + + + + + + +
+ ), +}; + +export const ControlledExpansion: StoryObj = { + render: () => { + const [expanded, setExpanded] = useState>(new Set(['root'])); + + return ( +
+
+ + +
+ + + Root + + Group A + + + Item A1 + + + + + Item A2 + + + + + Group B + + + Item B1 + + + + + +
+ ); + }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts index bc47a103f72f..530ec38f5d57 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts @@ -150,15 +150,13 @@ test.describe('Context Center', () => { formData.append('entityLink', '<#E::page::contextCenter.documents>'); formData.append('assetType', 'External'); - await apiContext.post('/api/v1/attachments/upload', { + await apiContext.post('/api/v1/contextCenter/drive/files/upload', { multipart: { file: { name: 'seed-document.txt', mimeType: 'text/plain', buffer: fileContent, }, - entityLink: '<#E::page::contextCenter.documents>', - assetType: 'External', }, }); @@ -492,6 +490,148 @@ test.describe('Context Center', () => { }); }); + // ─── Search ────────────────────────────────────────────────────────────────── + + test.describe('Search', () => { + test('searching articles filters the list to matching results', async ({ + page, + }) => { + await navigateToArticles(page); + + const header = page.getByTestId('context-center-header'); + const searchInput = header + .getByTestId('search-input') + .getByLabel('Search Articles'); + await searchInput.fill(ARTICLE_TITLE); + + // Wait for search results to update + await page.waitForResponse( + (res) => + res.url().includes('/api/v1/search/query') && res.status() === 200 + ); + + // The pre-created article appears in results + const card = page.getByTestId(ARTICLE_TITLE); + + await expect(card.first()).toBeVisible(); + }); + + test('searching articles with no match shows empty state', async ({ + page, + }) => { + await navigateToArticles(page); + + const header = page.getByTestId('context-center-header'); + const searchInput = header + .getByTestId('search-input') + .getByLabel('Search Articles'); + await searchInput.fill('zzznomatchzzz_playwright'); + + await page.waitForResponse( + (res) => + res.url().includes('/api/v1/search/query') && res.status() === 200 + ); + + await expect(page.getByTestId('no-data-placeholder')).toBeVisible({ + timeout: 8000, + }); + }); + + test('clearing article search restores the full list', async ({ page }) => { + await navigateToArticles(page); + + const header = page.getByTestId('context-center-header'); + const searchInput = header + .getByTestId('search-input') + .getByLabel('Search Articles'); + + await searchInput.fill('zzznomatch'); + await page.waitForResponse( + (res) => + res.url().includes('/api/v1/search/query') && res.status() === 200 + ); + + await searchInput.clear(); + await waitForAllLoadersToDisappear(page); + + const card = page.getByTestId(ARTICLE_TITLE); + await expect(card.first()).toBeVisible(); + }); + + test('searching documents filters the list to matching results', async ({ + page, + }) => { + await navigateToDocuments(page); + + const header = page.getByTestId('context-center-header'); + const searchInput = header + .getByTestId('search-input') + .getByLabel('Search Documents'); + await searchInput.fill('seed-document'); + + await page.waitForResponse( + (res) => + res.url().includes('/api/v1/search/query') && res.status() === 200 + ); + + const view = page.getByTestId('documents-view'); + const rows = view.locator('[data-testid^="document-row-"]'); + const count = await rows.count(); + + // If the seed document was indexed, it appears; otherwise the empty state shows + if (count > 0) { + await expect(rows.first()).toBeVisible(); + } else { + await expect(page.getByTestId('no-data-placeholder')).toBeVisible({ + timeout: 8000, + }); + } + }); + + test('searching documents with no match shows empty state', async ({ + page, + }) => { + await navigateToDocuments(page); + + const header = page.getByTestId('context-center-header'); + const searchInput = header + .getByTestId('search-input') + .getByLabel('Search Documents'); + await searchInput.fill('zzznomatchzzz_playwright'); + + await page.waitForResponse( + (res) => + res.url().includes('/api/v1/search/query') && res.status() === 200 + ); + + await expect(page.getByTestId('no-data-placeholder')).toBeVisible({ + timeout: 8000, + }); + }); + + test('clearing document search restores the full list', async ({ + page, + }) => { + await navigateToDocuments(page); + + const header = page.getByTestId('context-center-header'); + const searchInput = header + .getByTestId('search-input') + .getByLabel('Search Documents'); + + await searchInput.fill('zzznomatch'); + await page.waitForResponse( + (res) => + res.url().includes('/api/v1/search/query') && res.status() === 200 + ); + + await searchInput.clear(); + await waitForAllLoadersToDisappear(page); + + await expect(page.getByTestId('documents-view')).toBeVisible(); + }); + }); + // ─── Articles Page ─────────────────────────────────────────────────────────── test.describe('Articles Page', () => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts new file mode 100644 index 000000000000..de0c05d6b939 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts @@ -0,0 +1,135 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from '@playwright/test'; +import { performAdminLogin } from '../../utils/admin'; +import { getApiContext, redirectToHomePage, uuid } from '../../utils/common'; +import { waitForAllLoadersToDisappear } from '../../utils/entity'; +import { + checkNotificationAndApproveTask, + createUserApprovalWorkflowWithOwners, + verifyTaskStatus, +} from '../../utils/userWorkflowUtils'; +import { test } from '../fixtures/pages'; + +let knowledgePage: { fullyQualifiedName: string; id: string }; + +test.beforeAll('Setup workflow and knowledge page', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await createUserApprovalWorkflowWithOwners(apiContext, 'page'); + + const createResponse = await apiContext.post('/api/v1/knowledgeCenter', { + data: { + name: `pw-article-${uuid()}`, + displayName: `PW Article ${uuid()}`, + pageType: 'Article', + page: { + publicationDate: new Date().toISOString(), + relatedArticles: [], + }, + description: 'This is a test description for the knowledge page', + }, + }); + expect(createResponse.status()).toBe(201); + knowledgePage = await createResponse.json(); + + await afterAction(); +}); + +test.describe( + 'User Approval Workflow - Knowledge Page', + { tag: ['@Governance'] }, + () => { + test('Knowledge Page reviewer approval flow', async ({ + page, + dataConsumerPage, + }) => { + test.setTimeout(300_000); + + await redirectToHomePage(page); + await redirectToHomePage(dataConsumerPage); + + const { apiContext, afterAction } = await getApiContext(page); + const { apiContext: dcApiContext, afterAction: dcAfterAction } = + await getApiContext(dataConsumerPage); + + const dcUserResponse = await dcApiContext.get( + '/api/v1/users/loggedInUser' + ); + expect(dcUserResponse.status()).toBe(200); + const dataConsumerUser = await dcUserResponse.json(); + await dcAfterAction(); + + const patchResponse = await apiContext.patch( + `/api/v1/knowledgeCenter/${knowledgePage.id}`, + { + data: [ + { + op: 'add', + path: '/reviewers/0', + value: { + id: dataConsumerUser.id, + type: 'user', + displayName: dataConsumerUser.displayName, + fullyQualifiedName: dataConsumerUser.fullyQualifiedName, + name: dataConsumerUser.name, + }, + }, + ], + headers: { 'Content-Type': 'application/json-patch+json' }, + } + ); + expect(patchResponse.status()).toBe(200); + knowledgePage = await patchResponse.json(); + + const encodedFqn = encodeURIComponent(knowledgePage.fullyQualifiedName); + + await test.step('Navigate to Context Center Page', async () => { + await page.goto(`/context-center/articles/${encodedFqn}`); + await waitForAllLoadersToDisappear(page); + }); + + await test.step('Verify In Review status', async () => { + await verifyTaskStatus( + page, + 'inReview', + knowledgePage, + 'In Review', + apiContext, + 'knowledgeCenter' + ); + }); + + await test.step('Reviewer - Check notification and approve task', async () => { + await checkNotificationAndApproveTask(dataConsumerPage, async () => { + await dataConsumerPage.goto( + `/context-center/articles/${encodedFqn}` + ); + }); + }); + + await test.step('Verify Approved status', async () => { + await verifyTaskStatus( + page, + 'success', + knowledgePage, + 'Approved', + apiContext, + 'knowledgeCenter' + ); + }); + + await afterAction(); + }); + } +); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts new file mode 100644 index 000000000000..1b037372c9cd --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts @@ -0,0 +1,276 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { APIRequestContext, expect, Page } from '@playwright/test'; +import { SidebarItem } from '../constant/sidebar'; +import { DataProduct } from '../support/domain/DataProduct'; +import { TagClass } from '../support/tag/TagClass'; +import { redirectToHomePage, toastNotification, uuid } from './common'; +import { selectDataProduct } from './domain'; +import { waitForAllLoadersToDisappear } from './entity'; +import { sidebarClick } from './sidebar'; +import { TASK_OPEN_FETCH_LINK } from './task'; +import { approveTaskFromDetails } from './taskWorkflow'; + +export const checkNotificationAndApproveTask = async ( + dataConsumerPage: Page, + navigateToEntity: () => Promise +): Promise => { + await redirectToHomePage(dataConsumerPage); + await navigateToEntity(); + + await dataConsumerPage.getByTestId('activity_feed').click(); + const taskFeeds = dataConsumerPage.waitForResponse(TASK_OPEN_FETCH_LINK); + await dataConsumerPage + .getByTestId('global-setting-left-panel') + .getByText('Tasks') + .click(); + await taskFeeds; + + const taskCard = dataConsumerPage.getByTestId('task-feed-card').first(); + await taskCard.waitFor({ state: 'visible', timeout: 15_000 }); + await taskCard.click(); + + await approveTaskFromDetails(dataConsumerPage); + await toastNotification(dataConsumerPage, /Task resolved successfully/); +}; + +export const createUserApprovalWorkflowWithOwners = async ( + apiContext: APIRequestContext, + entityType = 'table', + workflowName?: string +) => { + const id = uuid(); + const name = workflowName ?? `pw-user-approval-owners-${entityType}-${id}`; + + const workflowPayload = { + name, + displayName: name, + description: 'User approval workflow for data asset entities using owners', + type: 'eventBasedEntity', + trigger: { + type: 'eventBasedEntity', + config: { + entityTypes: [entityType], + events: ['Created', 'Updated'], + exclude: [], + }, + output: ['relatedEntity', 'updatedBy'], + }, + nodes: [ + { + type: 'startEvent', + subType: 'startEvent', + name: 'start', + displayName: 'start', + }, + { + type: 'automatedTask', + subType: 'checkEntityAttributesTask', + name: 'checkEntityAttributesTask_1', + displayName: 'Check Condition', + input: ['relatedEntity'], + output: ['result'], + branches: ['true', 'false'], + config: { + rules: '{"and":[{"!=":[{"var":"description"},null]}]}', + }, + inputNamespaceMap: { + relatedEntity: 'global', + }, + }, + { + type: 'automatedTask', + subType: 'setEntityAttributeTask', + name: 'setEntityAttributeTask_1', + displayName: 'Set Action', + input: ['relatedEntity', 'updatedBy'], + output: [], + config: { + fieldName: 'status', + fieldValue: 'In Review', + }, + inputNamespaceMap: { + relatedEntity: 'global', + updatedBy: 'global', + }, + }, + { + type: 'endEvent', + subType: 'endEvent', + name: 'endEvent_1', + displayName: 'End', + }, + { + type: 'userTask', + subType: 'userApprovalTask', + name: 'userApprovalTask_1', + displayName: 'Request Approval', + input: ['relatedEntity'], + output: ['updatedBy'], + branches: ['true', 'false'], + config: { + assignees: { + addReviewers: false, + addOwners: true, + candidates: [], + }, + approvalThreshold: 1, + rejectionThreshold: 1, + }, + inputNamespaceMap: { + relatedEntity: 'global', + }, + }, + { + type: 'automatedTask', + subType: 'setEntityAttributeTask', + name: 'setEntityAttributeTask_2', + displayName: 'Set Action', + input: ['relatedEntity', 'updatedBy'], + output: [], + config: { + fieldName: 'status', + fieldValue: 'Approved', + }, + inputNamespaceMap: { + relatedEntity: 'global', + updatedBy: 'userApprovalTask_1', + }, + }, + { + type: 'endEvent', + subType: 'endEvent', + name: 'endEvent_2', + displayName: 'End', + }, + { + type: 'endEvent', + subType: 'endEvent', + name: 'endEvent_3', + displayName: 'End', + }, + ], + edges: [ + { + from: 'start', + to: 'checkEntityAttributesTask_1', + }, + { + from: 'checkEntityAttributesTask_1', + to: 'setEntityAttributeTask_1', + condition: 'true', + }, + { + from: 'checkEntityAttributesTask_1', + to: 'endEvent_1', + condition: 'false', + }, + { + from: 'setEntityAttributeTask_1', + to: 'userApprovalTask_1', + }, + { + from: 'userApprovalTask_1', + to: 'setEntityAttributeTask_2', + condition: 'true', + }, + { + from: 'userApprovalTask_1', + to: 'endEvent_2', + condition: 'false', + }, + { + from: 'setEntityAttributeTask_2', + to: 'endEvent_3', + }, + ], + config: { + storeStageStatus: true, + }, + }; + + const response = await apiContext.post( + '/api/v1/governance/workflowDefinitions', + { data: workflowPayload } + ); + + return response.json(); +}; + + +const getFQN = (entity: EntityWithFQN): string => { + if (entity instanceof TagClass && entity.responseData) { + return entity.responseData.fullyQualifiedName || ''; + } + if (entity instanceof DataProduct && entity.data) { + return entity.data.fullyQualifiedName || ''; + } + if ('fullyQualifiedName' in entity) { + return entity.fullyQualifiedName || ''; + } + + return ''; +}; + + +type EntityWithFQN = DataProduct | TagClass | { fullyQualifiedName: string }; + +export const verifyTaskStatus = async ( + page: Page, + statusBadge: string, + entity: EntityWithFQN, + statusLabel: string, + apiContext: APIRequestContext, + entityType: 'dataProduct' | 'tag' | 'knowledgeCenter' = 'dataProduct' +): Promise => { + const entityFQN = encodeURIComponent(getFQN(entity)); + + const apiEndpoints = { + dataProduct: `/api/v1/dataProducts/name/${entityFQN}?fields=reviewers`, + tag: `/api/v1/tags/name/${entityFQN}?fields=reviewers`, + knowledgeCenter: `/api/v1/contextCenter/pages/name/${entityFQN}?fields=reviewers`, + }; + + await expect + .poll( + async () => { + const response = await apiContext + .get(apiEndpoints[entityType]) + .then((res) => res.json()); + + return response?.entityStatus; + }, + { + message: `Wait for ${entityType} status to be ${statusLabel}`, + timeout: 300_000, + intervals: [60_000, 40_000, 20_000], + } + ) + .toBe(statusLabel); + + await redirectToHomePage(page); + + if (entityType === 'dataProduct') { + await sidebarClick(page, SidebarItem.DATA_PRODUCT); + await selectDataProduct(page, (entity as DataProduct).data); + } else if (entityType === 'tag') { + await sidebarClick(page, SidebarItem.TAGS); + await (entity as TagClass).visitPage(page); + } else { + await page.goto(`/context-center/articles/${entityFQN}`); + } + await waitForAllLoadersToDisappear(page); + await expect( + page.locator(`.status-badge.${statusBadge} .status-badge-label`) + ).toContainText(statusLabel); +}; \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx index ba97b6603929..ca78c917a8d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx @@ -11,8 +11,8 @@ * limitations under the License. */ -import { Button, Card, Typography } from '@openmetadata/ui-core-components'; -import { Plus, UploadCloud02 } from '@untitledui/icons'; +import { Button, Card, Input, Typography } from '@openmetadata/ui-core-components'; +import { Plus, SearchMd, UploadCloud02 } from '@untitledui/icons'; import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; @@ -27,6 +27,9 @@ const ContextCenterHeader: FC = ({ onCreateArticle, onUploadFile, actionsSlot, + searchQuery, + searchPlaceholder, + onSearch, }) => { const { t } = useTranslation(); const breadcrumbInsideCard = contextCenterClassBase.isBreadcrumbInsideCard(); @@ -78,7 +81,7 @@ const ContextCenterHeader: FC = ({ /> )} -
+
{title} @@ -87,7 +90,19 @@ const ContextCenterHeader: FC = ({ {subtitle} )}
- {hasPermission ? actionsSlot ?? defaultActions : null} +
+ {onSearch && ( + + )} + {hasPermission ? actionsSlot ?? defaultActions : null} +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.interface.ts index 3a0003f12dcf..71d3678c40f6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.interface.ts @@ -23,4 +23,7 @@ export interface ContextCenterHeaderProps { onUploadFile?: () => void; /** Replaces the default action buttons with a custom slot */ actionsSlot?: React.ReactNode; + searchQuery?: string; + searchPlaceholder?: string; + onSearch?: (value: string) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx new file mode 100644 index 000000000000..f7eb6ca1302e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Button, + Dialog, + Input, + Modal, + ModalOverlay, + Typography, +} from '@openmetadata/ui-core-components'; +import { AxiosError } from 'axios'; +import { Folder } from 'generated/entity/data/folder'; +import { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { createFolder } from '../../../rest/assetAPI'; +import { showErrorToast } from '../../../utils/ToastUtils'; + +export interface CreateFolderModalProps { + isOpen: boolean; + onClose: () => void; + onCreated: (folder: Folder) => void; +} + +const CreateFolderModal: FC = ({ + isOpen, + onClose, + onCreated, +}) => { + const { t } = useTranslation(); + const [name, setName] = useState(''); + const [isCreating, setIsCreating] = useState(false); + + const handleClose = () => { + setName(''); + onClose(); + }; + + const handleCreate = async () => { + const trimmed = name.trim(); + + if (!trimmed) { + return; + } + + try { + setIsCreating(true); + const folder = await createFolder({ name: trimmed, displayName: trimmed }); + onCreated(folder); + handleClose(); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsCreating(false); + } + }; + + return ( + !open && !isCreating && handleClose()}> + + + +
+ + {t('label.entity-name', { entity: t('label.folder') })} + + setName(value)} + /> +
+ +
+ + +
+
+
+
+
+ ); +}; + +export default CreateFolderModal; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx new file mode 100644 index 000000000000..52dd405ca7d5 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx @@ -0,0 +1,257 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { createFolder } from 'rest/assetAPI'; +import CreateFolderModal from './CreateFolderModal.component'; + +jest.mock('rest/assetAPI', () => ({ + createFolder: jest.fn(), +})); + +jest.mock('@openmetadata/ui-core-components', () => ({ + Button: jest.fn( + ({ + children, + onPress, + isDisabled, + isLoading, + 'data-testid': testId, + }: { + children: React.ReactNode; + onPress?: () => void; + isDisabled?: boolean; + isLoading?: boolean; + 'data-testid'?: string; + }) => ( + + ) + ), + Dialog: Object.assign( + jest.fn( + ({ + children, + title, + onClose, + }: { + children: React.ReactNode; + title: string; + onClose: () => void; + }) => ( +
+ {title} + + {children} +
+ ) + ), + { + Content: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + } + ), + Input: jest.fn( + ({ + value, + onChange, + 'data-testid': testId, + placeholder, + }: { + value: string; + onChange: (v: string) => void; + 'data-testid'?: string; + placeholder?: string; + }) => ( + onChange(e.target.value)} + /> + ) + ), + Modal: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + ModalOverlay: jest.fn( + ({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) => + isOpen ?
{children}
: null + ), + Typography: jest.fn(({ children }: { children: React.ReactNode }) => ( + {children} + )), +})); + +const defaultProps = { + isOpen: true, + onClose: jest.fn(), + onCreated: jest.fn(), +}; + +describe('CreateFolderModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders when isOpen is true', () => { + render(); + + expect(screen.getByTestId('modal-overlay')).toBeInTheDocument(); + expect(screen.getByTestId('dialog')).toBeInTheDocument(); + }); + + it('does not render when isOpen is false', () => { + render(); + + expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument(); + }); + + it('renders the folder name input', () => { + render(); + + expect(screen.getByTestId('folder-name-input')).toBeInTheDocument(); + }); + + it('create button is disabled when input is empty', () => { + render(); + + expect(screen.getByTestId('create-folder-btn')).toBeDisabled(); + }); + + it('create button is disabled when input is only whitespace', () => { + render(); + + fireEvent.change(screen.getByTestId('folder-name-input'), { + target: { value: ' ' }, + }); + + expect(screen.getByTestId('create-folder-btn')).toBeDisabled(); + }); + + it('create button is enabled when input has a valid name', () => { + render(); + + fireEvent.change(screen.getByTestId('folder-name-input'), { + target: { value: 'My Folder' }, + }); + + expect(screen.getByTestId('create-folder-btn')).not.toBeDisabled(); + }); + + it('calls createFolder with trimmed name on submit', async () => { + const folder = { id: 'folder-1', name: 'my-folder', displayName: 'My Folder' }; + (createFolder as jest.Mock).mockResolvedValue(folder); + + render(); + + fireEvent.change(screen.getByTestId('folder-name-input'), { + target: { value: ' My Folder ' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('create-folder-btn')); + }); + + expect(createFolder).toHaveBeenCalledWith({ + name: 'My Folder', + displayName: 'My Folder', + }); + }); + + it('calls onCreated with the returned folder on success', async () => { + const folder = { id: 'folder-1', name: 'my-folder', displayName: 'My Folder' }; + (createFolder as jest.Mock).mockResolvedValue(folder); + + render(); + + fireEvent.change(screen.getByTestId('folder-name-input'), { + target: { value: 'My Folder' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('create-folder-btn')); + }); + + await waitFor(() => + expect(defaultProps.onCreated).toHaveBeenCalledWith(folder) + ); + }); + + it('calls onClose after successful creation', async () => { + const folder = { id: 'folder-1', name: 'my-folder' }; + (createFolder as jest.Mock).mockResolvedValue(folder); + + render(); + + fireEvent.change(screen.getByTestId('folder-name-input'), { + target: { value: 'My Folder' }, + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('create-folder-btn')); + }); + + await waitFor(() => expect(defaultProps.onClose).toHaveBeenCalled()); + }); + + it('resets the input after successful creation', async () => { + const folder = { id: 'folder-1', name: 'my-folder' }; + (createFolder as jest.Mock).mockResolvedValue(folder); + + render(); + + const input = screen.getByTestId('folder-name-input'); + fireEvent.change(input, { target: { value: 'My Folder' } }); + + await act(async () => { + fireEvent.click(screen.getByTestId('create-folder-btn')); + }); + + await waitFor(() => expect(input).toHaveValue('')); + }); + + it('does not call createFolder when name is empty', async () => { + render(); + + await act(async () => { + fireEvent.click(screen.getByTestId('create-folder-btn')); + }); + + expect(createFolder).not.toHaveBeenCalled(); + }); + + it('calls onClose when cancel button is clicked', () => { + render(); + + fireEvent.click(screen.getByText(/cancel/i)); + + expect(defaultProps.onClose).toHaveBeenCalled(); + }); + + it('calls onClose when dialog close button is clicked', () => { + render(); + + fireEvent.click(screen.getByTestId('dialog-close')); + + expect(defaultProps.onClose).toHaveBeenCalled(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx new file mode 100644 index 000000000000..f89e2c5250df --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx @@ -0,0 +1,241 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ButtonUtility, + Card, + Skeleton, + Tree, + Typography, +} from '@openmetadata/ui-core-components'; +import { Plus, Trash01 } from '@untitledui/icons'; +import { AxiosError } from 'axios'; +import { Folder } from 'generated/entity/data/folder'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; +import DeleteModal from '../../../components/common/DeleteModal/DeleteModal'; +import { deleteFolder, listFolders } from '../../../rest/assetAPI'; +import { FileTypeBadge } from '../../../utils/ContextCenterUtils'; +import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; +import CreateFolderModal from '../CreateFolderModal/CreateFolderModal.component'; +import { DocFile } from './DocumentsView.interface'; + +export interface DocumentFolderViewProps { + files?: DocFile[]; + selectedFolderId?: string; + onSelectFolder: (folderId: string | undefined) => void; + onFoldersLoaded?: (folders: Folder[]) => void; +} + +const DocumentFolderView = ({ + files = [], + selectedFolderId, + onSelectFolder, + onFoldersLoaded, +}: DocumentFolderViewProps) => { + const { t } = useTranslation(); + const [folders, setFolders] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [folderToDelete, setFolderToDelete] = useState(); + const [isDeletingFolder, setIsDeletingFolder] = useState(false); + + const fetchFolders = useCallback(async () => { + setIsLoading(true); + try { + const data = await listFolders(); + setFolders(data); + onFoldersLoaded?.(data); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsLoading(false); + } + }, [onFoldersLoaded]); + + useEffect(() => { + fetchFolders(); + }, [fetchFolders]); + + const handleFolderCreated = (folder: Folder) => { + const updated = [...folders, folder]; + setFolders(updated); + onFoldersLoaded?.(updated); + }; + + const handleDeleteConfirm = async () => { + if (!folderToDelete) { + return; + } + + try { + setIsDeletingFolder(true); + await deleteFolder(folderToDelete.id); + const updated = folders.filter((f) => f.id !== folderToDelete.id); + setFolders(updated); + onFoldersLoaded?.(updated); + if (selectedFolderId === folderToDelete.id) { + onSelectFolder(undefined); + } + showSuccessToast( + t('server.entity-deleted-successfully', { entity: t('label.folder') }) + ); + setFolderToDelete(undefined); + } catch (err) { + const axiosErr = err as AxiosError<{ message?: string }>; + const message = axiosErr?.response?.data?.message ?? ''; + showErrorToast(message); + } finally { + setFolderToDelete(undefined); + setIsDeletingFolder(false); + } + }; + + const handleFolderItemSelect = (folderId: string) => { + onSelectFolder(selectedFolderId === folderId ? undefined : folderId); + }; + + return ( + <> + +
+
+
+ +
+
+ + {t('label.folder')} + + + {folders.length} {t('label.folder-plural')} + {files.length} {t('label.file-plural')} + +
+
+ setIsCreateModalOpen(true)} + /> +
+ +
+ {isLoading ? ( +
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ ) : ( + + {folders.map((folder) => { + const isSelected = selectedFolderId === folder.id; + const folderFiles = files.filter( + (f) => f.folderId === folder.id + ); + + return ( + + +
+ + + { + e.stopPropagation(); + setFolderToDelete(folder); + }} + /> +
+
+ + {folderFiles.map((file) => ( + + + + + {file.name} + + + + ))} +
+ ); + })} +
+ )} +
+
+ + setIsCreateModalOpen(false)} + onCreated={handleFolderCreated} + /> + + {folderToDelete && ( + setFolderToDelete(undefined)} + onDelete={handleDeleteConfirm} + /> + )} + + ); +}; + +export default DocumentFolderView; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx new file mode 100644 index 000000000000..4836fe2db21d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx @@ -0,0 +1,405 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { deleteFolder, listFolders } from 'rest/assetAPI'; +import DocumentFolderView from './DocumentFolderView.component'; +import { DocFile } from './DocumentsView.interface'; + +jest.mock('rest/assetAPI', () => ({ + listFolders: jest.fn(), + deleteFolder: jest.fn(), +})); + +jest.mock('../CreateFolderModal/CreateFolderModal.component', () => + jest.fn( + ({ + isOpen, + onCreated, + onClose, + }: { + isOpen: boolean; + onCreated: (f: unknown) => void; + onClose: () => void; + }) => + isOpen ? ( +
+ + +
+ ) : null + ) +); + +jest.mock('../../../components/common/DeleteModal/DeleteModal', () => + jest.fn( + ({ + open, + onDelete, + onCancel, + }: { + open: boolean; + onDelete: () => void; + onCancel: () => void; + }) => + open ? ( +
+ + +
+ ) : null + ) +); + +jest.mock('utils/ContextCenterUtils', () => ({ + FileTypeBadge: jest.fn(({ fileType }: { fileType: string }) => ( + {fileType} + )), +})); + +jest.mock('@openmetadata/ui-core-components', () => ({ + ButtonUtility: jest.fn( + ({ + onClick, + 'data-testid': testId, + tooltip, + }: { + onClick?: (e: React.MouseEvent) => void; + 'data-testid'?: string; + tooltip?: string; + }) => ( + + ) + ), + Card: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + Skeleton: jest.fn(() =>
), + Tree: Object.assign( + jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + { + Item: jest.fn( + ({ + children, + id, + }: { + children: React.ReactNode; + id: string; + }) =>
{children}
+ ), + ItemContent: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + } + ), + Typography: jest.fn(({ children }: { children: React.ReactNode }) => ( + {children} + )), +})); + +const mockFolders = [ + { id: 'folder-1', name: 'folder-1', displayName: 'Folder One' }, + { id: 'folder-2', name: 'folder-2', displayName: 'Folder Two' }, +]; + +const mockFiles: DocFile[] = [ + { + id: 'file-1', + name: 'report.pdf', + fileType: 'pdf', + sizeLabel: '1 MB', + folderId: 'folder-1', + }, + { + id: 'file-2', + name: 'data.csv', + fileType: 'other', + sizeLabel: '200 KB', + folderId: 'folder-2', + }, +]; + +describe('DocumentFolderView', () => { + beforeEach(() => { + jest.clearAllMocks(); + (listFolders as jest.Mock).mockResolvedValue(mockFolders); + }); + + it('shows skeletons while loading', () => { + (listFolders as jest.Mock).mockReturnValue(new Promise(() => undefined)); + render( + + ); + + expect(screen.getAllByTestId('skeleton').length).toBeGreaterThan(0); + }); + + it('renders folder names after loading', async () => { + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + expect(screen.getByText('Folder Two')).toBeInTheDocument(); + }); + + it('calls onFoldersLoaded with fetched folders', async () => { + const onFoldersLoaded = jest.fn(); + render( + + ); + + await waitFor(() => + expect(onFoldersLoaded).toHaveBeenCalledWith(mockFolders) + ); + }); + + it('renders files as children under their parent folder', async () => { + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + expect(screen.getByText('report.pdf')).toBeInTheDocument(); + expect(screen.getByText('data.csv')).toBeInTheDocument(); + }); + + it('shows folder and file counts in the subtitle', async () => { + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + expect(screen.getByText(/2/)).toBeInTheDocument(); + }); + + it('calls onSelectFolder with folderId when a folder is clicked', async () => { + const onSelectFolder = jest.fn(); + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByText('Folder One')); + + expect(onSelectFolder).toHaveBeenCalledWith('folder-1'); + }); + + it('calls onSelectFolder with undefined when the selected folder is clicked again', async () => { + const onSelectFolder = jest.fn(); + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByText('Folder One')); + + expect(onSelectFolder).toHaveBeenCalledWith(undefined); + }); + + it('opens the create folder modal when the add button is clicked', async () => { + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByTestId('add-folder-btn')); + + expect(screen.getByTestId('create-folder-modal')).toBeInTheDocument(); + }); + + it('adds the new folder to the list after creation', async () => { + const onFoldersLoaded = jest.fn(); + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByTestId('add-folder-btn')); + fireEvent.click(screen.getByTestId('modal-create-btn')); + + await waitFor(() => + expect(screen.getByText('New Folder')).toBeInTheDocument() + ); + + expect(onFoldersLoaded).toHaveBeenLastCalledWith( + expect.arrayContaining([ + expect.objectContaining({ displayName: 'New Folder' }), + ]) + ); + }); + + it('shows delete modal when delete button is clicked', async () => { + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + const deleteBtn = screen.getByTestId('delete-folder-btn-folder-1'); + fireEvent.click(deleteBtn); + + expect(screen.getByTestId('delete-modal')).toBeInTheDocument(); + }); + + it('calls deleteFolder and removes the folder on confirm', async () => { + (deleteFolder as jest.Mock).mockResolvedValue(undefined); + const onFoldersLoaded = jest.fn(); + + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByTestId('delete-folder-btn-folder-1')); + + await act(async () => { + fireEvent.click(screen.getByTestId('confirm-delete-btn')); + }); + + await waitFor(() => + expect(deleteFolder).toHaveBeenCalledWith('folder-1') + ); + await waitFor(() => + expect(screen.queryByText('Folder One')).not.toBeInTheDocument() + ); + }); + + it('calls onSelectFolder(undefined) when the selected folder is deleted', async () => { + (deleteFolder as jest.Mock).mockResolvedValue(undefined); + const onSelectFolder = jest.fn(); + + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByTestId('delete-folder-btn-folder-1')); + + await act(async () => { + fireEvent.click(screen.getByTestId('confirm-delete-btn')); + }); + + await waitFor(() => + expect(onSelectFolder).toHaveBeenCalledWith(undefined) + ); + }); + + it('closes delete modal on cancel without deleting', async () => { + render( + + ); + + await waitFor(() => + expect(screen.getByText('Folder One')).toBeInTheDocument() + ); + + fireEvent.click(screen.getByTestId('delete-folder-btn-folder-1')); + fireEvent.click(screen.getByTestId('cancel-delete-btn')); + + expect(deleteFolder).not.toHaveBeenCalled(); + expect(screen.queryByTestId('delete-modal')).not.toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 8b1ddf65ff4a..41f905ed0470 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -23,40 +23,25 @@ import { import { Download01, Share06, Trash01 } from '@untitledui/icons'; import { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { FileTypeBadge } from 'utils/ContextCenterUtils'; import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; -import { FILE_TYPE_STYLES } from '../../../constants/ContextCenter.constants'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { getShortRelativeTime } from '../../../utils/date-time/DateTimeUtils'; -import { - DocFile, - DocFileType, - DocumentsViewProps, -} from './DocumentsView.interface'; - -const FileTypeBadge: FC<{ fileType: DocFileType }> = ({ fileType }) => { - const { bg, label, text } = FILE_TYPE_STYLES[fileType || 'other']; - - return ( - - {label} - - ); -}; - -// ─── Actions dropdown ───────────────────────────────────────────────────────── +import { DocFile, DocumentsViewProps } from './DocumentsView.interface'; interface FileActionsProps { canDelete?: boolean; file: DocFile; onShareFile?: (file: DocFile) => void; onDeleteFile?: (file: DocFile) => void; + onMoveFile?: (file: DocFile) => void; } const FileActions: FC = ({ canDelete, file, onDeleteFile, + onMoveFile, onShareFile, }) => { const { t } = useTranslation(); @@ -74,6 +59,8 @@ const FileActions: FC = ({ onAction={(key) => { if (key === 'share') { onShareFile?.(file); + } else if (key === 'move') { + onMoveFile?.(file); } else if (key === 'delete') { onDeleteFile?.(file); } @@ -84,6 +71,12 @@ const FileActions: FC = ({ id="share" label={t('label.share-file')} /> + {canDelete && (
@@ -109,26 +102,15 @@ const FileActions: FC = ({ const FileRowSkeleton: FC = () => (
- {/* File type badge */} - - - {/* File details */} +
-
- - {/* Actions */}
@@ -136,14 +118,13 @@ const FileRowSkeleton: FC = () => (
); -// ─── File row ───────────────────────────────────────────────────────────────── - interface FileRowProps { canDelete?: boolean; file: DocFile; onDownload?: (file: DocFile) => void; onShareFile?: (file: DocFile) => void; onDeleteFile?: (file: DocFile) => void; + onMoveFile?: (file: DocFile) => void; } const FileRow: FC = ({ @@ -151,6 +132,7 @@ const FileRow: FC = ({ file, onDeleteFile, onDownload, + onMoveFile, onShareFile, }) => { const { t } = useTranslation(); @@ -212,6 +194,7 @@ const FileRow: FC = ({ canDelete={canDelete} file={file} onDeleteFile={onDeleteFile} + onMoveFile={onMoveFile} onShareFile={onShareFile} />
@@ -222,21 +205,19 @@ const FileRow: FC = ({ const DocumentViewLoading = () => Array.from({ length: 8 }).map((_, idx) => ); -// ─── Main component ─────────────────────────────────────────────────────────── - const DocumentsView: FC = ({ canDelete, data, isLoading, onDeleteFile, onDownload, + onMoveFile, onShareFile, }) => { return ( - {/* Right: file list */} {data.length > 0 || isLoading ? (
{isLoading ? ( @@ -249,6 +230,7 @@ const DocumentsView: FC = ({ key={file.id} onDeleteFile={onDeleteFile} onDownload={onDownload} + onMoveFile={onMoveFile} onShareFile={onShareFile} /> )) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts index 989da28b9e22..e548f854908b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts @@ -15,12 +15,14 @@ export type DocFileType = 'pdf' | 'xls' | 'csv' | 'doc' | 'image' | 'other'; export interface DocFile { id: string; + driveFileId?: string; name: string; fileType: DocFileType; sizeLabel: string; updatedBy?: string; updatedAt?: number; folderId?: string; + folderFqn?: string; } export interface DocFolder { @@ -36,4 +38,5 @@ export interface DocumentsViewProps { onDownload?: (file: DocFile) => void; onShareFile?: (file: DocFile) => void; onDeleteFile?: (file: DocFile) => void; + onMoveFile?: (file: DocFile) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx new file mode 100644 index 000000000000..ddff6ce17673 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx @@ -0,0 +1,162 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Button, + Dialog, + Modal, + ModalOverlay, + Select, + Typography, +} from '@openmetadata/ui-core-components'; +import { ChevronRight } from '@untitledui/icons'; +import { AxiosError } from 'axios'; +import { Folder } from 'generated/entity/data/folder'; +import { FC, Key, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FileTypeBadge } from 'utils/ContextCenterUtils'; +import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; +import { moveFileToFolder } from '../../../rest/assetAPI'; +import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; +import { DocFile } from '../DocumentsView/DocumentsView.interface'; + +export interface MoveToFolderModalProps { + file: DocFile; + folders: Folder[]; + isOpen: boolean; + onClose: () => void; + onMoved: (file: DocFile, targetFolderId: string) => void; +} + +const MoveToFolderModal: FC = ({ + file, + folders, + isOpen, + onClose, + onMoved, +}) => { + const { t } = useTranslation(); + const [selectedFolderId, setSelectedFolderId] = useState(''); + const [isMoving, setIsMoving] = useState(false); + + const currentFolderName = file.folderId + ? (folders.find((f) => f.id === file.folderId)?.displayName ?? + folders.find((f) => f.id === file.folderId)?.name) + : undefined; + + const availableFolders = folders.filter((f) => f.id !== file.folderId); + + const handleClose = () => { + setSelectedFolderId(''); + onClose(); + }; + + const handleSave = async () => { + if (!selectedFolderId) { + return; + } + + try { + setIsMoving(true); + await moveFileToFolder(file.driveFileId ?? file.id, selectedFolderId); + onMoved(file, selectedFolderId); + showSuccessToast( + t('message.entity-moved-successfully', { entity: t('label.document') }) + ); + handleClose(); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsMoving(false); + } + }; + + return ( + !open && !isMoving && handleClose()}> + + + +
+ + + {file.name} + +
+ +
+ + + {currentFolderName ?? t('label.no-folder')} + + +
+ +
+
+ +
+ + +
+
+
+
+
+ ); +}; + +export default MoveToFolderModal; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx new file mode 100644 index 000000000000..1ed2965a976e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx @@ -0,0 +1,300 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { moveFileToFolder } from 'rest/assetAPI'; +import { DocFile } from '../DocumentsView/DocumentsView.interface'; +import MoveToFolderModal from './MoveToFolderModal.component'; + +jest.mock('rest/assetAPI', () => ({ + moveFileToFolder: jest.fn(), +})); + +let mockOnSelectionChange: ((key: React.Key) => void) | undefined; + +jest.mock('@openmetadata/ui-core-components', () => ({ + Button: jest.fn( + ({ + children, + onPress, + isDisabled, + 'data-testid': testId, + }: { + children: React.ReactNode; + onPress?: () => void; + isDisabled?: boolean; + 'data-testid'?: string; + }) => ( + + ) + ), + Dialog: Object.assign( + jest.fn( + ({ + children, + title, + onClose, + }: { + children: React.ReactNode; + title: string; + onClose: () => void; + }) => ( +
+ {title} + + {children} +
+ ) + ), + { + Content: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + } + ), + Modal: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + ModalOverlay: jest.fn( + ({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) => + isOpen ?
{children}
: null + ), + Select: Object.assign( + jest.fn( + ({ + children, + onSelectionChange, + 'data-testid': testId, + }: { + children: React.ReactNode; + onSelectionChange?: (key: React.Key) => void; + 'data-testid'?: string; + }) => { + mockOnSelectionChange = onSelectionChange; + + return ( +
{children}
+ ); + } + ), + { + Item: jest.fn(({ id, label }: { id: string; label: string }) => ( + + )), + } + ), + Typography: jest.fn(({ children }: { children: React.ReactNode }) => ( + {children} + )), +})); + +jest.mock('utils/ContextCenterUtils', () => ({ + FileTypeBadge: jest.fn(({ fileType }: { fileType: string }) => ( + {fileType} + )), +})); + +const mockFile: DocFile = { + id: 'asset-1', + driveFileId: 'drive-1', + name: 'report.pdf', + fileType: 'pdf', + sizeLabel: '2 MB', + folderId: 'folder-a', +}; + +const mockFolders = [ + { id: 'folder-a', name: 'folder-a', displayName: 'Folder A' }, + { id: 'folder-b', name: 'folder-b', displayName: 'Folder B' }, + { id: 'folder-c', name: 'folder-c', displayName: 'Folder C' }, +]; + +const defaultProps = { + file: mockFile, + folders: mockFolders, + isOpen: true, + onClose: jest.fn(), + onMoved: jest.fn(), +}; + +describe('MoveToFolderModal', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockOnSelectionChange = undefined; + }); + + it('renders when isOpen is true', () => { + render(); + + expect(screen.getByTestId('modal-overlay')).toBeInTheDocument(); + expect(screen.getByTestId('dialog')).toBeInTheDocument(); + }); + + it('does not render when isOpen is false', () => { + render(); + + expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument(); + }); + + it('renders the file name', () => { + render(); + + expect(screen.getByText('report.pdf')).toBeInTheDocument(); + }); + + it('renders the current folder name', () => { + render(); + + expect(screen.getByText('Folder A')).toBeInTheDocument(); + }); + + it('shows "no-folder" when file has no folderId', () => { + render( + + ); + + expect(screen.getByText(/no-folder/i)).toBeInTheDocument(); + }); + + it('excludes the current folder from select options', () => { + render(); + + expect(screen.queryByTestId('select-item-folder-a')).not.toBeInTheDocument(); + expect(screen.getByTestId('select-item-folder-b')).toBeInTheDocument(); + expect(screen.getByTestId('select-item-folder-c')).toBeInTheDocument(); + }); + + it('save button is disabled when no folder is selected', () => { + render(); + + expect(screen.getByTestId('move-folder-save-btn')).toBeDisabled(); + }); + + it('save button is enabled after selecting a folder', () => { + render(); + + act(() => { + mockOnSelectionChange?.('folder-b'); + }); + + expect(screen.getByTestId('move-folder-save-btn')).not.toBeDisabled(); + }); + + it('calls moveFileToFolder with driveFileId and selected folder on save', async () => { + (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); + + render(); + + act(() => { + mockOnSelectionChange?.('folder-b'); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('move-folder-save-btn')); + }); + + expect(moveFileToFolder).toHaveBeenCalledWith('drive-1', 'folder-b'); + }); + + it('falls back to file.id when driveFileId is undefined', async () => { + (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); + + render( + + ); + + act(() => { + mockOnSelectionChange?.('folder-b'); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('move-folder-save-btn')); + }); + + expect(moveFileToFolder).toHaveBeenCalledWith('asset-1', 'folder-b'); + }); + + it('calls onMoved with file and targetFolderId on success', async () => { + (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); + + render(); + + act(() => { + mockOnSelectionChange?.('folder-b'); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('move-folder-save-btn')); + }); + + await waitFor(() => + expect(defaultProps.onMoved).toHaveBeenCalledWith(mockFile, 'folder-b') + ); + }); + + it('calls onClose after successful move', async () => { + (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); + + render(); + + act(() => { + mockOnSelectionChange?.('folder-b'); + }); + + await act(async () => { + fireEvent.click(screen.getByTestId('move-folder-save-btn')); + }); + + await waitFor(() => expect(defaultProps.onClose).toHaveBeenCalled()); + }); + + it('calls onClose when cancel button is clicked', () => { + render(); + + fireEvent.click(screen.getByText(/cancel/i)); + + expect(defaultProps.onClose).toHaveBeenCalled(); + }); + + it('calls onClose when dialog close button is clicked', () => { + render(); + + fireEvent.click(screen.getByTestId('dialog-close')); + + expect(defaultProps.onClose).toHaveBeenCalled(); + }); + + it('does not call moveFileToFolder when no folder selected', async () => { + render(); + + await act(async () => { + fireEvent.click(screen.getByTestId('move-folder-save-btn')); + }); + + expect(moveFileToFolder).not.toHaveBeenCalled(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx index 64695611c328..7ba6ef5b465e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component.tsx @@ -25,8 +25,8 @@ import { AlertCircle, Trash01, UploadCloud02 } from '@untitledui/icons'; import { FC, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DOCUMENT_MAX_FILE_SIZE } from '../../../constants/ContextCenter.constants'; -import { Asset } from '../../../generated/attachments/asset'; -import { uploadAsset } from '../../../rest/assetAPI'; +import { ContextFile } from '../../../generated/entity/data/contextFile'; +import { uploadDriveFile } from '../../../rest/assetAPI'; import { QueuedFile, StagedFile, @@ -35,7 +35,7 @@ import { const UploadDocumentModal: FC = ({ isOpen, - entityLink, + folderFqn, onClose, onUploaded, }) => { @@ -44,14 +44,14 @@ const UploadDocumentModal: FC = ({ const [queuedFiles, setQueuedFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [sizeError, setSizeError] = useState(''); - const uploadedAssetsRef = useRef([]); + const uploadedFilesRef = useRef([]); const cancelledRef = useRef(false); const hasStartedUploading = queuedFiles.length > 0; const handleClose = () => { cancelledRef.current = true; - uploadedAssetsRef.current = []; + uploadedFilesRef.current = []; setStagedFiles([]); setQueuedFiles([]); setIsUploading(false); @@ -59,7 +59,9 @@ const UploadDocumentModal: FC = ({ onClose(); }; - const uploadSingleFile = async (entry: QueuedFile): Promise => { + const uploadSingleFile = async ( + entry: QueuedFile + ): Promise => { setQueuedFiles((prev) => prev.map((f) => f.id === entry.id ? { ...f, progress: 0, status: 'uploading' } : f @@ -67,14 +69,14 @@ const UploadDocumentModal: FC = ({ ); try { - const asset = await uploadAsset(entry.file, entityLink); + const contextFile = await uploadDriveFile(entry.file, folderFqn); setQueuedFiles((prev) => prev.map((f) => f.id === entry.id ? { ...f, progress: 100, status: 'done' } : f ) ); - return asset; + return contextFile; } catch { setQueuedFiles((prev) => prev.map((f) => @@ -115,12 +117,12 @@ const UploadDocumentModal: FC = ({ } setIsUploading(true); - const asset = await uploadSingleFile(entry); + const contextFile = await uploadSingleFile(entry); setIsUploading(false); - if (asset) { - uploadedAssetsRef.current = [...uploadedAssetsRef.current, asset]; - onUploaded?.([asset]); + if (contextFile) { + uploadedFilesRef.current = [...uploadedFilesRef.current, contextFile]; + onUploaded?.([contextFile]); } }; @@ -142,23 +144,23 @@ const UploadDocumentModal: FC = ({ setQueuedFiles((prev) => [...prev, ...newQueued]); setIsUploading(true); - const batchAssets: Asset[] = []; + const batchFiles: ContextFile[] = []; for (const entry of newQueued) { if (cancelledRef.current) { break; } - const asset = await uploadSingleFile(entry); - if (asset) { - batchAssets.push(asset); + const contextFile = await uploadSingleFile(entry); + if (contextFile) { + batchFiles.push(contextFile); } } if (!cancelledRef.current) { setIsUploading(false); - if (batchAssets.length > 0) { - onUploaded?.(batchAssets); + if (batchFiles.length > 0) { + onUploaded?.(batchFiles); } } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.interface.ts index 40e2d8b28176..1f0e7e7230bb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.interface.ts @@ -11,13 +11,13 @@ * limitations under the License. */ -import { Asset } from '../../../generated/attachments/asset'; +import { ContextFile } from '../../../generated/entity/data/contextFile'; export interface UploadDocumentModalProps { isOpen: boolean; - entityLink: string; + folderFqn?: string; onClose: () => void; - onUploaded?: (assets: Asset[]) => void; + onUploaded?: (files: ContextFile[]) => void; } export type UploadStatus = 'uploading' | 'done' | 'error'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx index a6e5f9b4f686..cf1a6b0eb7f5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx @@ -64,6 +64,7 @@ import { QuickLinkFormModalFormData, } from '../QuickLinkFormModal/QuickLinkFormModal'; +import { getEntityName } from 'utils/EntityUtils'; import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore'; import { deleteKnowledgePage } from '../../../rest/knowledgeCenterAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; @@ -336,7 +337,7 @@ const KnowledgeCard: FC = ({ className="m-b-0 d-block entity-header-display-name text-lg font-semibold cursor-pointer knowledge-card-title text-primary" data-testid="entity-header-display-name" ellipsis={{ tooltip: true }}> - {knowledgePage?.displayName || t('label.untitled')} + {getEntityName(knowledgePage) || t('label.untitled')} {isQuickLink && !readonly && quickLinkActions}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx index a57fd57c2adc..fe175428de1a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx @@ -24,6 +24,7 @@ import { useState, } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import { TagClassBase } from 'utils/TagClassBase'; import { useActivityFeedProvider } from '../../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { ActivityFeedLayoutType } from '../../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; @@ -38,6 +39,7 @@ import { QueryVoteType } from '../../../components/Database/TableQueries/TableQu import { VotingDataProps } from '../../../components/Entity/Voting/voting.interface'; import { CREATE_PAGE_HASH, + KNOWLEDGE_CENTER_CLASSIFICATION, LONG_DELAY, SHORT_DELAY, } from '../../../constants/constants'; @@ -695,6 +697,14 @@ const KnowledgePageDetailComponent: FC = ({ [tabs, activeTab] ); + useEffect(() => { + TagClassBase.filterClassification = []; + + return () => { + TagClassBase.filterClassification = [KNOWLEDGE_CENTER_CLASSIFICATION]; + }; + }, []); + const pageConfig = useMemo(() => { let rightPanel = null; if ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx index cae1229a121b..a0b54e17e9e4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx @@ -59,6 +59,7 @@ import { KnowledgePage, PageType, } from '../../../interface/knowledge-center.interface'; +import { SearchIndex } from '../../../enums/search.enum'; import { followKnowledgePage, getListKnowledgePages, @@ -66,6 +67,7 @@ import { unFollowKnowledgePage, updateKnowledgePageVote, } from '../../../rest/knowledgeCenterAPI'; +import { searchQuery as fetchSearchResults } from '../../../rest/searchAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; import { Transi18next } from '../../../utils/i18next/LocalUtil'; import { showErrorToast } from '../../../utils/ToastUtils'; @@ -82,6 +84,7 @@ interface KnowledgePageListComponentProps { permissions: OperationPermission; hideAddButton?: boolean; rightPanelSlot?: React.ReactNode; + searchQuery?: string; } const KnowledgePageListComponent = forwardRef< @@ -89,7 +92,13 @@ const KnowledgePageListComponent = forwardRef< KnowledgePageListComponentProps >( ( - { onPageChange, permissions, hideAddButton = false, rightPanelSlot }, + { + onPageChange, + permissions, + hideAddButton = false, + rightPanelSlot, + searchQuery, + }, ref ) => { const { currentUser, theme } = useApplicationStore(); @@ -101,6 +110,7 @@ const KnowledgePageListComponent = forwardRef< const [isLoadingMore, setIsLoadingMore] = useState(false); const [knowledgePages, setKnowledgePages] = useState([]); const [paging, setPaging] = useState({ total: 0 }); + const [pageOffset, setPageOffset] = useState(0); const [isCreatingNewPage, setIsCreatingNewPage] = useState(false); const [showAddLinkModal, setShowAddLinkModal] = useState(false); const { getResourceLimit } = useLimitStore(); @@ -116,22 +126,42 @@ const KnowledgePageListComponent = forwardRef< const handleRefreshTagsCategory = (value: boolean) => setRefreshTagsCategory(value); - const fetchKnowledgePages = async (after?: string) => { - if (after) { + const fetchKnowledgePages = async (offset = 0) => { + if (offset > 0) { setIsLoadingMore(true); } else { setIsLoading(true); } try { - const { data, paging: pagingObj } = await getListKnowledgePages({ - fields: getKnowledgePageFields(), - after, - limit: PAGE_SIZE_MEDIUM, - }); - setKnowledgePages((prev) => - uniqBy(after ? [...prev, ...data] : data, 'id') - ); - setPaging(pagingObj); + if (searchQuery) { + const results = await fetchSearchResults({ + query: searchQuery, + searchIndex: SearchIndex.KNOWLEDGE_PAGE_INDEX, + queryFilter: { + query: { term: { pageType: PageType.ARTICLE } }, + }, + sortField: 'updatedAt', + sortOrder: 'desc', + pageSize: PAGE_SIZE_MEDIUM, + }); + setKnowledgePages( + results.hits.hits.map((hit) => hit._source as KnowledgePage) + ); + setPaging({ total: results.hits.total.value }); + } else { + const { data, paging: pagingObj } = await getListKnowledgePages({ + fields: getKnowledgePageFields(), + limit: PAGE_SIZE_MEDIUM, + offset, + pageType: PageType.ARTICLE, + sortBy: 'updatedAt', + sortOrder: 'desc', + }); + setKnowledgePages((prev) => + uniqBy(offset > 0 ? [...prev, ...data] : data, 'id') + ); + setPaging(pagingObj); + } } catch (error) { showErrorToast(error as AxiosError); } finally { @@ -299,21 +329,21 @@ const KnowledgePageListComponent = forwardRef< useEffect(() => { if (hasViewPermission) { - fetchKnowledgePages(); + setPageOffset(0); + fetchKnowledgePages(0); } else { setIsLoading(false); } - }, [hasViewPermission]); + }, [hasViewPermission, searchQuery]); - /** - * Handle infinite scrolling - */ useEffect(() => { - const after = paging.after; - if (isInView && after && !isLoadingMore && hasViewPermission) { - fetchKnowledgePages(after); + const hasMore = knowledgePages.length < paging.total; + if (isInView && hasMore && !isLoadingMore && !searchQuery && hasViewPermission) { + const nextOffset = pageOffset + PAGE_SIZE_MEDIUM; + setPageOffset(nextOffset); + fetchKnowledgePages(nextOffset); } - }, [isInView, paging, isLoadingMore, hasViewPermission]); + }, [isInView, paging.total, knowledgePages.length, isLoadingMore, searchQuery, hasViewPermission]); const items: MenuProps['items'] = [ { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx index 4b00a5f7cb9b..8ff2efeda505 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx @@ -11,11 +11,11 @@ * limitations under the License. */ import { + Typography as AntTypography, Button, Modal, Skeleton, Tree, - Typography as AntTypography, } from 'antd'; import { DataNode } from 'antd/es/tree'; import { AntTreeNodeProps, DirectoryTreeProps, TreeProps } from 'antd/lib/tree'; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts index 18c6545fd221..4dc7d131ba92 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/search.enum.ts @@ -57,5 +57,6 @@ export enum SearchIndex { SPREADSHEET = 'spreadsheet', WORKSHEET = 'worksheet', KNOWLEDGE_PAGE_INDEX = 'page', + DRIVE_FILE = 'contextFile', MARKETPLACE = 'marketplace', } diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts index d06ac1d3c6f4..a32f84df2d9c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts @@ -62,6 +62,8 @@ import { TestCaseResolutionStatus } from '../generated/tests/testCaseResolutionS import { TestSuite } from '../generated/tests/testSuite'; import { TagLabel } from '../generated/type/tagLabel'; import { AggregatedCostAnalysisReportDataSearchSource } from './data-insight.interface'; +import { Asset } from '../generated/attachments/asset'; +import { ContextFile } from '../generated/entity/data/contextFile'; import { KnowledgePage } from './knowledge-center.interface'; /** @@ -243,6 +245,8 @@ export interface KnowledgePageSearchSource extends SearchSourceBase, KnowledgePage {} +export interface DriveFileSearchSource extends SearchSourceBase, ContextFile {} + export type ExploreSearchSource = | TableSearchSource | DashboardSearchSource @@ -323,6 +327,7 @@ export type SearchIndexSearchSourceMapping = { [SearchIndex.WORKSHEET]: WorksheetSearchSource; [SearchIndex.COLUMN]: TableColumnSearchSource; [SearchIndex.KNOWLEDGE_PAGE_INDEX]: KnowledgePageSearchSource; + [SearchIndex.DRIVE_FILE]: DriveFileSearchSource; [SearchIndex.MARKETPLACE]: DataProductSearchSource | DomainSearchSource; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json index 60bcad4dc33d..0e0a8966fad4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json @@ -931,6 +931,8 @@ "focus-home": "التركيز على الصفحة الرئيسية", "focus-on-node": "التركيز على العقدة", "focus-selected": "التركيز على المحدد", + "folder": "مجلد", + "folder-plural": "مجلدات", "follow": "متابعة", "followed-lowercase": "تمت المتابعة", "follower-plural": "متابعون", @@ -1371,6 +1373,7 @@ "move": "نقل", "move-anyway": "نقل على أي حال", "move-the-entity": "نقل {{entity}}", + "move-to-folder": "نقل إلى المجلد", "ms": "مللي ثانية", "ms-team-plural": "فرق MS Teams", "multi-select": "تحديد متعدد", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "لا يوجد {{entity}}", "no-entity-available": "لا يتوفر {{entity}}", "no-entity-selected": "لم يتم تحديد {{entity}}", + "no-folder": "لا يوجد مجلد", "no-knowledge-articles-available": "لا توجد مقالات معرفة متاحة", "no-kpis-yet": "ابدأ بتتبع ما يهم", "no-matching-data-asset": "لم يتم العثور على أصول بيانات مطابقة", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 65367f362c90..7be1e49c89e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -931,6 +931,8 @@ "focus-home": "Zur Startansicht", "focus-on-node": "Auf Knoten fokussieren", "focus-selected": "Auswahl fokussieren", + "folder": "Ordner", + "folder-plural": "Ordner", "follow": "Folgen", "followed-lowercase": "gefolgt", "follower-plural": "Follower", @@ -1371,6 +1373,7 @@ "move": "Bewegen", "move-anyway": "Trotzdem verschieben", "move-the-entity": "{{entity}} verschieben", + "move-to-folder": "In Ordner verschieben", "ms": "Millisekunden", "ms-team-plural": "MS-Teams", "multi-select": "Mehrfachauswahl", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "Kein {{entity}} zugewiesen", "no-entity-available": "Keine {{entity}} sind verfügbar", "no-entity-selected": "Keine {{entity}} ausgewählt", + "no-folder": "Kein Ordner", "no-knowledge-articles-available": "Keine Wissensartikel verfügbar", "no-kpis-yet": "Beginnen Sie damit, das Wichtige zu verfolgen", "no-matching-data-asset": "Keine passenden Datenanlagen gefunden", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 64ea4019d6a9..2f0763586d3b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -931,6 +931,8 @@ "focus-home": "Focus Home", "focus-on-node": "Focus on Node", "focus-selected": "Focus Selected", + "folder": "Folder", + "folder-plural": "Folders", "follow": "Follow", "followed-lowercase": "followed", "follower-plural": "Followers", @@ -1371,6 +1373,7 @@ "move": "Move", "move-anyway": "Move Anyway", "move-the-entity": "Move the {{entity}}", + "move-to-folder": "Move to Folder", "ms": "Milliseconds", "ms-team-plural": "MS Teams", "multi-select": "Multi Select", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "No {{entity}} assigned", "no-entity-available": "No {{entity}} are available", "no-entity-selected": "No {{entity}} Selected", + "no-folder": "No Folder", "no-knowledge-articles-available": "No Knowledge Articles Available", "no-kpis-yet": "Start Tracking What Matters", "no-matching-data-asset": "No matching data assets found", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 4663bccc192d..3f3ebaca028b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -931,6 +931,8 @@ "focus-home": "Ir al Inicio", "focus-on-node": "Enfocar en Nodo", "focus-selected": "Enfocar Selección", + "folder": "Carpeta", + "folder-plural": "Carpetas", "follow": "Seguir", "followed-lowercase": "seguido", "follower-plural": "Seguidores", @@ -1371,6 +1373,7 @@ "move": "Mover", "move-anyway": "Mover de todos modos", "move-the-entity": "Mover la {{entity}}", + "move-to-folder": "Mover a la carpeta", "ms": "Milisegundos", "ms-team-plural": "Equipos de MS", "multi-select": "Selector Múltiple", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "No {{entity}} asignado", "no-entity-available": "No hay {{entity}} disponibles", "no-entity-selected": "No se ha seleccionado {{entity}}", + "no-folder": "Sin carpeta", "no-knowledge-articles-available": "No hay artículos de conocimiento disponibles", "no-kpis-yet": "Empieza a rastrear lo que importa", "no-matching-data-asset": "No se encontraron activos de datos coincidentes", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 86cc8257a028..9521ce41a8d9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -931,6 +931,8 @@ "focus-home": "Retour au Centre", "focus-on-node": "Centrer sur le Noeud", "focus-selected": "Centrer la Sélection", + "folder": "Dossier", + "folder-plural": "Dossiers", "follow": "Suivre", "followed-lowercase": "suivi·e", "follower-plural": "Suiveurs", @@ -1371,6 +1373,7 @@ "move": "Déplacer", "move-anyway": "Déplacer quand même", "move-the-entity": "Déplacer {{entity}}", + "move-to-folder": "Déplacer vers le dossier", "ms": "Millisecondes", "ms-team-plural": "Équipes MS", "multi-select": "Sélection multiple", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "Aucun {{entity}} attribué", "no-entity-available": "Aucun {{entity}} disponible", "no-entity-selected": "Aucun {{entity}} Sélectionné", + "no-folder": "Pas de dossier", "no-knowledge-articles-available": "Aucun article de connaissances disponible", "no-kpis-yet": "Commencez à suivre ce qui compte", "no-matching-data-asset": "Aucun actif de données trouvé", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index 0a629e07c867..3f2e22223aeb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -931,6 +931,8 @@ "focus-home": "Ir ao Inicio", "focus-on-node": "Enfocar no Nodo", "focus-selected": "Enfocar Selección", + "folder": "Cartafol", + "folder-plural": "Cartafoles", "follow": "Seguir", "followed-lowercase": "seguido", "follower-plural": "Seguidores", @@ -1371,6 +1373,7 @@ "move": "Mover", "move-anyway": "Mover de todos os xeitos", "move-the-entity": "Mover o {{entity}}", + "move-to-folder": "Mover ao cartafol", "ms": "Milisegundos", "ms-team-plural": "Equipos MS", "multi-select": "Selección múltiple", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "Non se asignou ningún {{entity}}", "no-entity-available": "Non {{entity}} están dispoñibles", "no-entity-selected": "Non hai {{entity}} seleccionado", + "no-folder": "Sen cartafol", "no-knowledge-articles-available": "Non hai artigos de coñecemento dispoñibles", "no-kpis-yet": "Comeza a rastrexar o que importa", "no-matching-data-asset": "Non se atoparon activos de datos coincidentes", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index 6e5da5ec5e63..edf3fc0f010d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -931,6 +931,8 @@ "focus-home": "מיקוד על דף הבית", "focus-on-node": "התמקד בצומת", "focus-selected": "מיקוד על הנבחר", + "folder": "תיקייה", + "folder-plural": "תיקיות", "follow": "עקוב", "followed-lowercase": "עקוב", "follower-plural": "עוקבים", @@ -1371,6 +1373,7 @@ "move": "העבר", "move-anyway": "העבר בכל זאת", "move-the-entity": "העבר את {{entity}}", + "move-to-folder": "העבר לתיקייה", "ms": "מילי-שנייה", "ms-team-plural": "צוותי MS", "multi-select": "בחירה מרובה", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "לא הוקצה {{entity}}", "no-entity-available": "אין {{entity}} זמינים", "no-entity-selected": "לא נבחר {{entity}}", + "no-folder": "אין תיקייה", "no-knowledge-articles-available": "אין מאמרי ידע זמינים", "no-kpis-yet": "התחל לעקוב אחר מה שחשוב", "no-matching-data-asset": "לא נמצאו נכסי נתונים תואמים", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 5baeccd65f88..12dc9e16a0b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -931,6 +931,8 @@ "focus-home": "ホームにフォーカス", "focus-on-node": "ノードにフォーカス", "focus-selected": "選択項目にフォーカス", + "folder": "フォルダー", + "folder-plural": "フォルダー", "follow": "フォロー", "followed-lowercase": "フォロー中", "follower-plural": "フォロワー", @@ -1371,6 +1373,7 @@ "move": "移動", "move-anyway": "それでも移動", "move-the-entity": "{{entity}} を移動", + "move-to-folder": "フォルダーへ移動", "ms": "ミリ秒", "ms-team-plural": "MSチーム", "multi-select": "複数選択", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "{{entity}} が割り当てられていません", "no-entity-available": "利用可能な{{entity}}がありません", "no-entity-selected": "{{entity}}が選択されていません", + "no-folder": "フォルダーなし", "no-knowledge-articles-available": "利用可能なナレッジ記事がありません", "no-kpis-yet": "KPIの追跡を始めましょう", "no-matching-data-asset": "一致するデータアセットはありません", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index b42ee849da82..a29f39f85224 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -931,6 +931,8 @@ "focus-home": "홈으로 포커스", "focus-on-node": "노드에 집중", "focus-selected": "선택 항목 포커스", + "folder": "폴더", + "folder-plural": "폴더", "follow": "팔로우", "followed-lowercase": "팔로우됨", "follower-plural": "팔로워들", @@ -1371,6 +1373,7 @@ "move": "이동", "move-anyway": "그래도 이동", "move-the-entity": "{{entity}} 이동", + "move-to-folder": "폴더로 이동", "ms": "밀리초", "ms-team-plural": "MS 팀들", "multi-select": "다중 선택", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "{{entity}}이 할당되지 않았습니다.", "no-entity-available": "사용 가능한 {{entity}}가 없습니다", "no-entity-selected": "선택된 {{entity}} 없음", + "no-folder": "폴더 없음", "no-knowledge-articles-available": "사용 가능한 지식 문서가 없습니다", "no-kpis-yet": "중요한 항목 추적을 시작하세요", "no-matching-data-asset": "일치하는 데이터 자산을 찾을 수 없습니다", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index 7f209d33dff2..a30e068cd225 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -931,6 +931,8 @@ "focus-home": "मुख्यपृष्ठावर लक्ष केंद्रित करा", "focus-on-node": "नोडवर लक्ष केंद्रित करा", "focus-selected": "निवडलेल्यावर लक्ष केंद्रित करा", + "folder": "फोल्डर", + "folder-plural": "फोल्डर", "follow": "अनुसरण करा", "followed-lowercase": "अनुसरण केले", "follower-plural": "अनुयायी", @@ -1371,6 +1373,7 @@ "move": "हलवा", "move-anyway": "तरीही हलवा", "move-the-entity": "{{entity}} हलवा", + "move-to-folder": "फोल्डरमध्ये हलवा", "ms": "मिलीसेकंद", "ms-team-plural": "एमएस टीम्स", "multi-select": "मल्टी सिलेक्ट", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "कोणतेही {{entity}} नियुक्त केलेले नाही", "no-entity-available": "कोणतेही {{entity}} उपलब्ध नाहीत", "no-entity-selected": "कोणतेही {{entity}} निवडलेले नाही", + "no-folder": "फोल्डर नाही", "no-knowledge-articles-available": "कोणतेही ज्ञान लेख उपलब्ध नाहीत", "no-kpis-yet": "महत्त्वाच्या गोष्टींचे ट्रॅकिंग सुरू करा", "no-matching-data-asset": "जुळणारी डेटा ॲसेट सापडली नाही", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 75575b2ca169..6c90f8f8a00a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -931,6 +931,8 @@ "focus-home": "Naar Startweergave", "focus-on-node": "Focus op Knooppunt", "focus-selected": "Selectie Focussen", + "folder": "Map", + "folder-plural": "Mappen", "follow": "Volgen", "followed-lowercase": "gevolgd", "follower-plural": "Volgers", @@ -1371,6 +1373,7 @@ "move": "Verplaatsen", "move-anyway": "Toch verplaatsen", "move-the-entity": "Verplaats de {{entity}}", + "move-to-folder": "Verplaatsen naar map", "ms": "Milliseconden", "ms-team-plural": "MS-teams", "multi-select": "Meervoudige selectie", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "Geen {{entity}} toegewezen", "no-entity-available": "Er zijn geen {{entity}} beschikbaar", "no-entity-selected": "Geen {{entity}} geselecteerd", + "no-folder": "Geen map", "no-knowledge-articles-available": "Geen kennisartikelen beschikbaar", "no-kpis-yet": "Begin met het bijhouden van wat belangrijk is", "no-matching-data-asset": "Geen overeenkomende data-assets gevonden", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index fe3d92a22909..7332c937f105 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -931,6 +931,8 @@ "focus-home": "تمرکز بر صفحه اصلی", "focus-on-node": "تمرکز بر گره", "focus-selected": "تمرکز بر انتخاب شده", + "folder": "پوشه", + "folder-plural": "پوشه ها", "follow": "دنبال کردن", "followed-lowercase": "دنبال شده", "follower-plural": "دنبال‌کنندگان", @@ -1371,6 +1373,7 @@ "move": "Mover", "move-anyway": "همچنان جابجا کن", "move-the-entity": "انتقال {{entity}}", + "move-to-folder": "انتقال به پوشه", "ms": "میلی‌ثانیه", "ms-team-plural": "تیم‌های MS", "multi-select": "چند انتخابی", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "هیچ {{entity}} اختصاص داده نشده است", "no-entity-available": "هیچ {{entity}} در دسترس نیست", "no-entity-selected": "هیچ {{entity}} انتخاب نشده", + "no-folder": "بدون پوشه", "no-knowledge-articles-available": "هیچ مقاله دانشی موجود نیست", "no-kpis-yet": "شروع به پیگیری موارد بااهمیت کنید", "no-matching-data-asset": "هیچ دارایی داده‌ی منطبق یافت نشد", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index 35e274457c7f..b8484a36bbbb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -931,6 +931,8 @@ "focus-home": "Ir para Início", "focus-on-node": "Focar no Nó", "focus-selected": "Focar na Seleção", + "folder": "Pasta", + "folder-plural": "Pastas", "follow": "Seguir", "followed-lowercase": "seguido", "follower-plural": "Seguidores", @@ -1371,6 +1373,7 @@ "move": "Mover", "move-anyway": "Mover mesmo assim", "move-the-entity": "Mover a {{entity}}", + "move-to-folder": "Mover para a pasta", "ms": "Milissegundos", "ms-team-plural": "Equipes MS", "multi-select": "Seleção múltipla", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "Nenhum {{entity}} atribuído", "no-entity-available": "Nenhum {{entity}} disponível", "no-entity-selected": "Nenhum {{entity}} selecionado", + "no-folder": "Nenhuma pasta", "no-knowledge-articles-available": "Nenhum artigo de conhecimento disponível", "no-kpis-yet": "Comece a acompanhar o que importa", "no-matching-data-asset": "Nenhum ativo de dados correspondente encontrado", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index cbc6ab56ebf5..173a2668ebde 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -931,6 +931,8 @@ "focus-home": "Ir para Início", "focus-on-node": "Focar no Nó", "focus-selected": "Focar na Seleção", + "folder": "Pasta", + "folder-plural": "Pastas", "follow": "Seguir", "followed-lowercase": "seguido", "follower-plural": "Seguidores", @@ -1371,6 +1373,7 @@ "move": "Mover", "move-anyway": "Mover mesmo assim", "move-the-entity": "Mover a {{entity}}", + "move-to-folder": "Mover para a pasta", "ms": "Milissegundos", "ms-team-plural": "MS Teams", "multi-select": "Seleção múltipla", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "Nenhum(a) {{entity}} atribuído(a)", "no-entity-available": "Nenhum(a) {{entity}} disponível", "no-entity-selected": "Nenhum(a) {{entity}} Selecionado(a)", + "no-folder": "Nenhuma pasta", "no-knowledge-articles-available": "Não há artigos de conhecimento disponíveis", "no-kpis-yet": "Comece a acompanhar o que importa", "no-matching-data-asset": "Nenhum ativo de dados correspondente encontrado", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 7592f5dfa3cd..a5708585eb68 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -931,6 +931,8 @@ "focus-home": "На главную", "focus-on-node": "Фокус на узел", "focus-selected": "Фокус на выбранном", + "folder": "Папка", + "folder-plural": "Папки", "follow": "Подписаться", "followed-lowercase": "Подписан", "follower-plural": "Подписчики", @@ -1371,6 +1373,7 @@ "move": "Переместить", "move-anyway": "Переместить всё равно", "move-the-entity": "Переместите объект «{{entity}}»", + "move-to-folder": "Переместить в папку", "ms": "Миллисекунды", "ms-team-plural": "MS Команды", "multi-select": "Множественный выбор", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "{{entity}} не назначены", "no-entity-available": "Нет доступных объектов «{{entity}}»", "no-entity-selected": "Объект «{{entity}}» не выбран", + "no-folder": "Нет папки", "no-knowledge-articles-available": "Нет доступных статей базы знаний", "no-kpis-yet": "Начните отслеживать то, что важно", "no-matching-data-asset": "Подходящие объекты данных не найдены", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index cb435aaca239..c397ab68fc8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -931,6 +931,8 @@ "focus-home": "โฟกัสหน้าแรก", "focus-on-node": "โฟกัสที่โหนด", "focus-selected": "โฟกัสที่เลือก", + "folder": "โฟลเดอร์", + "folder-plural": "โฟลเดอร์", "follow": "ติดตาม", "followed-lowercase": "ติดตามแล้ว", "follower-plural": "ผู้ติดตาม", @@ -1371,6 +1373,7 @@ "move": "ย้าย", "move-anyway": "ย้ายต่อไป", "move-the-entity": "ย้าย {{entity}}", + "move-to-folder": "ย้ายไปยังโฟลเดอร์", "ms": "มิลลิวินาที", "ms-team-plural": "MS Teams", "multi-select": "หลายการเลือก", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "ไม่มี {{entity}} ที่กำหนด", "no-entity-available": "ไม่มี {{entity}} ที่พร้อมใช้งาน", "no-entity-selected": "ไม่มี {{entity}} ที่เลือก", + "no-folder": "ไม่มีโฟลเดอร์", "no-knowledge-articles-available": "ไม่มีบทความความรู้ที่พร้อมใช้งาน", "no-kpis-yet": "เริ่มติดตามสิ่งที่สำคัญ", "no-matching-data-asset": "ไม่พบสินทรัพย์ข้อมูลที่ตรงกัน", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json index 01d20111fd5d..ba4fd404ac28 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json @@ -931,6 +931,8 @@ "focus-home": "Ana Sayfaya Odaklan", "focus-on-node": "Düğüme Odaklan", "focus-selected": "Seçilene Odaklan", + "folder": "Klasör", + "folder-plural": "Klasörler", "follow": "Takip Et", "followed-lowercase": "takip edildi", "follower-plural": "Takipçiler", @@ -1371,6 +1373,7 @@ "move": "Taşı", "move-anyway": "Yine de taşı", "move-the-entity": "{{entity}} Taşı", + "move-to-folder": "Klasöre taşı", "ms": "Milisaniye", "ms-team-plural": "MS Teams", "multi-select": "Çoklu Seçim", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "{{entity}} atanmadı", "no-entity-available": "Kullanılabilir {{entity}} yok", "no-entity-selected": "Seçili {{entity}} Yok", + "no-folder": "Klasör yok", "no-knowledge-articles-available": "Kullanılabilir bilgi makalesi yok", "no-kpis-yet": "Önemli olanı takip etmeye başlayın", "no-matching-data-asset": "Eşleşen veri varlığı bulunamadı", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 549689e5535e..0ce3eb0b8f99 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -931,6 +931,8 @@ "focus-home": "焦点回到主页", "focus-on-node": "聚焦节点", "focus-selected": "聚焦选中项", + "folder": "文件夹", + "folder-plural": "文件夹", "follow": "关注", "followed-lowercase": "已关注", "follower-plural": "关注者", @@ -1371,6 +1373,7 @@ "move": "移动", "move-anyway": "仍然移动", "move-the-entity": "移动{{entity}}", + "move-to-folder": "移动到文件夹", "ms": "毫秒", "ms-team-plural": "微软团队", "multi-select": "多选", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "未分配 {{entity}}", "no-entity-available": "没有可用的 {{entity}}", "no-entity-selected": "未选择 {{entity}}", + "no-folder": "无文件夹", "no-knowledge-articles-available": "暂无可用的知识文章", "no-kpis-yet": "开始跟踪重要内容", "no-matching-data-asset": "未找到匹配的数据资产", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json index 36873dc062a0..7964fd609780 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json @@ -931,6 +931,8 @@ "focus-home": "焦點回到首頁", "focus-on-node": "聚焦節點", "focus-selected": "聚焦已選項目", + "folder": "資料夾", + "folder-plural": "資料夾", "follow": "追蹤", "followed-lowercase": "已追蹤", "follower-plural": "追蹤者", @@ -1371,6 +1373,7 @@ "move": "移動", "move-anyway": "仍要移動", "move-the-entity": "移動 {{entity}}", + "move-to-folder": "移動到資料夾", "ms": "毫秒", "ms-team-plural": "MS Teams", "multi-select": "多選", @@ -1420,6 +1423,7 @@ "no-entity-assigned": "未指派 {{entity}}", "no-entity-available": "無可用的 {{entity}}", "no-entity-selected": "未選取 {{entity}}", + "no-folder": "無資料夾", "no-knowledge-articles-available": "目前沒有可用的知識文章", "no-kpis-yet": "開始追蹤重要指標", "no-matching-data-asset": "找不到相符的資料資產", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx index 2da54c0c81c5..22b487c2350f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx @@ -19,6 +19,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { withActivityFeed } from '../../../components/AppRouter/withActivityFeed'; +import AlertBar from '../../../components/AlertBar/AlertBar'; import ArticleDetailHeader from '../../../components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component'; import ArticleVersionHeader from '../../../components/ContextCenter/ArticleVersionHeader/ArticleVersionHeader.component'; import ContextCenterHeader from '../../../components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component'; @@ -38,6 +39,7 @@ import { } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityTabs } from '../../../enums/entity.enum'; import LimitWrapper from '../../../hoc/LimitWrapper'; +import { useAlertStore } from '../../../hooks/useAlertStore'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useFqn } from '../../../hooks/useFqn'; import { @@ -62,6 +64,7 @@ const ContextCenterArticlesPage = () => { const { fqn } = useFqn(); const { version } = useRequiredParams<{ version?: string }>(); const { currentUser } = useApplicationStore(); + const { alert } = useAlertStore(); const USERId = currentUser?.id ?? ''; const { getResourcePermission } = usePermissionProvider(); const { getResourceLimit } = useLimitStore(); @@ -79,6 +82,7 @@ const ContextCenterArticlesPage = () => { }); const [isRightPanelOpen, setIsRightPanelOpen] = useState(true); const [showAddLinkModal, setShowAddLinkModal] = useState(false); + const [articleSearchQuery, setArticleSearchQuery] = useState(''); const handleFetchKnowledgePageHierarchy = useCallback( (forceRefresh?: boolean) => @@ -226,8 +230,13 @@ const ContextCenterArticlesPage = () => { { activeTitle: true, name: t('label.article-plural'), url: '' }, ]} hasPermission={permissions?.Create} + searchPlaceholder={t('label.search-entity', { + entity: t('label.article-plural'), + })} + searchQuery={articleSearchQuery} subtitle={t('message.internal-knowledge-base-agent-training')} title={t('label.article-plural')} + onSearch={setArticleSearchQuery} /> ); }; @@ -295,6 +304,7 @@ const ContextCenterArticlesPage = () => { rightPanelSlot={ contextCenterClassBase.isEmbeddedMode() ? null : undefined } + searchQuery={articleSearchQuery} onPageChange={handlePageChange} /> ); @@ -303,6 +313,7 @@ const ContextCenterArticlesPage = () => { fqn, isRightPanelOpen, permissions, + articleSearchQuery, handlePageChange, handleFetchKnowledgePageHierarchy, handleToggleRightPanel, @@ -312,6 +323,7 @@ const ContextCenterArticlesPage = () => {
+ {alert && } {renderHeader()} { const { t } = useTranslation(); const navigate = useNavigate(); + const { alert } = useAlertStore(); const { currentUser } = useApplicationStore(); const { getResourcePermission } = usePermissionProvider(); @@ -75,6 +81,9 @@ const ContextCenterDashboardPage: FC = () => { const response = await getListKnowledgePages({ fields: 'tags,page', limit: RECENT_ARTICLES_LIMIT, + pageType: PageType.ARTICLE, + sortBy: 'updatedAt', + sortOrder: 'desc', }); setArticles( response.data.map((page: KnowledgePage) => @@ -91,8 +100,8 @@ const ContextCenterDashboardPage: FC = () => { const fetchDocuments = useCallback(async () => { setIsDocumentsLoading(true); try { - const assets = await fetchContextCenterDocuments(); - setDocuments(assets.map(assetToDocumentItem)); + const files = await listContextFiles(RECENT_DOCUMENTS_LIMIT); + setDocuments(files.map(contextFileToUploadedDocumentItem)); } catch (err) { showErrorToast(err as AxiosError); } finally { @@ -117,14 +126,18 @@ const ContextCenterDashboardPage: FC = () => { fetchPermission(); }, [fetchRecentArticles, fetchDocuments, fetchPermission]); - const handleUploaded = useCallback((newAssets: Asset[]) => { - setDocuments((prev) => [...newAssets.map(assetToDocumentItem), ...prev]); + const handleUploaded = useCallback((newFiles: ContextFile[]) => { + setDocuments((prev) => [ + ...newFiles.map(contextFileToUploadedDocumentItem), + ...prev, + ]); }, []); return (
+ {alert && } {
setIsUploadModalOpen(false)} onUploaded={handleUploaded} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index 033619e5ad91..8a09e3ea8546 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -13,40 +13,53 @@ import { Home02 } from '@untitledui/icons'; import { AxiosError } from 'axios'; +import DocumentFolderView from 'components/ContextCenter/DocumentsView/DocumentFolderView.component'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; +import AlertBar from '../../../components/AlertBar/AlertBar'; import DeleteModal from '../../../components/common/DeleteModal/DeleteModal'; +import '../../../components/common/ResizablePanels/resizable-panels.less'; import ContextCenterHeader from '../../../components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component'; import DocumentsView from '../../../components/ContextCenter/DocumentsView/DocumentsView.component'; import { DocFile } from '../../../components/ContextCenter/DocumentsView/DocumentsView.interface'; +import MoveToFolderModal from '../../../components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component'; import UploadDocumentModal from '../../../components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, ResourceEntity, } from '../../../context/PermissionProvider/PermissionProvider.interface'; -import { deleteAsset } from '../../../rest/assetAPI'; +import { SearchIndex } from '../../../enums/search.enum'; +import { ContextFile } from '../../../generated/entity/data/contextFile'; +import { Folder } from '../../../generated/entity/data/folder'; +import { deleteAsset, listContextFiles } from '../../../rest/assetAPI'; +import { searchQuery as fetchSearchResults } from '../../../rest/searchAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; import { - assetToDocumentItem, - CONTEXT_CENTER_DOCUMENTS_ENTITY_LINK, - fetchContextCenterDocuments, + contextFileToDocumentItem, handleAssetDownload, } from '../../../utils/ContextCenterUtils'; +import { useAlertStore } from '../../../hooks/useAlertStore'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; const ContextCenterDocumentsPage: FC = () => { const { t } = useTranslation(); + const { alert } = useAlertStore(); const { getResourcePermission } = usePermissionProvider(); - const [documents, setDocuments] = useState([]); + const [allDocuments, setAllDocuments] = useState([]); const [isDocumentsLoading, setIsDocumentsLoading] = useState(true); + const [documentSearchQuery, setDocumentSearchQuery] = useState(''); const [isDeletingFile, setIsDeletingFile] = useState(false); const [fileToDelete, setFileToDelete] = useState(); + const [fileToMove, setFileToMove] = useState(); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [permissions, setPermissions] = useState( DEFAULT_ENTITY_PERMISSION ); + const [selectedFolderId, setSelectedFolderId] = useState(); + const [folders, setFolders] = useState([]); const { hasCreatePermission, hasDeletePermission } = useMemo( () => ({ @@ -56,21 +69,51 @@ const ContextCenterDocumentsPage: FC = () => { [permissions.Create, permissions.Delete] ); + const selectedFolderFqn = useMemo( + () => + selectedFolderId + ? folders.find((f) => f.id === selectedFolderId)?.fullyQualifiedName + : undefined, + [selectedFolderId, folders] + ); + + const documents = useMemo(() => { + if (!selectedFolderId) { + return allDocuments; + } + + return allDocuments.filter((d) => d.folderId === selectedFolderId); + }, [allDocuments, selectedFolderId]); + const fetchDocuments = useCallback(async () => { setIsDocumentsLoading(true); try { - const assets = await fetchContextCenterDocuments(); - setDocuments(assets.map(assetToDocumentItem)); + if (documentSearchQuery) { + const results = await fetchSearchResults({ + query: documentSearchQuery, + searchIndex: SearchIndex.DRIVE_FILE, + sortField: 'updatedAt', + sortOrder: 'desc', + }); + setAllDocuments( + results.hits.hits.map((hit) => + contextFileToDocumentItem(hit._source as unknown as ContextFile) + ) + ); + } else { + const files = await listContextFiles(); + setAllDocuments(files.map(contextFileToDocumentItem)); + } } catch (err) { showErrorToast(err as AxiosError); } finally { setIsDocumentsLoading(false); } - }, []); + }, [documentSearchQuery]); useEffect(() => { fetchDocuments(); - }, []); + }, [fetchDocuments]); const fetchPermission = useCallback(async () => { try { @@ -102,8 +145,8 @@ const ContextCenterDocumentsPage: FC = () => { try { setIsDeletingFile(true); - await deleteAsset(fileToDelete.id, true); - setDocuments((prev) => + await deleteAsset(fileToDelete.id, false); + setAllDocuments((prev) => prev.filter((document) => document.id !== fileToDelete.id) ); showSuccessToast( @@ -119,10 +162,29 @@ const ContextCenterDocumentsPage: FC = () => { } }, [fileToDelete, t]); + const handleMoveFile = useCallback((file: DocFile) => { + setFileToMove(file); + }, []); + + const handleFileMoved = useCallback( + (file: DocFile, targetFolderId: string) => { + setAllDocuments((prev) => + prev.map((d) => + d.driveFileId === file.driveFileId + ? { ...d, folderId: targetFolderId } + : d + ) + ); + setFileToMove(undefined); + }, + [] + ); + return (
+ {alert && } { }, ]} hasPermission={hasCreatePermission} + searchPlaceholder={t('label.search-entity', { + entity: t('label.document-plural'), + })} + searchQuery={documentSearchQuery} subtitle={t('message.context-center-documents-subtitle')} title={t('label.document-plural')} + onSearch={setDocumentSearchQuery} onUploadFile={() => setIsUploadModalOpen(true)} /> -
- -
+ + + + + + +
+
+
+ + + + + + setIsUploadModalOpen(false)} - onUploaded={() => fetchDocuments()} + onUploaded={(newFiles) => + setAllDocuments((prev) => [ + ...prev, + ...newFiles.map(contextFileToDocumentItem), + ]) + } /> {fileToDelete && ( @@ -176,6 +268,16 @@ const ContextCenterDocumentsPage: FC = () => { onDelete={handleConfirmDelete} /> )} + + {fileToMove && ( + setFileToMove(undefined)} + onMoved={handleFileMoved} + /> + )}
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts index 93e6af682484..47dd62377e37 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts @@ -12,8 +12,75 @@ */ import { AxiosResponse } from 'axios'; import { Asset, AssetType } from '../generated/attachments/asset'; +import { ContextFile } from '../generated/entity/data/contextFile'; +import { Folder } from '../generated/entity/data/folder'; import APIClient from './index'; +export interface CreateFolderRequest { + name: string; + displayName?: string; +} + +export const createFolder = async ( + data: CreateFolderRequest +): Promise => { + const response = await APIClient.post('/contextCenter/drive/folders', data); + + return response.data; +}; + +export const listFolders = async (): Promise => { + const response = await APIClient.get<{ data: Folder[] }>('/contextCenter/drive/folders'); + + return response.data.data ?? []; +}; + +export const deleteFolder = async ( + id: string, + hardDelete = false +): Promise => { + await APIClient.delete(`/contextCenter/drive/folders/${id}`, { + params: { hardDelete }, + }); +}; + +export const listContextFiles = async (limit = 100): Promise => { + const response = await APIClient.get<{ data: ContextFile[] }>( + '/contextCenter/drive/files', + { params: { fields: 'folder', limit } } + ); + + return response.data.data ?? []; +}; + +export const moveFileToFolder = async ( + driveFileId: string, + folderId: string +): Promise => { + await APIClient.put(`/contextCenter/drive/files/${driveFileId}/move`, { + folder: { id: folderId, type: 'folder' }, + }); +}; + +export const uploadDriveFile = async ( + file: File, + folderFqn?: string +): Promise => { + const formData = new FormData(); + formData.append('file', file); + + if (folderFqn) { + formData.append('folder', folderFqn); + } + + const response = await APIClient.post>( + '/contextCenter/drive/files/upload', + formData + ); + + return response.data; +}; + export const uploadAsset = async ( file: File, entityLink: string, @@ -32,12 +99,38 @@ export const uploadAsset = async ( return response.data; }; +export interface ListAssetsByFqnParams { + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + limit?: number; +} + export const listAssetsByFqn = async ( fqn: string, - assetType: AssetType = AssetType.External + assetType: AssetType = AssetType.External, + params?: ListAssetsByFqnParams ): Promise => { const response = await APIClient.get( - `/attachments/fqn/${encodeURIComponent(fqn)}/${assetType}` + `/attachments/fqn/${encodeURIComponent(fqn)}/${assetType}`, + { params } + ); + + return response.data; +}; + +export const deleteDriveFile = async ( + id: string, + hardDelete = false +): Promise => { + await APIClient.delete(`/contextCenter/drive/files/${id}`, { + params: { hardDelete }, + }); +}; + +export const downloadDriveFile = async (id: string): Promise => { + const response = await APIClient.get( + `/contextCenter/drive/files/${id}/download`, + { params: { redirect: false, expiry: 300 }, responseType: 'blob' } ); return response.data; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts index 50982b3d8257..5945b7c7095f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts @@ -12,6 +12,7 @@ */ import { AxiosResponse } from 'axios'; import { Operation } from 'fast-json-patch'; +import { PagingResponse } from 'Models'; import { VotingDataProps } from '../components/Entity/Voting/voting.interface'; import { EntityReference } from '../generated/entity/type'; import { EntityHistory } from '../generated/type/entityHistory'; @@ -24,7 +25,6 @@ import { PageHierarchy, PageType, } from '../interface/knowledge-center.interface'; -import { PagingResponse } from '../Models'; import APIClient from '../rest/index'; export interface KnowledgePageHierarchyParams { @@ -39,6 +39,9 @@ export type KnowledgePageListParams = ListParams & { entityType?: string; entityId?: string; tagFQN?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + offset?: number; }; export const getListKnowledgePages = async ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx index 31eb08e54636..557728e92a25 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx @@ -15,22 +15,29 @@ import { File06 } from '@untitledui/icons'; import { AxiosError } from 'axios'; import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill'; import { isNull, isUndefined } from 'lodash'; +import { FC } from 'react'; import { ReactComponent as DOCIcon } from '../assets/svg/ic-doc.svg'; import { ReactComponent as ImageIcon } from '../assets/svg/ic-image.svg'; import { ReactComponent as PDFIcon } from '../assets/svg/ic-pdf.svg'; import { ReactComponent as XLSIcon } from '../assets/svg/ic-xls.svg'; import { ArticleCardItem } from '../components/ContextCenter/ArticleCard/ArticleCard.interface'; -import { DocFile } from '../components/ContextCenter/DocumentsView/DocumentsView.interface'; +import { DocFile, DocFileType } from '../components/ContextCenter/DocumentsView/DocumentsView.interface'; import { UploadedDocumentItem } from '../components/ContextCenter/UploadedDocumentCard/UploadedDocumentCard.interface'; import { CREATE_PAGE_HASH } from '../constants/constants'; +import { FILE_TYPE_STYLES } from '../constants/ContextCenter.constants'; import { EntityType } from '../enums/entity.enum'; import { Asset, AssetType } from '../generated/attachments/asset'; +import { ContextFile } from '../generated/entity/data/contextFile'; import { CreateKnowledgePage, PageType, QuickLink, } from '../interface/knowledge-center.interface'; -import { downloadAsset, listAssetsByFqn } from '../rest/assetAPI'; +import { + downloadAsset, + listAssetsByFqn, + ListAssetsByFqnParams, +} from '../rest/assetAPI'; import { postKnowledgePage } from '../rest/knowledgeCenterAPI'; import contextCenterClassBase from './ContextCenterClassBase'; import EntityLink from './EntityLink'; @@ -114,6 +121,30 @@ export const assetToDocumentItem = (asset: Asset): UploadedDocumentItem => ({ updatedAt: asset.updatedAt ?? 0, }); +export const contextFileToDocumentItem = (file: ContextFile): DocFile => ({ + driveFileId: file.id, + fileType: extensionToFileType(file.displayName ?? file.name), + folderId: file.folder?.id, + folderFqn: file.folder?.fullyQualifiedName, + id: file.assetId ?? file.id, + name: file.displayName ?? file.name, + sizeLabel: formatBytes(file.fileSize), + updatedAt: file.updatedAt, + updatedBy: file.updatedBy, +}); + +export const contextFileToUploadedDocumentItem = ( + file: ContextFile +): UploadedDocumentItem => ({ + fileType: extensionToFileType(file.displayName ?? file.name), + id: file.assetId ?? file.id, + name: file.displayName ?? file.name, + sizeLabel: formatBytes(file.fileSize), + status: 'processed', + updatedAt: file.updatedAt ?? 0, + updatedBy: file.updatedBy ?? '', +}); + export const knowledgePageToArticleItem = ( data: { id: string; @@ -142,8 +173,14 @@ export const knowledgePageToArticleItem = ( title: getEntityName(data) || untitledLabel, }); -export const fetchContextCenterDocuments = async (): Promise => { - return listAssetsByFqn(CONTEXT_CENTER_DOCUMENTS_FQN, AssetType.External); +export const fetchContextCenterDocuments = async ( + params?: ListAssetsByFqnParams +): Promise => { + return listAssetsByFqn( + CONTEXT_CENTER_DOCUMENTS_FQN, + AssetType.External, + params + ); }; export const createArticleKnowledgePage = async ( @@ -198,3 +235,14 @@ export const handleAssetDownload = async (file: DocFile) => { } } }; + +export const FileTypeBadge: FC<{ fileType: DocFileType }> = ({ fileType }) => { + const { bg, label, text } = FILE_TYPE_STYLES[fileType || 'other']; + + return ( + + {label} + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx index 60ea17ed2e35..e0ad1b9b6f3b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx @@ -55,7 +55,7 @@ import oracleConnection from '../jsons/connectionSchemas/connections/database/or import pinotConnection from '../jsons/connectionSchemas/connections/database/pinotDBConnection.json'; import postgresConnection from '../jsons/connectionSchemas/connections/database/postgresConnection.json'; import prestoConnection from '../jsons/connectionSchemas/connections/database/prestoConnection.json'; -import questdbConnection from '../jsons/connectionSchemas/connections/database/questdbConnection.json'; +// import questdbConnection from '../jsons/connectionSchemas/connections/database/questdbConnection.json'; import redshiftConnection from '../jsons/connectionSchemas/connections/database/redshiftConnection.json'; import salesforceConnection from '../jsons/connectionSchemas/connections/database/salesforceConnection.json'; import sapErpConnection from '../jsons/connectionSchemas/connections/database/sapErpConnection.json'; @@ -201,7 +201,7 @@ export const getDatabaseConfig = (type: DatabaseServiceType) => { break; } case DatabaseServiceType.QuestDB: { - schema = questdbConnection; + // schema = questdbConnection; break; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts index 6676215b75c3..97dbed457099 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts @@ -47,6 +47,7 @@ type TagWidgetKeys = | DetailPageWidgetKeys.DOMAIN; class TagClassBase { + static filterClassification: string[] = [] defaultWidgetHeight: Record; constructor() { From 6f6376cc638490c9c69b8fda09f00242e0016d24 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 19 May 2026 00:45:43 +0530 Subject: [PATCH 02/17] lint fix --- .../KnowledgeCenterReviewerWorkflow.spec.ts | 8 +- .../ui/playwright/utils/userWorkflowUtils.ts | 4 +- .../ContextCenterHeader.component.tsx | 7 +- .../CreateFolderModal.component.tsx | 5 +- .../CreateFolderModal.test.tsx | 20 +++- .../DocumentFolderView.component.tsx | 37 +++++--- .../DocumentsView/DocumentFolderView.test.tsx | 91 ++++++------------- .../DocumentsView/DocumentsView.component.tsx | 7 +- .../MoveToFolderModal.component.tsx | 8 +- .../MoveToFolderModal.test.tsx | 16 +++- .../KnowledgePageListComponent.tsx | 19 +++- .../ui/src/interface/search.interface.ts | 3 +- .../ContextCenterArticlesPage.tsx | 2 +- .../ContextCenterDocumentsPage.tsx | 6 +- .../main/resources/ui/src/rest/assetAPI.ts | 9 +- .../ui/src/utils/DatabaseServiceUtils.tsx | 4 +- .../resources/ui/src/utils/TagClassBase.ts | 2 +- 17 files changed, 133 insertions(+), 115 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts index de0c05d6b939..0d39fdd89628 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts @@ -28,7 +28,7 @@ test.beforeAll('Setup workflow and knowledge page', async ({ browser }) => { const { apiContext, afterAction } = await performAdminLogin(browser); await createUserApprovalWorkflowWithOwners(apiContext, 'page'); - const createResponse = await apiContext.post('/api/v1/knowledgeCenter', { + const createResponse = await apiContext.post('/api/v1/contextCenter/pages', { data: { name: `pw-article-${uuid()}`, displayName: `PW Article ${uuid()}`, @@ -71,7 +71,7 @@ test.describe( await dcAfterAction(); const patchResponse = await apiContext.patch( - `/api/v1/knowledgeCenter/${knowledgePage.id}`, + `/api/v1/contextCenter/pages/${knowledgePage.id}`, { data: [ { @@ -112,9 +112,7 @@ test.describe( await test.step('Reviewer - Check notification and approve task', async () => { await checkNotificationAndApproveTask(dataConsumerPage, async () => { - await dataConsumerPage.goto( - `/context-center/articles/${encodedFqn}` - ); + await dataConsumerPage.goto(`/context-center/articles/${encodedFqn}`); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts index 1b037372c9cd..e5bb4b9ae001 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts @@ -207,7 +207,6 @@ export const createUserApprovalWorkflowWithOwners = async ( return response.json(); }; - const getFQN = (entity: EntityWithFQN): string => { if (entity instanceof TagClass && entity.responseData) { return entity.responseData.fullyQualifiedName || ''; @@ -222,7 +221,6 @@ const getFQN = (entity: EntityWithFQN): string => { return ''; }; - type EntityWithFQN = DataProduct | TagClass | { fullyQualifiedName: string }; export const verifyTaskStatus = async ( @@ -273,4 +271,4 @@ export const verifyTaskStatus = async ( await expect( page.locator(`.status-badge.${statusBadge} .status-badge-label`) ).toContainText(statusLabel); -}; \ No newline at end of file +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx index ca78c917a8d3..3634667b734e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx @@ -11,7 +11,12 @@ * limitations under the License. */ -import { Button, Card, Input, Typography } from '@openmetadata/ui-core-components'; +import { + Button, + Card, + Input, + Typography, +} from '@openmetadata/ui-core-components'; import { Plus, SearchMd, UploadCloud02 } from '@untitledui/icons'; import { FC } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx index f7eb6ca1302e..da22370ad50a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx @@ -55,7 +55,10 @@ const CreateFolderModal: FC = ({ try { setIsCreating(true); - const folder = await createFolder({ name: trimmed, displayName: trimmed }); + const folder = await createFolder({ + name: trimmed, + displayName: trimmed, + }); onCreated(folder); handleClose(); } catch (err) { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx index 52dd405ca7d5..5197775de003 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.test.tsx @@ -11,7 +11,13 @@ * limitations under the License. */ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; import { createFolder } from 'rest/assetAPI'; import CreateFolderModal from './CreateFolderModal.component'; @@ -158,7 +164,11 @@ describe('CreateFolderModal', () => { }); it('calls createFolder with trimmed name on submit', async () => { - const folder = { id: 'folder-1', name: 'my-folder', displayName: 'My Folder' }; + const folder = { + id: 'folder-1', + name: 'my-folder', + displayName: 'My Folder', + }; (createFolder as jest.Mock).mockResolvedValue(folder); render(); @@ -178,7 +188,11 @@ describe('CreateFolderModal', () => { }); it('calls onCreated with the returned folder on success', async () => { - const folder = { id: 'folder-1', name: 'my-folder', displayName: 'My Folder' }; + const folder = { + id: 'folder-1', + name: 'my-folder', + displayName: 'My Folder', + }; (createFolder as jest.Mock).mockResolvedValue(folder); render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx index f89e2c5250df..7620aca7cc65 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx @@ -12,11 +12,11 @@ */ import { - ButtonUtility, - Card, - Skeleton, - Tree, - Typography, + ButtonUtility, + Card, + Skeleton, + Tree, + Typography, } from '@openmetadata/ui-core-components'; import { Plus, Trash01 } from '@untitledui/icons'; import { AxiosError } from 'axios'; @@ -118,9 +118,15 @@ const DocumentFolderView = ({ {t('label.folder')} - - {folders.length} {t('label.folder-plural')} - {files.length} {t('label.file-plural')} + + + {folders.length} {t('label.folder-plural')} + + + {files.length} {t('label.file-plural')} +
@@ -138,13 +144,16 @@ const DocumentFolderView = ({ {isLoading ? (
{Array.from({ length: 4 }).map((_, i) => ( - + ))}
) : ( - + {folders.map((folder) => { const isSelected = selectedFolderId === folder.id; const folderFiles = files.filter( @@ -153,9 +162,7 @@ const DocumentFolderView = ({ return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx index 4836fe2db21d..35e3af0861b4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx @@ -11,7 +11,13 @@ * limitations under the License. */ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; import { deleteFolder, listFolders } from 'rest/assetAPI'; import DocumentFolderView from './DocumentFolderView.component'; import { DocFile } from './DocumentsView.interface'; @@ -37,7 +43,11 @@ jest.mock('../CreateFolderModal/CreateFolderModal.component', () => @@ -90,10 +100,7 @@ jest.mock('@openmetadata/ui-core-components', () => ({ 'data-testid'?: string; tooltip?: string; }) => ( - ) @@ -108,13 +115,9 @@ jest.mock('@openmetadata/ui-core-components', () => ({ )), { Item: jest.fn( - ({ - children, - id, - }: { - children: React.ReactNode; - id: string; - }) =>
{children}
+ ({ children, id }: { children: React.ReactNode; id: string }) => ( +
{children}
+ ) ), ItemContent: jest.fn(({ children }: { children: React.ReactNode }) => (
{children}
@@ -156,23 +159,13 @@ describe('DocumentFolderView', () => { it('shows skeletons while loading', () => { (listFolders as jest.Mock).mockReturnValue(new Promise(() => undefined)); - render( - - ); + render(); expect(screen.getAllByTestId('skeleton').length).toBeGreaterThan(0); }); it('renders folder names after loading', async () => { - render( - - ); + render(); await waitFor(() => expect(screen.getByText('Folder One')).toBeInTheDocument() @@ -197,12 +190,7 @@ describe('DocumentFolderView', () => { }); it('renders files as children under their parent folder', async () => { - render( - - ); + render(); await waitFor(() => expect(screen.getByText('Folder One')).toBeInTheDocument() @@ -213,12 +201,7 @@ describe('DocumentFolderView', () => { }); it('shows folder and file counts in the subtitle', async () => { - render( - - ); + render(); await waitFor(() => expect(screen.getByText('Folder One')).toBeInTheDocument() @@ -230,10 +213,7 @@ describe('DocumentFolderView', () => { it('calls onSelectFolder with folderId when a folder is clicked', async () => { const onSelectFolder = jest.fn(); render( - + ); await waitFor(() => @@ -265,12 +245,7 @@ describe('DocumentFolderView', () => { }); it('opens the create folder modal when the add button is clicked', async () => { - render( - - ); + render(); await waitFor(() => expect(screen.getByText('Folder One')).toBeInTheDocument() @@ -310,12 +285,7 @@ describe('DocumentFolderView', () => { }); it('shows delete modal when delete button is clicked', async () => { - render( - - ); + render(); await waitFor(() => expect(screen.getByText('Folder One')).toBeInTheDocument() @@ -349,9 +319,7 @@ describe('DocumentFolderView', () => { fireEvent.click(screen.getByTestId('confirm-delete-btn')); }); - await waitFor(() => - expect(deleteFolder).toHaveBeenCalledWith('folder-1') - ); + await waitFor(() => expect(deleteFolder).toHaveBeenCalledWith('folder-1')); await waitFor(() => expect(screen.queryByText('Folder One')).not.toBeInTheDocument() ); @@ -379,18 +347,11 @@ describe('DocumentFolderView', () => { fireEvent.click(screen.getByTestId('confirm-delete-btn')); }); - await waitFor(() => - expect(onSelectFolder).toHaveBeenCalledWith(undefined) - ); + await waitFor(() => expect(onSelectFolder).toHaveBeenCalledWith(undefined)); }); it('closes delete modal on cancel without deleting', async () => { - render( - - ); + render(); await waitFor(() => expect(screen.getByText('Folder One')).toBeInTheDocument() diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 41f905ed0470..4d8757d9c8d5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -102,7 +102,12 @@ const FileActions: FC = ({ const FileRowSkeleton: FC = () => (
- +
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx index ddff6ce17673..9e1529ad81a3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx @@ -50,8 +50,8 @@ const MoveToFolderModal: FC = ({ const [isMoving, setIsMoving] = useState(false); const currentFolderName = file.folderId - ? (folders.find((f) => f.id === file.folderId)?.displayName ?? - folders.find((f) => f.id === file.folderId)?.name) + ? folders.find((f) => f.id === file.folderId)?.displayName ?? + folders.find((f) => f.id === file.folderId)?.name : undefined; const availableFolders = folders.filter((f) => f.id !== file.folderId); @@ -95,7 +95,9 @@ const MoveToFolderModal: FC = ({
- + {file.name}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx index 1ed2965a976e..d1b9bbb0d927 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx @@ -11,7 +11,13 @@ * limitations under the License. */ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; import { moveFileToFolder } from 'rest/assetAPI'; import { DocFile } from '../DocumentsView/DocumentsView.interface'; import MoveToFolderModal from './MoveToFolderModal.component'; @@ -86,9 +92,7 @@ jest.mock('@openmetadata/ui-core-components', () => ({ }) => { mockOnSelectionChange = onSelectionChange; - return ( -
{children}
- ); + return
{children}
; } ), { @@ -180,7 +184,9 @@ describe('MoveToFolderModal', () => { it('excludes the current folder from select options', () => { render(); - expect(screen.queryByTestId('select-item-folder-a')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('select-item-folder-a') + ).not.toBeInTheDocument(); expect(screen.getByTestId('select-item-folder-b')).toBeInTheDocument(); expect(screen.getByTestId('select-item-folder-c')).toBeInTheDocument(); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx index a0b54e17e9e4..b5d4e3445ff0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageListComponent/KnowledgePageListComponent.tsx @@ -48,6 +48,7 @@ import { getKnowledgePageFields } from '../../../constants/KnowledgeCenter.const import { useLimitStore } from '../../../context/LimitsProvider/useLimitsStore'; import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../../enums/common.enum'; +import { SearchIndex } from '../../../enums/search.enum'; import { Paging } from '../../../generated/type/paging'; import LimitWrapper from '../../../hoc/LimitWrapper'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; @@ -59,7 +60,6 @@ import { KnowledgePage, PageType, } from '../../../interface/knowledge-center.interface'; -import { SearchIndex } from '../../../enums/search.enum'; import { followKnowledgePage, getListKnowledgePages, @@ -338,12 +338,25 @@ const KnowledgePageListComponent = forwardRef< useEffect(() => { const hasMore = knowledgePages.length < paging.total; - if (isInView && hasMore && !isLoadingMore && !searchQuery && hasViewPermission) { + if ( + isInView && + hasMore && + !isLoadingMore && + !searchQuery && + hasViewPermission + ) { const nextOffset = pageOffset + PAGE_SIZE_MEDIUM; setPageOffset(nextOffset); fetchKnowledgePages(nextOffset); } - }, [isInView, paging.total, knowledgePages.length, isLoadingMore, searchQuery, hasViewPermission]); + }, [ + isInView, + paging.total, + knowledgePages.length, + isLoadingMore, + searchQuery, + hasViewPermission, + ]); const items: MenuProps['items'] = [ { diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts index a32f84df2d9c..58c143e14d36 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/search.interface.ts @@ -20,6 +20,7 @@ import { APICollection } from '../generated/entity/data/apiCollection'; import { APIEndpoint } from '../generated/entity/data/apiEndpoint'; import { Chart } from '../generated/entity/data/chart'; import { Container } from '../generated/entity/data/container'; +import { ContextFile } from '../generated/entity/data/contextFile'; import { Dashboard } from '../generated/entity/data/dashboard'; import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel'; import { @@ -62,8 +63,6 @@ import { TestCaseResolutionStatus } from '../generated/tests/testCaseResolutionS import { TestSuite } from '../generated/tests/testSuite'; import { TagLabel } from '../generated/type/tagLabel'; import { AggregatedCostAnalysisReportDataSearchSource } from './data-insight.interface'; -import { Asset } from '../generated/attachments/asset'; -import { ContextFile } from '../generated/entity/data/contextFile'; import { KnowledgePage } from './knowledge-center.interface'; /** diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx index 22b487c2350f..5367958b9c12 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx @@ -18,8 +18,8 @@ import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import { withActivityFeed } from '../../../components/AppRouter/withActivityFeed'; import AlertBar from '../../../components/AlertBar/AlertBar'; +import { withActivityFeed } from '../../../components/AppRouter/withActivityFeed'; import ArticleDetailHeader from '../../../components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component'; import ArticleVersionHeader from '../../../components/ContextCenter/ArticleVersionHeader/ArticleVersionHeader.component'; import ContextCenterHeader from '../../../components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index 8a09e3ea8546..0c9073a24047 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -33,6 +33,7 @@ import { import { SearchIndex } from '../../../enums/search.enum'; import { ContextFile } from '../../../generated/entity/data/contextFile'; import { Folder } from '../../../generated/entity/data/folder'; +import { useAlertStore } from '../../../hooks/useAlertStore'; import { deleteAsset, listContextFiles } from '../../../rest/assetAPI'; import { searchQuery as fetchSearchResults } from '../../../rest/searchAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; @@ -40,7 +41,6 @@ import { contextFileToDocumentItem, handleAssetDownload, } from '../../../utils/ContextCenterUtils'; -import { useAlertStore } from '../../../hooks/useAlertStore'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; @@ -226,7 +226,9 @@ const ContextCenterDocumentsPage: FC = () => { /> - +
diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts index 47dd62377e37..b338216e5439 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts @@ -24,13 +24,18 @@ export interface CreateFolderRequest { export const createFolder = async ( data: CreateFolderRequest ): Promise => { - const response = await APIClient.post('/contextCenter/drive/folders', data); + const response = await APIClient.post( + '/contextCenter/drive/folders', + data + ); return response.data; }; export const listFolders = async (): Promise => { - const response = await APIClient.get<{ data: Folder[] }>('/contextCenter/drive/folders'); + const response = await APIClient.get<{ data: Folder[] }>( + '/contextCenter/drive/folders' + ); return response.data.data ?? []; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx index e0ad1b9b6f3b..60ea17ed2e35 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatabaseServiceUtils.tsx @@ -55,7 +55,7 @@ import oracleConnection from '../jsons/connectionSchemas/connections/database/or import pinotConnection from '../jsons/connectionSchemas/connections/database/pinotDBConnection.json'; import postgresConnection from '../jsons/connectionSchemas/connections/database/postgresConnection.json'; import prestoConnection from '../jsons/connectionSchemas/connections/database/prestoConnection.json'; -// import questdbConnection from '../jsons/connectionSchemas/connections/database/questdbConnection.json'; +import questdbConnection from '../jsons/connectionSchemas/connections/database/questdbConnection.json'; import redshiftConnection from '../jsons/connectionSchemas/connections/database/redshiftConnection.json'; import salesforceConnection from '../jsons/connectionSchemas/connections/database/salesforceConnection.json'; import sapErpConnection from '../jsons/connectionSchemas/connections/database/sapErpConnection.json'; @@ -201,7 +201,7 @@ export const getDatabaseConfig = (type: DatabaseServiceType) => { break; } case DatabaseServiceType.QuestDB: { - // schema = questdbConnection; + schema = questdbConnection; break; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts index 97dbed457099..502195ec6130 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts @@ -47,7 +47,7 @@ type TagWidgetKeys = | DetailPageWidgetKeys.DOMAIN; class TagClassBase { - static filterClassification: string[] = [] + static filterClassification: string[] = []; defaultWidgetHeight: Record; constructor() { From 6ec6f61e656bd14a07ca26136de9e4b5959b801e Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 19 May 2026 00:50:07 +0530 Subject: [PATCH 03/17] icon fix --- .../ContextCenter/DocumentsView/DocumentsView.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 4d8757d9c8d5..72cbcac4a674 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -20,7 +20,7 @@ import { TooltipTrigger, Typography, } from '@openmetadata/ui-core-components'; -import { Download01, Share06, Trash01 } from '@untitledui/icons'; +import { Download01, Pin02, Share06, Trash01 } from '@untitledui/icons'; import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { FileTypeBadge } from 'utils/ContextCenterUtils'; @@ -73,7 +73,7 @@ const FileActions: FC = ({ /> From 9b73d4b43bea1b7cc357480364975a8297eb611d Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 19 May 2026 16:37:14 +0530 Subject: [PATCH 04/17] fixed playwright test --- .../e2e/Features/ContextCenter.spec.ts | 10 +++++----- .../e2e/Features/KnowledgeCenterList.spec.ts | 2 +- .../ArticleDetailHeader.component.tsx | 2 -- .../ArticleDetailHeader.interface.ts | 1 - .../DocumentsView/DocumentFolderView.test.tsx | 2 +- .../MoveToFolderModal.test.tsx | 5 +++++ .../UploadDocumentModal.test.tsx | 18 +++++++++--------- .../ContextCenterArticlesPage.tsx | 1 - .../ContextCenterDocumentsPage.tsx | 4 ++-- .../ui/src/utils/ContextCenterUtils.tsx | 4 ++-- 10 files changed, 25 insertions(+), 24 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts index 530ec38f5d57..4b0fb74cb283 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts @@ -922,7 +922,7 @@ test.describe('Context Center', () => { await page.getByTestId('delete-btn').click(); const apiDeleteRes = page.waitForResponse( - /\/api\/v1\/contextCenter\/pages\/.+\?hardDelete=true/ + '/api/v1/contextCenter/pages/*?hardDelete=false&recursive=true' ); await page.getByTestId('confirm-button').click(); await apiDeleteRes; @@ -1009,7 +1009,7 @@ test.describe('Context Center', () => { await expect(modal.getByText('test-upload.txt').first()).toBeVisible(); // Attach the file - const uploadRes = page.waitForResponse('/api/v1/attachments/upload'); + const uploadRes = page.waitForResponse('/api/v1/contextCenter/drive/files/upload'); await modal.getByRole('button', { name: /attach/i }).click(); // Progress bar / uploading state @@ -1060,9 +1060,9 @@ test.describe('Context Center', () => { await expect(firstRow).toBeVisible(); - // Listen for the download API call — download triggers /api/v1/assets/:id/download + // Listen for the download API call const downloadRes = page.waitForResponse( - /\/api\/v1\/attachments\/[^/]+\/download(?:\?.*)?$/ + /\/api\/v1\/contextCenter\/drive\/files\/[^/]+\/download(?:\?.*)?$/ ); await firstRow.locator('button').nth(0).click(); const res = await downloadRes; @@ -1094,7 +1094,7 @@ test.describe('Context Center', () => { await expect(deleteModal).toBeVisible(); const deleteRes = page.waitForResponse( - /\/api\/v1\/attachments\/[^?]+\?hardDelete=true/ + /\/api\/v1\/contextCenter\/drive\/files\/[^?]+\?hardDelete=false/ ); await page.getByTestId('confirm-button').click(); const res = await deleteRes; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterList.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterList.spec.ts index 5df7d07c161e..c0ac7cf860a0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterList.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterList.spec.ts @@ -250,7 +250,7 @@ test.describe('Knowledge Center List', () => { const paginationResponse = page.waitForResponse( (response) => response.url().includes('/api/v1/contextCenter/pages') && - response.url().includes('after=') + response.url().includes('offset=') ); await observerElement.scrollIntoViewIfNeeded(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx index 03c2dfd922c9..002eae9500f7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx @@ -85,7 +85,6 @@ const ArticleDetailHeader: FC = ({ onToggleRightPanel, onVoteChange, onFollowChange, - onToggleDelete, onSave, onSetThreadLink, fetchKnowledgePageHierarchy, @@ -183,7 +182,6 @@ const ArticleDetailHeader: FC = ({ ); await fetchKnowledgePageHierarchy?.(true); setIsDeleteModalOpen(false); - onToggleDelete(); navigate(contextCenterClassBase.getArticlesListPath()); } catch (error) { showErrorToast(error as AxiosError); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.interface.ts index 5b2bf98beb89..f88cb178fbcf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.interface.ts @@ -33,7 +33,6 @@ export interface ArticleDetailHeaderProps { onToggleRightPanel: () => void; onVoteChange: (type: VotingDataProps) => Promise; onFollowChange: () => Promise; - onToggleDelete: () => void; onSave?: () => void; onSetThreadLink: (link: string) => void; fetchKnowledgePageHierarchy?: (forceRefresh?: boolean) => Promise; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx index 35e3af0861b4..2d139779e916 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.test.tsx @@ -207,7 +207,7 @@ describe('DocumentFolderView', () => { expect(screen.getByText('Folder One')).toBeInTheDocument() ); - expect(screen.getByText(/2/)).toBeInTheDocument(); + expect(screen.getAllByText(/2/).length).toBeGreaterThanOrEqual(1); }); it('calls onSelectFolder with folderId when a folder is clicked', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx index d1b9bbb0d927..43b8a3dc9964 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx @@ -26,6 +26,11 @@ jest.mock('rest/assetAPI', () => ({ moveFileToFolder: jest.fn(), })); +jest.mock('utils/ToastUtils', () => ({ + showSuccessToast: jest.fn(), + showErrorToast: jest.fn(), +})); + let mockOnSelectionChange: ((key: React.Key) => void) | undefined; jest.mock('@openmetadata/ui-core-components', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx index 4b39bb4c5516..5f42e1b2fb4a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx @@ -18,11 +18,11 @@ import { screen, waitFor, } from '@testing-library/react'; -import { uploadAsset } from '../../../rest/assetAPI'; +import { uploadDriveFile } from '../../../rest/assetAPI'; import UploadDocumentModal from './UploadDocumentModal.component'; jest.mock('rest/assetAPI', () => ({ - uploadAsset: jest.fn(), + uploadDriveFile: jest.fn(), })); let mockOnDropFiles: ((files: FileList) => void) | undefined; @@ -264,9 +264,9 @@ describe('UploadDocumentModal', () => { expect(defaultProps.onClose).toHaveBeenCalled(); }); - it('calls uploadAsset and onUploaded when attach is clicked', async () => { + it('calls uploadDriveFile and onUploaded when attach is clicked', async () => { const mockAsset = { id: 'asset-1', name: 'test.pdf' }; - (uploadAsset as jest.Mock).mockResolvedValue(mockAsset); + (uploadDriveFile as jest.Mock).mockResolvedValue(mockAsset); render(); @@ -276,7 +276,7 @@ describe('UploadDocumentModal', () => { fireEvent.click(screen.getByText(/attach-file-plural/i)); - await waitFor(() => expect(uploadAsset).toHaveBeenCalled()); + await waitFor(() => expect(uploadDriveFile).toHaveBeenCalled()); expect( await screen.findByTestId('progress-bar-test.pdf') @@ -296,7 +296,7 @@ describe('UploadDocumentModal', () => { }); it('shows the failed state in the progress bar on upload error', async () => { - (uploadAsset as jest.Mock).mockRejectedValue(new Error('upload failed')); + (uploadDriveFile as jest.Mock).mockRejectedValue(new Error('upload failed')); render(); @@ -312,7 +312,7 @@ describe('UploadDocumentModal', () => { }); it('shows retry button for failed uploads', async () => { - (uploadAsset as jest.Mock).mockRejectedValue(new Error('upload failed')); + (uploadDriveFile as jest.Mock).mockRejectedValue(new Error('upload failed')); render(); @@ -327,7 +327,7 @@ describe('UploadDocumentModal', () => { it('retries a failed upload when the retry button is clicked', async () => { const mockAsset = { id: 'asset-retry', name: 'fail.pdf' }; - (uploadAsset as jest.Mock) + (uploadDriveFile as jest.Mock) .mockRejectedValueOnce(new Error('first attempt failed')) .mockResolvedValueOnce(mockAsset); @@ -342,7 +342,7 @@ describe('UploadDocumentModal', () => { const retryBtn = await screen.findByTestId('retry-fail.pdf'); fireEvent.click(retryBtn); - await waitFor(() => expect(uploadAsset).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(uploadDriveFile).toHaveBeenCalledTimes(2)); await waitFor(() => expect(defaultProps.onUploaded).toHaveBeenCalledWith([mockAsset]) ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx index 5367958b9c12..189b6afc41fe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArticlesPage/ContextCenterArticlesPage.tsx @@ -177,7 +177,6 @@ const ContextCenterArticlesPage = () => { onSave={page.handlers?.onSave} onSetThreadLink={page.handlers?.onSetThreadLink ?? (() => undefined)} onTabChange={page.onTabChange} - onToggleDelete={page.handlers?.onToggleDelete ?? (() => undefined)} onToggleRightPanel={handleToggleRightPanel} onVoteChange={page.handlers?.onVoteChange ?? (async () => undefined)} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index 0c9073a24047..4278d338f2dc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -34,7 +34,7 @@ import { SearchIndex } from '../../../enums/search.enum'; import { ContextFile } from '../../../generated/entity/data/contextFile'; import { Folder } from '../../../generated/entity/data/folder'; import { useAlertStore } from '../../../hooks/useAlertStore'; -import { deleteAsset, listContextFiles } from '../../../rest/assetAPI'; +import { deleteDriveFile, listContextFiles } from '../../../rest/assetAPI'; import { searchQuery as fetchSearchResults } from '../../../rest/searchAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; import { @@ -145,7 +145,7 @@ const ContextCenterDocumentsPage: FC = () => { try { setIsDeletingFile(true); - await deleteAsset(fileToDelete.id, false); + await deleteDriveFile(fileToDelete.driveFileId ?? fileToDelete.id, false); setAllDocuments((prev) => prev.filter((document) => document.id !== fileToDelete.id) ); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx index 557728e92a25..7749e4f815ae 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx @@ -34,7 +34,7 @@ import { QuickLink, } from '../interface/knowledge-center.interface'; import { - downloadAsset, + downloadDriveFile, listAssetsByFqn, ListAssetsByFqnParams, } from '../rest/assetAPI'; @@ -218,7 +218,7 @@ export const handleAssetDownload = async (file: DocFile) => { let element: HTMLAnchorElement | undefined; try { - const blob = await downloadAsset(file.id); + const blob = await downloadDriveFile(file.driveFileId ?? file.id); url = URL.createObjectURL(blob); element = document.createElement('a'); element.href = url; From 206bcd36d269ab33dcf6eb8423e1f20fcc850bbf Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Tue, 19 May 2026 17:06:27 +0530 Subject: [PATCH 05/17] fixed upload folder UI and lint fix --- .../e2e/Features/ContextCenter.spec.ts | 8 +- .../CreateFolderModal.component.tsx | 26 +- .../DocumentsView/DocumentsView.component.tsx | 138 ++++++-- .../DocumentsView/DocumentsView.interface.ts | 8 +- .../MoveToFolderModal.component.tsx | 164 --------- .../MoveToFolderModal.test.tsx | 311 ------------------ .../UploadDocumentModal.test.tsx | 8 +- .../ui/src/locale/languages/ar-sa.json | 1 + .../ui/src/locale/languages/de-de.json | 1 + .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/locale/languages/es-es.json | 1 + .../ui/src/locale/languages/fr-fr.json | 1 + .../ui/src/locale/languages/gl-es.json | 1 + .../ui/src/locale/languages/he-he.json | 1 + .../ui/src/locale/languages/ja-jp.json | 1 + .../ui/src/locale/languages/ko-kr.json | 1 + .../ui/src/locale/languages/mr-in.json | 1 + .../ui/src/locale/languages/nl-nl.json | 1 + .../ui/src/locale/languages/pr-pr.json | 1 + .../ui/src/locale/languages/pt-br.json | 1 + .../ui/src/locale/languages/pt-pt.json | 1 + .../ui/src/locale/languages/ru-ru.json | 1 + .../ui/src/locale/languages/th-th.json | 1 + .../ui/src/locale/languages/tr-tr.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + .../ui/src/locale/languages/zh-tw.json | 1 + .../ContextCenterDocumentsPage.tsx | 34 +- 27 files changed, 187 insertions(+), 529 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts index 4b0fb74cb283..608b8956c9f4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts @@ -876,7 +876,7 @@ test.describe('Context Center', () => { await card.getByTestId('delete-quick-link-btn').click(); const deleteRes = page.waitForResponse( - '/api/v1/contextCenter/pages/*?hardDelete=true&recursive=false' + /\/api\/v1\/contextCenter\/pages\/[^?]+\?hardDelete=true&recursive=false/ ); await page.getByTestId('confirm-button').click(); const res = await deleteRes; @@ -922,7 +922,7 @@ test.describe('Context Center', () => { await page.getByTestId('delete-btn').click(); const apiDeleteRes = page.waitForResponse( - '/api/v1/contextCenter/pages/*?hardDelete=false&recursive=true' + /\/api\/v1\/contextCenter\/pages\/[^?]+\?hardDelete=false&recursive=true/ ); await page.getByTestId('confirm-button').click(); await apiDeleteRes; @@ -1009,7 +1009,9 @@ test.describe('Context Center', () => { await expect(modal.getByText('test-upload.txt').first()).toBeVisible(); // Attach the file - const uploadRes = page.waitForResponse('/api/v1/contextCenter/drive/files/upload'); + const uploadRes = page.waitForResponse( + '/api/v1/contextCenter/drive/files/upload' + ); await modal.getByRole('button', { name: /attach/i }).click(); // Progress bar / uploading state diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx index da22370ad50a..28515b462503 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx @@ -19,6 +19,7 @@ import { ModalOverlay, Typography, } from '@openmetadata/ui-core-components'; +import { FolderPlus } from '@untitledui/icons'; import { AxiosError } from 'axios'; import { Folder } from 'generated/entity/data/folder'; import { FC, useState } from 'react'; @@ -74,14 +75,23 @@ const CreateFolderModal: FC = ({ isOpen={isOpen} onOpenChange={(open) => !open && !isCreating && handleClose()}> - + +
+ +
+ + {t('label.create-new-folder')} +
- + {t('label.entity-name', { entity: t('label.folder') })} = ({ />
-
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 72cbcac4a674..adf3e77abc7f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -20,31 +20,70 @@ import { TooltipTrigger, Typography, } from '@openmetadata/ui-core-components'; -import { Download01, Pin02, Share06, Trash01 } from '@untitledui/icons'; -import { FC } from 'react'; +import { + ChevronRight, + Download01, + Pin02, + Share06, + Trash01, +} from '@untitledui/icons'; +import { AxiosError } from 'axios'; +import { FC, useState } from 'react'; +import { + Menu as AriaMenu, + MenuItem as AriaMenuItem, + Popover as AriaPopover, + SubmenuTrigger, +} from 'react-aria-components'; import { useTranslation } from 'react-i18next'; -import { FileTypeBadge } from 'utils/ContextCenterUtils'; import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; +import { moveFileToFolder } from '../../../rest/assetAPI'; +import { FileTypeBadge } from '../../../utils/ContextCenterUtils'; import { getShortRelativeTime } from '../../../utils/date-time/DateTimeUtils'; -import { DocFile, DocumentsViewProps } from './DocumentsView.interface'; +import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; +import { + DocFile, + DocumentsViewProps, + FolderOption, +} from './DocumentsView.interface'; interface FileActionsProps { canDelete?: boolean; file: DocFile; + folders?: FolderOption[]; onShareFile?: (file: DocFile) => void; onDeleteFile?: (file: DocFile) => void; - onMoveFile?: (file: DocFile) => void; + onFileMoved?: (file: DocFile, targetFolderId: string) => void; } const FileActions: FC = ({ canDelete, file, + folders = [], onDeleteFile, - onMoveFile, + onFileMoved, onShareFile, }) => { const { t } = useTranslation(); + const [isMoving, setIsMoving] = useState(false); + + const availableFolders = folders.filter((f) => f.id !== file.folderId); + + const handleMoveToFolder = async (folderId: string) => { + try { + setIsMoving(true); + await moveFileToFolder(file.driveFileId ?? file.id, folderId); + onFileMoved?.(file, folderId); + showSuccessToast( + t('message.entity-moved-successfully', { entity: t('label.document') }) + ); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsMoving(false); + } + }; return ( @@ -59,10 +98,6 @@ const FileActions: FC = ({ onAction={(key) => { if (key === 'share') { onShareFile?.(file); - } else if (key === 'move') { - onMoveFile?.(file); - } else if (key === 'delete') { - onDeleteFile?.(file); } }}> = ({ id="share" label={t('label.share-file')} /> - + + + + `tw:group tw:block tw:cursor-pointer tw:px-1.5 tw:py-px tw:outline-hidden${ + state.isDisabled ? ' tw:cursor-not-allowed tw:opacity-50' : '' + }` + } + data-testid="move-btn" + isDisabled={isMoving || availableFolders.length === 0}> + {() => ( +
+
+ )} +
+ + + {availableFolders.map((folder) => ( + + `tw:group tw:block tw:cursor-pointer tw:px-1.5 tw:py-px tw:outline-hidden${ + state.isDisabled ? ' tw:cursor-not-allowed' : '' + }` + } + data-testid={`move-to-folder-${folder.id}`} + id={folder.id} + key={folder.id} + textValue={folder.name} + onAction={() => handleMoveToFolder(folder.id)}> + {() => ( +
+ + {folder.name} + +
+ )} +
+ ))} +
+
+
+ {canDelete && (
@@ -126,18 +217,20 @@ const FileRowSkeleton: FC = () => ( interface FileRowProps { canDelete?: boolean; file: DocFile; + folders?: FolderOption[]; onDownload?: (file: DocFile) => void; onShareFile?: (file: DocFile) => void; onDeleteFile?: (file: DocFile) => void; - onMoveFile?: (file: DocFile) => void; + onFileMoved?: (file: DocFile, targetFolderId: string) => void; } const FileRow: FC = ({ canDelete, file, + folders, onDeleteFile, onDownload, - onMoveFile, + onFileMoved, onShareFile, }) => { const { t } = useTranslation(); @@ -198,8 +291,9 @@ const FileRow: FC = ({
@@ -213,10 +307,11 @@ const DocumentViewLoading = () => const DocumentsView: FC = ({ canDelete, data, + folders, isLoading, onDeleteFile, onDownload, - onMoveFile, + onFileMoved, onShareFile, }) => { return ( @@ -232,10 +327,11 @@ const DocumentsView: FC = ({ )) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts index e548f854908b..86ac32adafd2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.interface.ts @@ -31,12 +31,18 @@ export interface DocFolder { files: DocFile[]; } +export interface FolderOption { + id: string; + name: string; +} + export interface DocumentsViewProps { canDelete?: boolean; data: DocFile[]; + folders?: FolderOption[]; isLoading: boolean; onDownload?: (file: DocFile) => void; onShareFile?: (file: DocFile) => void; onDeleteFile?: (file: DocFile) => void; - onMoveFile?: (file: DocFile) => void; + onFileMoved?: (file: DocFile, targetFolderId: string) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx deleted file mode 100644 index 9e1529ad81a3..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2026 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Button, - Dialog, - Modal, - ModalOverlay, - Select, - Typography, -} from '@openmetadata/ui-core-components'; -import { ChevronRight } from '@untitledui/icons'; -import { AxiosError } from 'axios'; -import { Folder } from 'generated/entity/data/folder'; -import { FC, Key, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { FileTypeBadge } from 'utils/ContextCenterUtils'; -import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; -import { moveFileToFolder } from '../../../rest/assetAPI'; -import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; -import { DocFile } from '../DocumentsView/DocumentsView.interface'; - -export interface MoveToFolderModalProps { - file: DocFile; - folders: Folder[]; - isOpen: boolean; - onClose: () => void; - onMoved: (file: DocFile, targetFolderId: string) => void; -} - -const MoveToFolderModal: FC = ({ - file, - folders, - isOpen, - onClose, - onMoved, -}) => { - const { t } = useTranslation(); - const [selectedFolderId, setSelectedFolderId] = useState(''); - const [isMoving, setIsMoving] = useState(false); - - const currentFolderName = file.folderId - ? folders.find((f) => f.id === file.folderId)?.displayName ?? - folders.find((f) => f.id === file.folderId)?.name - : undefined; - - const availableFolders = folders.filter((f) => f.id !== file.folderId); - - const handleClose = () => { - setSelectedFolderId(''); - onClose(); - }; - - const handleSave = async () => { - if (!selectedFolderId) { - return; - } - - try { - setIsMoving(true); - await moveFileToFolder(file.driveFileId ?? file.id, selectedFolderId); - onMoved(file, selectedFolderId); - showSuccessToast( - t('message.entity-moved-successfully', { entity: t('label.document') }) - ); - handleClose(); - } catch (err) { - showErrorToast(err as AxiosError); - } finally { - setIsMoving(false); - } - }; - - return ( - !open && !isMoving && handleClose()}> - - - -
- - - {file.name} - -
- -
- - - {currentFolderName ?? t('label.no-folder')} - - -
- -
-
- -
- - -
-
-
-
-
- ); -}; - -export default MoveToFolderModal; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx deleted file mode 100644 index 43b8a3dc9964..000000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/MoveToFolderModal/MoveToFolderModal.test.tsx +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright 2026 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; -import { moveFileToFolder } from 'rest/assetAPI'; -import { DocFile } from '../DocumentsView/DocumentsView.interface'; -import MoveToFolderModal from './MoveToFolderModal.component'; - -jest.mock('rest/assetAPI', () => ({ - moveFileToFolder: jest.fn(), -})); - -jest.mock('utils/ToastUtils', () => ({ - showSuccessToast: jest.fn(), - showErrorToast: jest.fn(), -})); - -let mockOnSelectionChange: ((key: React.Key) => void) | undefined; - -jest.mock('@openmetadata/ui-core-components', () => ({ - Button: jest.fn( - ({ - children, - onPress, - isDisabled, - 'data-testid': testId, - }: { - children: React.ReactNode; - onPress?: () => void; - isDisabled?: boolean; - 'data-testid'?: string; - }) => ( - - ) - ), - Dialog: Object.assign( - jest.fn( - ({ - children, - title, - onClose, - }: { - children: React.ReactNode; - title: string; - onClose: () => void; - }) => ( -
- {title} - - {children} -
- ) - ), - { - Content: jest.fn(({ children }: { children: React.ReactNode }) => ( -
{children}
- )), - } - ), - Modal: jest.fn(({ children }: { children: React.ReactNode }) => ( -
{children}
- )), - ModalOverlay: jest.fn( - ({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) => - isOpen ?
{children}
: null - ), - Select: Object.assign( - jest.fn( - ({ - children, - onSelectionChange, - 'data-testid': testId, - }: { - children: React.ReactNode; - onSelectionChange?: (key: React.Key) => void; - 'data-testid'?: string; - }) => { - mockOnSelectionChange = onSelectionChange; - - return
{children}
; - } - ), - { - Item: jest.fn(({ id, label }: { id: string; label: string }) => ( - - )), - } - ), - Typography: jest.fn(({ children }: { children: React.ReactNode }) => ( - {children} - )), -})); - -jest.mock('utils/ContextCenterUtils', () => ({ - FileTypeBadge: jest.fn(({ fileType }: { fileType: string }) => ( - {fileType} - )), -})); - -const mockFile: DocFile = { - id: 'asset-1', - driveFileId: 'drive-1', - name: 'report.pdf', - fileType: 'pdf', - sizeLabel: '2 MB', - folderId: 'folder-a', -}; - -const mockFolders = [ - { id: 'folder-a', name: 'folder-a', displayName: 'Folder A' }, - { id: 'folder-b', name: 'folder-b', displayName: 'Folder B' }, - { id: 'folder-c', name: 'folder-c', displayName: 'Folder C' }, -]; - -const defaultProps = { - file: mockFile, - folders: mockFolders, - isOpen: true, - onClose: jest.fn(), - onMoved: jest.fn(), -}; - -describe('MoveToFolderModal', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockOnSelectionChange = undefined; - }); - - it('renders when isOpen is true', () => { - render(); - - expect(screen.getByTestId('modal-overlay')).toBeInTheDocument(); - expect(screen.getByTestId('dialog')).toBeInTheDocument(); - }); - - it('does not render when isOpen is false', () => { - render(); - - expect(screen.queryByTestId('modal-overlay')).not.toBeInTheDocument(); - }); - - it('renders the file name', () => { - render(); - - expect(screen.getByText('report.pdf')).toBeInTheDocument(); - }); - - it('renders the current folder name', () => { - render(); - - expect(screen.getByText('Folder A')).toBeInTheDocument(); - }); - - it('shows "no-folder" when file has no folderId', () => { - render( - - ); - - expect(screen.getByText(/no-folder/i)).toBeInTheDocument(); - }); - - it('excludes the current folder from select options', () => { - render(); - - expect( - screen.queryByTestId('select-item-folder-a') - ).not.toBeInTheDocument(); - expect(screen.getByTestId('select-item-folder-b')).toBeInTheDocument(); - expect(screen.getByTestId('select-item-folder-c')).toBeInTheDocument(); - }); - - it('save button is disabled when no folder is selected', () => { - render(); - - expect(screen.getByTestId('move-folder-save-btn')).toBeDisabled(); - }); - - it('save button is enabled after selecting a folder', () => { - render(); - - act(() => { - mockOnSelectionChange?.('folder-b'); - }); - - expect(screen.getByTestId('move-folder-save-btn')).not.toBeDisabled(); - }); - - it('calls moveFileToFolder with driveFileId and selected folder on save', async () => { - (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); - - render(); - - act(() => { - mockOnSelectionChange?.('folder-b'); - }); - - await act(async () => { - fireEvent.click(screen.getByTestId('move-folder-save-btn')); - }); - - expect(moveFileToFolder).toHaveBeenCalledWith('drive-1', 'folder-b'); - }); - - it('falls back to file.id when driveFileId is undefined', async () => { - (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); - - render( - - ); - - act(() => { - mockOnSelectionChange?.('folder-b'); - }); - - await act(async () => { - fireEvent.click(screen.getByTestId('move-folder-save-btn')); - }); - - expect(moveFileToFolder).toHaveBeenCalledWith('asset-1', 'folder-b'); - }); - - it('calls onMoved with file and targetFolderId on success', async () => { - (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); - - render(); - - act(() => { - mockOnSelectionChange?.('folder-b'); - }); - - await act(async () => { - fireEvent.click(screen.getByTestId('move-folder-save-btn')); - }); - - await waitFor(() => - expect(defaultProps.onMoved).toHaveBeenCalledWith(mockFile, 'folder-b') - ); - }); - - it('calls onClose after successful move', async () => { - (moveFileToFolder as jest.Mock).mockResolvedValue(undefined); - - render(); - - act(() => { - mockOnSelectionChange?.('folder-b'); - }); - - await act(async () => { - fireEvent.click(screen.getByTestId('move-folder-save-btn')); - }); - - await waitFor(() => expect(defaultProps.onClose).toHaveBeenCalled()); - }); - - it('calls onClose when cancel button is clicked', () => { - render(); - - fireEvent.click(screen.getByText(/cancel/i)); - - expect(defaultProps.onClose).toHaveBeenCalled(); - }); - - it('calls onClose when dialog close button is clicked', () => { - render(); - - fireEvent.click(screen.getByTestId('dialog-close')); - - expect(defaultProps.onClose).toHaveBeenCalled(); - }); - - it('does not call moveFileToFolder when no folder selected', async () => { - render(); - - await act(async () => { - fireEvent.click(screen.getByTestId('move-folder-save-btn')); - }); - - expect(moveFileToFolder).not.toHaveBeenCalled(); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx index 5f42e1b2fb4a..a01ec1c8f41e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx @@ -296,7 +296,9 @@ describe('UploadDocumentModal', () => { }); it('shows the failed state in the progress bar on upload error', async () => { - (uploadDriveFile as jest.Mock).mockRejectedValue(new Error('upload failed')); + (uploadDriveFile as jest.Mock).mockRejectedValue( + new Error('upload failed') + ); render(); @@ -312,7 +314,9 @@ describe('UploadDocumentModal', () => { }); it('shows retry button for failed uploads', async () => { - (uploadDriveFile as jest.Mock).mockRejectedValue(new Error('upload failed')); + (uploadDriveFile as jest.Mock).mockRejectedValue( + new Error('upload failed') + ); render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json index 0e0a8966fad4..0f674cca21ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json @@ -443,6 +443,7 @@ "create-entity": "إنشاء {{entity}}", "create-lowercase": "إنشاء", "create-new-bundle-suite": "إنشاء مجموعة حزمة جديدة", + "create-new-folder": "إنشاء مجلد جديد", "create-new-test-suite": "إنشاء مجموعة اختبار جديدة", "create-new-workflow": "إنشاء سير عمل جديد", "create-widget": "إنشاء أداة", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 7be1e49c89e7..8766b5405a8d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -443,6 +443,7 @@ "create-entity": "{{entity}} erstellen", "create-lowercase": "erstellen", "create-new-bundle-suite": "Neue Bundle-Suite erstellen", + "create-new-folder": "Neuen Ordner erstellen", "create-new-test-suite": "Neuen Testsuite erstellen", "create-new-workflow": "Neuen Workflow erstellen", "create-widget": "Widget erstellen", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 2f0763586d3b..643410249e8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -443,6 +443,7 @@ "create-entity": "Create {{entity}}", "create-lowercase": "create", "create-new-bundle-suite": "Create new Bundle Suite", + "create-new-folder": "Create New Folder", "create-new-test-suite": "Create new test suite", "create-new-workflow": "Create New Workflow", "create-widget": "Create Widget", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 3f3ebaca028b..198840fd2141 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -443,6 +443,7 @@ "create-entity": "Crear {{entity}}", "create-lowercase": "crear", "create-new-bundle-suite": "Crear nueva Bundle Suite", + "create-new-folder": "Crear nueva carpeta", "create-new-test-suite": "Crear nueva suite de tests", "create-new-workflow": "Crear nuevo flujo de trabajo", "create-widget": "Crear Widget", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 9521ce41a8d9..ce9fd991133e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -443,6 +443,7 @@ "create-entity": "Créer {{entity}}", "create-lowercase": "créer", "create-new-bundle-suite": "Créer une nouvelle Suite de Bundle", + "create-new-folder": "Créer un nouveau dossier", "create-new-test-suite": "Créer un Nouvel Ensemble de Tests", "create-new-workflow": "Créer un nouveau workflow", "create-widget": "Créer un Widget", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index 3f2e22223aeb..ff7847ad136c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -443,6 +443,7 @@ "create-entity": "Crear {{entity}}", "create-lowercase": "crear", "create-new-bundle-suite": "Crear nova Suite de Bundle", + "create-new-folder": "Crear novo cartafol", "create-new-test-suite": "Crear un novo conxunto de probas", "create-new-workflow": "Crear novo fluxo de traballo", "create-widget": "Crear Widget", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index edf3fc0f010d..a1bec9e3b5b7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -443,6 +443,7 @@ "create-entity": "צור {{entity}}", "create-lowercase": "ליצור", "create-new-bundle-suite": "צור Bundle Suite חדש", + "create-new-folder": "צור תיקייה חדשה", "create-new-test-suite": "צור מערכת בדיקה חדשה", "create-new-workflow": "יצירת תהליך עבודה חדש", "create-widget": "צור ווידג'ט", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 12dc9e16a0b6..ae557271dd69 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -443,6 +443,7 @@ "create-entity": "{{entity}}を作成", "create-lowercase": "作成", "create-new-bundle-suite": "新しいBundle Suiteを作成", + "create-new-folder": "新しいフォルダを作成", "create-new-test-suite": "新しいテストスイートを作成", "create-new-workflow": "新規ワークフローを作成", "create-widget": "ウィジェットを作成", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index a29f39f85224..87755ace93ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -443,6 +443,7 @@ "create-entity": "{{entity}} 생성", "create-lowercase": "생성", "create-new-bundle-suite": "새 Bundle Suite 만들기", + "create-new-folder": "새 폴더 만들기", "create-new-test-suite": "새 테스트 스위트 생성", "create-new-workflow": "새 워크플로 만들기", "create-widget": "위젯 생성", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index a30e068cd225..39ec173fee22 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -443,6 +443,7 @@ "create-entity": "{{entity}} तयार करा", "create-lowercase": "तयार करा", "create-new-bundle-suite": "नवीन बंडल सूट तयार करा", + "create-new-folder": "नवीन फोल्डर तयार करा", "create-new-test-suite": "नवीन चाचणी संच तयार करा", "create-new-workflow": "नवीन वर्कफ्लो तयार करा", "create-widget": "विजेट तयार करा", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 6c90f8f8a00a..721419820f10 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -443,6 +443,7 @@ "create-entity": "{{entity}} maken", "create-lowercase": "maken", "create-new-bundle-suite": "Nieuwe Bundle Suite maken", + "create-new-folder": "Nieuwe map aanmaken", "create-new-test-suite": "Nieuwe testsuite maken", "create-new-workflow": "Nieuwe workflow maken", "create-widget": "Widget maken", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index 7332c937f105..bccca43b84c5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -443,6 +443,7 @@ "create-entity": "ایجاد {{entity}}", "create-lowercase": "ایجاد", "create-new-bundle-suite": "ایجاد مجموعه باندل جدید", + "create-new-folder": "Criar nova pasta", "create-new-test-suite": "ایجاد مجموعه تست جدید", "create-new-workflow": "ایجاد جریان کاری جدید", "create-widget": "ایجاد ویجت", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index b8484a36bbbb..56ea2c12fd8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -443,6 +443,7 @@ "create-entity": "Criar {{entity}}", "create-lowercase": "criar", "create-new-bundle-suite": "Criar novo Bundle Suite", + "create-new-folder": "Criar nova pasta", "create-new-test-suite": "Criar novo conjunto de teste", "create-new-workflow": "Criar novo fluxo de trabalho", "create-widget": "Criar widget", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index 173a2668ebde..56555cf9fbc1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -443,6 +443,7 @@ "create-entity": "Criar {{entity}}", "create-lowercase": "criar", "create-new-bundle-suite": "Criar novo Bundle Suite", + "create-new-folder": "Criar nova pasta", "create-new-test-suite": "Criar novo conjunto de teste", "create-new-workflow": "Criar novo fluxo de trabalho", "create-widget": "Criar widget", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index a5708585eb68..8c4c3e949aa0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -443,6 +443,7 @@ "create-entity": "Создать объект «{{entity}}»", "create-lowercase": "создать", "create-new-bundle-suite": "Создать новый Bundle Suite", + "create-new-folder": "Создать новую папку", "create-new-test-suite": "Создать новый набор тестов", "create-new-workflow": "Создать новый рабочий процесс", "create-widget": "Создать виджет", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index c397ab68fc8c..d598c79a396d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -443,6 +443,7 @@ "create-entity": "สร้าง {{entity}}", "create-lowercase": "สร้าง", "create-new-bundle-suite": "สร้าง Bundle Suite ใหม่", + "create-new-folder": "สร้างโฟลเดอร์ใหม่", "create-new-test-suite": "สร้างชุดทดสอบใหม่", "create-new-workflow": "สร้างเวิร์กโฟลว์ใหม่", "create-widget": "สร้างวิดเจ็ต", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json index ba4fd404ac28..cb72461881f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json @@ -443,6 +443,7 @@ "create-entity": "{{entity}} Oluştur", "create-lowercase": "oluştur", "create-new-bundle-suite": "Yeni Bundle Suite oluştur", + "create-new-folder": "Yeni Klasör Oluştur", "create-new-test-suite": "Yeni test paketi oluştur", "create-new-workflow": "Yeni İş Akışı Oluştur", "create-widget": "Widget oluştur", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 0ce3eb0b8f99..3e5290a11f20 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -443,6 +443,7 @@ "create-entity": "新建{{entity}}", "create-lowercase": "创建", "create-new-bundle-suite": "创建新的Bundle Suite", + "create-new-folder": "创建新文件夹", "create-new-test-suite": "创建新的质控测试", "create-new-workflow": "创建新工作流", "create-widget": "创建小组件", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json index 7964fd609780..98c9eca9fce4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json @@ -443,6 +443,7 @@ "create-entity": "建立 {{entity}}", "create-lowercase": "建立", "create-new-bundle-suite": "建立新的 Bundle Suite", + "create-new-folder": "建立新資料夾", "create-new-test-suite": "建立新測試套件", "create-new-workflow": "建立新工作流程", "create-widget": "建立小工具", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index 4278d338f2dc..adfd68f7ab9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -22,8 +22,10 @@ import DeleteModal from '../../../components/common/DeleteModal/DeleteModal'; import '../../../components/common/ResizablePanels/resizable-panels.less'; import ContextCenterHeader from '../../../components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component'; import DocumentsView from '../../../components/ContextCenter/DocumentsView/DocumentsView.component'; -import { DocFile } from '../../../components/ContextCenter/DocumentsView/DocumentsView.interface'; -import MoveToFolderModal from '../../../components/ContextCenter/MoveToFolderModal/MoveToFolderModal.component'; +import { + DocFile, + FolderOption, +} from '../../../components/ContextCenter/DocumentsView/DocumentsView.interface'; import UploadDocumentModal from '../../../components/ContextCenter/UploadDocumentModal/UploadDocumentModal.component'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { @@ -53,7 +55,6 @@ const ContextCenterDocumentsPage: FC = () => { const [documentSearchQuery, setDocumentSearchQuery] = useState(''); const [isDeletingFile, setIsDeletingFile] = useState(false); const [fileToDelete, setFileToDelete] = useState(); - const [fileToMove, setFileToMove] = useState(); const [isUploadModalOpen, setIsUploadModalOpen] = useState(false); const [permissions, setPermissions] = useState( DEFAULT_ENTITY_PERMISSION @@ -77,6 +78,15 @@ const ContextCenterDocumentsPage: FC = () => { [selectedFolderId, folders] ); + const folderOptions = useMemo( + () => + folders.map((f) => ({ + id: f.id, + name: f.displayName ?? f.name, + })), + [folders] + ); + const documents = useMemo(() => { if (!selectedFolderId) { return allDocuments; @@ -162,10 +172,6 @@ const ContextCenterDocumentsPage: FC = () => { } }, [fileToDelete, t]); - const handleMoveFile = useCallback((file: DocFile) => { - setFileToMove(file); - }, []); - const handleFileMoved = useCallback( (file: DocFile, targetFolderId: string) => { setAllDocuments((prev) => @@ -175,7 +181,6 @@ const ContextCenterDocumentsPage: FC = () => { : d ) ); - setFileToMove(undefined); }, [] ); @@ -238,10 +243,11 @@ const ContextCenterDocumentsPage: FC = () => { @@ -270,16 +276,6 @@ const ContextCenterDocumentsPage: FC = () => { onDelete={handleConfirmDelete} /> )} - - {fileToMove && ( - setFileToMove(undefined)} - onMoved={handleFileMoved} - /> - )}
); }; From 8798233b17439b554cc807821006268b4ac6f31b Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Wed, 20 May 2026 12:19:37 +0530 Subject: [PATCH 06/17] fixed playwright test --- .../e2e/Features/ContextCenter.spec.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts index 608b8956c9f4..683631e9cf7d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts @@ -1071,27 +1071,43 @@ test.describe('Context Center', () => { expect(res.status()).toBe(200); }); - test('delete document removes it from the list', async ({ page }) => { + test('delete document removes it from the list', async ({ + browser, + page, + }) => { + const fileName = `delete-doc-${uuid()}.txt`; + + // Upload a dedicated document so this test is independent of the download test + const { apiContext, afterAction } = await createNewPage(browser); + await apiContext.post('/api/v1/contextCenter/drive/files/upload', { + multipart: { + file: { + name: fileName, + mimeType: 'text/plain', + buffer: Buffer.from('document for delete test'), + }, + }, + }); + await afterAction(); + await navigateToDocuments(page); const view = page.getByTestId('documents-view'); - const firstRow = view.locator('[data-testid^="document-row-"]').first(); + const targetRow = view.locator(`[data-testid^="document-row-"]`).filter({ + has: page.getByText(fileName), + }); - // Relies on at least one document existing from prior upload test - await expect(firstRow).toBeVisible(); - await firstRow.scrollIntoViewIfNeeded(); + await expect(targetRow).toBeVisible(); + await targetRow.scrollIntoViewIfNeeded(); - const rowId = await firstRow.getAttribute('data-testid'); + const rowId = await targetRow.getAttribute('data-testid'); - // Open the three-dot actions dropdown (second button in the row, after download) - await firstRow.locator('button[aria-label="Open menu"]').click(); + await targetRow.locator('button[aria-label="Open menu"]').click(); - // Click Delete from the dropdown menu const deleteItem = page.getByTestId('delete-btn'); await expect(deleteItem).toBeVisible(); await deleteItem.click(); - // Confirm via DeleteModal from core-components const deleteModal = page.getByTestId('modal-header'); await expect(deleteModal).toBeVisible(); @@ -1102,7 +1118,6 @@ test.describe('Context Center', () => { const res = await deleteRes; expect(res.status()).toBe(200); - // Row is removed from the list if (rowId) { await expect(page.getByTestId(rowId)).not.toBeVisible(); } From 4abe1ce825ada5f9ed53006d007c8ee384df622b Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Wed, 20 May 2026 14:31:52 +0530 Subject: [PATCH 07/17] added archive page --- .../ContextCenterRouter.tsx | 20 +- .../ArchiveView/ArchiveView.component.tsx | 158 ++++++++++++++ .../ArchiveView/ArchiveView.interface.ts | 29 +++ .../ui/src/constants/LeftSidebar.constants.ts | 16 +- .../ui/src/locale/languages/en-us.json | 4 + .../ContextCenterArchivePage.tsx | 203 +++++++++++++++++- .../main/resources/ui/src/rest/assetAPI.ts | 18 ++ .../ui/src/rest/knowledgeCenterAPI.ts | 20 ++ 8 files changed, 441 insertions(+), 27 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.interface.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx index 88efb2a97b10..fc7433100300 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx @@ -73,14 +73,14 @@ const KnowledgeCenterFilterPage = withSuspenseFallback( // ) // ); -// const ContextCenterArchivePage = withSuspenseFallback( -// React.lazy( -// () => -// import( -// '../../../pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage' -// ) -// ) -// ); +const ContextCenterArchivePage = withSuspenseFallback( + React.lazy( + () => + import( + '../../../pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage' + ) + ) +); const ContextCenterRouter = () => { return ( @@ -131,11 +131,11 @@ const ContextCenterRouter = () => { ROUTES.CONTEXT_CENTER, '' )} - /> + />*/} } path={ROUTES.CONTEXT_CENTER_ARCHIVE.replace(ROUTES.CONTEXT_CENTER, '')} - /> */} + /> ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx new file mode 100644 index 000000000000..9683d9d123fb --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx @@ -0,0 +1,158 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Button, + Card, + Skeleton, + Typography, +} from '@openmetadata/ui-core-components'; +import { File06, RefreshCcw01, Trash01 } from '@untitledui/icons'; +import classNames from 'classnames'; +import { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; +import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; +import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; +import { getShortRelativeTime } from '../../../utils/date-time/DateTimeUtils'; +import { ArchiveItem, ArchiveViewProps } from './ArchiveView.interface'; + +const ArchiveRowSkeleton: FC = () => ( +
+ +
+ + +
+
+ + +
+
+); + +interface ArchiveRowProps { + item: ArchiveItem; + onRestore: (item: ArchiveItem) => void; + onDelete: (item: ArchiveItem) => void; +} + +const ArchiveRow: FC = ({ item, onDelete, onRestore }) => { + const { t } = useTranslation(); + + const Icon = item.type === 'article' ? File06 : FolderIcon; + + return ( +
+
+ +
+ +
+ + {item.name} + + + {item.updatedBy && ( + <> + {t('label.archived-by', { name: item.updatedBy })} + {item.updatedAt && ( + <> · {getShortRelativeTime(item.updatedAt)} + )} + + )} + {!item.updatedBy && + item.updatedAt && + getShortRelativeTime(item.updatedAt)} + +
+ +
+ + +
+
+ ); +}; + +const ArchiveView: FC = ({ + data, + isLoading, + onDelete, + onRestore, +}) => { + if (isLoading) { + return ( + + {Array.from({ length: 8 }).map((_, idx) => ( + + ))} + + ); + } + + if (data.length === 0) { + return ( + + + + ); + } + + return ( +
+ {data.map((item) => ( + + ))} +
+ ); +}; + +export default ArchiveView; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.interface.ts new file mode 100644 index 000000000000..8a978a0aae3c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.interface.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type ArchiveItemType = 'article' | 'document'; + +export interface ArchiveItem { + id: string; + name: string; + type: ArchiveItemType; + updatedBy?: string; + updatedAt?: number; +} + +export interface ArchiveViewProps { + data: ArchiveItem[]; + isLoading: boolean; + onRestore: (item: ArchiveItem) => void; + onDelete: (item: ArchiveItem) => void; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts index b8966519d5c8..9679b1aec0b6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts @@ -11,7 +11,7 @@ * limitations under the License. */ -import { Cube01, File06, Lightbulb03 } from '@untitledui/icons'; +import { Archive, Cube01, File06, Lightbulb03 } from '@untitledui/icons'; import { ReactComponent as GovernIcon } from '../assets/svg/bank.svg'; import { ReactComponent as ClassificationIcon } from '../assets/svg/classification.svg'; import { ReactComponent as DataQualityRulesIcon } from '../assets/svg/data-observability/data-quality-rules.svg'; @@ -251,13 +251,13 @@ export const SIDEBAR_LIST: Array = [ // icon: IntegrationIcon, // dataTestId: `app-bar-item-context-center-integrations`, // }, - // { - // key: ROUTES.CONTEXT_CENTER_ARCHIVE, - // title: 'label.archive', - // redirect_url: ROUTES.CONTEXT_CENTER_ARCHIVE, - // icon: ContextCenterArchiveIcon, - // dataTestId: `app-bar-item-context-center-archive`, - // }, + { + key: ROUTES.CONTEXT_CENTER_ARCHIVE, + title: 'label.archive', + redirect_url: ROUTES.CONTEXT_CENTER_ARCHIVE, + icon: Archive, + dataTestId: `app-bar-item-context-center-archive`, + }, ], }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 643410249e8c..eb7a754a6fd8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -159,7 +159,10 @@ "approver-plural": "Approvers", "april": "April", "archive": "Archive", + "archive-file-plural": "Archive Files", + "archive-plural": "Archives", "archived": "Archived", + "archived-by": "Archived by {{name}}", "argument-plural": "Arguments", "arrow-symbol": "→", "article": "Article", @@ -2591,6 +2594,7 @@ "connection-test-successful": "Connection test was successful.", "connection-test-warning": "Test connection partially successful: Some steps had failures, we will only ingest partial metadata.", "consumer-aligned-domain-type-description": "Domains that are user-facing where the end product of a combination of data from various domains are available for business users or data citizens for data-driven decision-making.", + "context-center-archive-subtitle": "Archived articles and documents. Restore or permanently delete items.", "context-center-archive-subtitle": "View archived articles and documents", "context-center-dashboard-subtitle": "Overview of your knowledge base and document library", "context-center-documents-subtitle": "Manage and organize your uploaded documents", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx index 6511d95ae071..a2eafa46b2e6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx @@ -11,26 +11,52 @@ * limitations under the License. */ +import { + Badge, + Card, + Tabs, + Typography, +} from '@openmetadata/ui-core-components'; import { Home02 } from '@untitledui/icons'; import { AxiosError } from 'axios'; -import { FC, useCallback, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import DeleteModal from '../../../components/common/DeleteModal/DeleteModal'; +import ArchiveView from '../../../components/ContextCenter/ArchiveView/ArchiveView.component'; +import { ArchiveItem } from '../../../components/ContextCenter/ArchiveView/ArchiveView.interface'; import ContextCenterHeader from '../../../components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component'; -import { ROUTES } from '../../../constants/constants'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { OperationPermission, ResourceEntity, } from '../../../context/PermissionProvider/PermissionProvider.interface'; +import { Include } from '../../../generated/type/include'; +import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { + deleteDriveFile, + listArchivedContextFiles, + restoreDriveFile, +} from '../../../rest/assetAPI'; +import { + deleteKnowledgePage, + getListKnowledgePages, + restoreKnowledgePage, +} from '../../../rest/knowledgeCenterAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils'; -import { showErrorToast } from '../../../utils/ToastUtils'; +import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; + +type FilterKey = 'all' | 'mine' | 'article' | 'document'; const ContextCenterArchivePage: FC = () => { const { t } = useTranslation(); - const navigate = useNavigate(); + const { currentUser } = useApplicationStore(); const { getResourcePermission } = usePermissionProvider(); + const [allItems, setAllItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [activeFilter, setActiveFilter] = useState('all'); + const [itemToDelete, setItemToDelete] = useState(); + const [isDeleting, setIsDeleting] = useState(false); const [permissions, setPermissions] = useState( DEFAULT_ENTITY_PERMISSION ); @@ -46,13 +72,116 @@ const ContextCenterArchivePage: FC = () => { } }, [getResourcePermission]); + const fetchArchivedItems = useCallback(async () => { + setIsLoading(true); + try { + const [pagesResponse, files] = await Promise.all([ + getListKnowledgePages({ include: Include.Deleted, limit: 100 }), + listArchivedContextFiles(), + ]); + + const articleItems: ArchiveItem[] = (pagesResponse.data ?? []).map( + (page) => ({ + id: page.id, + name: page.displayName ?? page.name, + type: 'article' as const, + updatedBy: page.updatedBy, + updatedAt: page.updatedAt, + }) + ); + + const documentItems: ArchiveItem[] = files.map((file) => ({ + id: file.id, + name: file.displayName ?? file.name, + type: 'document' as const, + updatedBy: file.updatedBy, + updatedAt: file.updatedAt, + })); + + const merged = [...articleItems, ...documentItems].sort( + (a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0) + ); + + setAllItems(merged); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsLoading(false); + } + }, []); + useEffect(() => { fetchPermission(); - }, [fetchPermission]); + fetchArchivedItems(); + }, [fetchPermission, fetchArchivedItems]); + + const filteredItems = useMemo(() => { + switch (activeFilter) { + case 'mine': + return allItems.filter((item) => item.updatedBy === currentUser?.name); + case 'article': + return allItems.filter((item) => item.type === 'article'); + case 'document': + return allItems.filter((item) => item.type === 'document'); + default: + return allItems; + } + }, [allItems, activeFilter, currentUser?.name]); + + const handleRestore = useCallback( + async (item: ArchiveItem) => { + try { + if (item.type === 'article') { + await restoreKnowledgePage(item.id); + } else { + await restoreDriveFile(item.id); + } + setAllItems((prev) => prev.filter((i) => i.id !== item.id)); + showSuccessToast( + t('message.entity-restored-success', { entity: item.name }) + ); + } catch (err) { + showErrorToast(err as AxiosError); + } + }, + [t] + ); + + const handleDeleteClick = useCallback((item: ArchiveItem) => { + setItemToDelete(item); + }, []); + + const handleCancelDelete = useCallback(() => { + setItemToDelete(undefined); + }, []); + + const handleConfirmDelete = useCallback(async () => { + if (!itemToDelete) { + return; + } + + try { + setIsDeleting(true); + if (itemToDelete.type === 'article') { + await deleteKnowledgePage(itemToDelete.id, true); + } else { + await deleteDriveFile(itemToDelete.id, true); + } + setAllItems((prev) => prev.filter((i) => i.id !== itemToDelete.id)); + showSuccessToast( + t('server.entity-deleted-successfully', { entity: itemToDelete.name }) + ); + setItemToDelete(undefined); + } catch (err) { + showErrorToast(err as AxiosError); + } finally { + setIsDeleting(false); + } + }, [itemToDelete, t]); return (
{ ]} hasPermission={permissions?.Create} subtitle={t('message.context-center-archive-subtitle')} - title={t('label.archive')} - onCreateArticle={() => navigate(ROUTES.CONTEXT_CENTER_ARTICLES)} + title={t('label.archive-plural')} /> + +
+ + {t('label.archive-file-plural')} + + + {filteredItems.length} + +
+
+ setActiveFilter(key as FilterKey)}> + + {(tab) => ( + + isSelected + ? 'tw:rounded-md tw:border tw:border-brand-100 tw:bg-brand-50 tw:px-3 tw:py-1.5 tw:text-sm tw:font-semibold tw:text-brand-700 tw:cursor-pointer' + : 'tw:rounded-md tw:border tw:border-gray-300 tw:bg-white tw:px-3 tw:py-1.5 tw:text-sm tw:font-semibold tw:text-quaternary tw:cursor-pointer' + } + /> + )} + + +
+ + +
+ + {itemToDelete && ( + + )}
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts index b338216e5439..f4ba85d18240 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts @@ -132,6 +132,24 @@ export const deleteDriveFile = async ( }); }; +export const listArchivedContextFiles = async (): Promise => { + const response = await APIClient.get<{ data: ContextFile[] }>( + '/contextCenter/drive/files', + { params: { include: 'deleted', limit: 100 } } + ); + + return response.data.data ?? []; +}; + +export const restoreDriveFile = async (id: string): Promise => { + const response = await APIClient.put< + { id: string }, + AxiosResponse + >('/contextCenter/drive/files/restore', { id }); + + return response.data; +}; + export const downloadDriveFile = async (id: string): Promise => { const response = await APIClient.get( `/contextCenter/drive/files/${id}/download`, diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts index 5945b7c7095f..7497c58a3b63 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts @@ -69,6 +69,26 @@ export const getKnowledgePageByFqn = async ( return response.data; }; +export const deleteKnowledgePage = async ( + id: string, + hardDelete = false +): Promise => { + await APIClient.delete(`/contextCenter/pages/${id}`, { + params: { hardDelete }, + }); +}; + +export const restoreKnowledgePage = async ( + id: string +): Promise => { + const response = await APIClient.put< + { id: string }, + AxiosResponse + >('/contextCenter/pages/restore', { id }); + + return response.data; +}; + export const postKnowledgePage = async (data: CreateKnowledgePage) => { const response = await APIClient.post< CreateKnowledgePage, From 56d12826db1445c5617a2966a917b16fb24e1b30 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 22 May 2026 13:29:13 +0530 Subject: [PATCH 08/17] lint fix --- .../KnowledgeCenterReviewerWorkflow.spec.ts | 133 --------- .../ui/playwright/utils/userWorkflowUtils.ts | 274 ------------------ .../ContextCenterRouter.tsx | 2 +- .../CreateFolderModal.component.tsx | 2 +- .../ui/src/locale/languages/ar-sa.json | 3 + .../ui/src/locale/languages/de-de.json | 3 + .../ui/src/locale/languages/en-us.json | 1 - .../ui/src/locale/languages/es-es.json | 3 + .../ui/src/locale/languages/fr-fr.json | 3 + .../ui/src/locale/languages/gl-es.json | 3 + .../ui/src/locale/languages/he-he.json | 3 + .../ui/src/locale/languages/ja-jp.json | 3 + .../ui/src/locale/languages/ko-kr.json | 3 + .../ui/src/locale/languages/mr-in.json | 3 + .../ui/src/locale/languages/nl-nl.json | 3 + .../ui/src/locale/languages/pr-pr.json | 3 + .../ui/src/locale/languages/pt-br.json | 3 + .../ui/src/locale/languages/pt-pt.json | 3 + .../ui/src/locale/languages/ru-ru.json | 3 + .../ui/src/locale/languages/th-th.json | 3 + .../ui/src/locale/languages/tr-tr.json | 3 + .../ui/src/locale/languages/zh-cn.json | 3 + .../ui/src/locale/languages/zh-tw.json | 3 + 23 files changed, 56 insertions(+), 410 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts deleted file mode 100644 index 0d39fdd89628..000000000000 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/KnowledgeCenterReviewerWorkflow.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2024 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from '@playwright/test'; -import { performAdminLogin } from '../../utils/admin'; -import { getApiContext, redirectToHomePage, uuid } from '../../utils/common'; -import { waitForAllLoadersToDisappear } from '../../utils/entity'; -import { - checkNotificationAndApproveTask, - createUserApprovalWorkflowWithOwners, - verifyTaskStatus, -} from '../../utils/userWorkflowUtils'; -import { test } from '../fixtures/pages'; - -let knowledgePage: { fullyQualifiedName: string; id: string }; - -test.beforeAll('Setup workflow and knowledge page', async ({ browser }) => { - const { apiContext, afterAction } = await performAdminLogin(browser); - await createUserApprovalWorkflowWithOwners(apiContext, 'page'); - - const createResponse = await apiContext.post('/api/v1/contextCenter/pages', { - data: { - name: `pw-article-${uuid()}`, - displayName: `PW Article ${uuid()}`, - pageType: 'Article', - page: { - publicationDate: new Date().toISOString(), - relatedArticles: [], - }, - description: 'This is a test description for the knowledge page', - }, - }); - expect(createResponse.status()).toBe(201); - knowledgePage = await createResponse.json(); - - await afterAction(); -}); - -test.describe( - 'User Approval Workflow - Knowledge Page', - { tag: ['@Governance'] }, - () => { - test('Knowledge Page reviewer approval flow', async ({ - page, - dataConsumerPage, - }) => { - test.setTimeout(300_000); - - await redirectToHomePage(page); - await redirectToHomePage(dataConsumerPage); - - const { apiContext, afterAction } = await getApiContext(page); - const { apiContext: dcApiContext, afterAction: dcAfterAction } = - await getApiContext(dataConsumerPage); - - const dcUserResponse = await dcApiContext.get( - '/api/v1/users/loggedInUser' - ); - expect(dcUserResponse.status()).toBe(200); - const dataConsumerUser = await dcUserResponse.json(); - await dcAfterAction(); - - const patchResponse = await apiContext.patch( - `/api/v1/contextCenter/pages/${knowledgePage.id}`, - { - data: [ - { - op: 'add', - path: '/reviewers/0', - value: { - id: dataConsumerUser.id, - type: 'user', - displayName: dataConsumerUser.displayName, - fullyQualifiedName: dataConsumerUser.fullyQualifiedName, - name: dataConsumerUser.name, - }, - }, - ], - headers: { 'Content-Type': 'application/json-patch+json' }, - } - ); - expect(patchResponse.status()).toBe(200); - knowledgePage = await patchResponse.json(); - - const encodedFqn = encodeURIComponent(knowledgePage.fullyQualifiedName); - - await test.step('Navigate to Context Center Page', async () => { - await page.goto(`/context-center/articles/${encodedFqn}`); - await waitForAllLoadersToDisappear(page); - }); - - await test.step('Verify In Review status', async () => { - await verifyTaskStatus( - page, - 'inReview', - knowledgePage, - 'In Review', - apiContext, - 'knowledgeCenter' - ); - }); - - await test.step('Reviewer - Check notification and approve task', async () => { - await checkNotificationAndApproveTask(dataConsumerPage, async () => { - await dataConsumerPage.goto(`/context-center/articles/${encodedFqn}`); - }); - }); - - await test.step('Verify Approved status', async () => { - await verifyTaskStatus( - page, - 'success', - knowledgePage, - 'Approved', - apiContext, - 'knowledgeCenter' - ); - }); - - await afterAction(); - }); - } -); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts deleted file mode 100644 index e5bb4b9ae001..000000000000 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/userWorkflowUtils.ts +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright 2026 Collate. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { APIRequestContext, expect, Page } from '@playwright/test'; -import { SidebarItem } from '../constant/sidebar'; -import { DataProduct } from '../support/domain/DataProduct'; -import { TagClass } from '../support/tag/TagClass'; -import { redirectToHomePage, toastNotification, uuid } from './common'; -import { selectDataProduct } from './domain'; -import { waitForAllLoadersToDisappear } from './entity'; -import { sidebarClick } from './sidebar'; -import { TASK_OPEN_FETCH_LINK } from './task'; -import { approveTaskFromDetails } from './taskWorkflow'; - -export const checkNotificationAndApproveTask = async ( - dataConsumerPage: Page, - navigateToEntity: () => Promise -): Promise => { - await redirectToHomePage(dataConsumerPage); - await navigateToEntity(); - - await dataConsumerPage.getByTestId('activity_feed').click(); - const taskFeeds = dataConsumerPage.waitForResponse(TASK_OPEN_FETCH_LINK); - await dataConsumerPage - .getByTestId('global-setting-left-panel') - .getByText('Tasks') - .click(); - await taskFeeds; - - const taskCard = dataConsumerPage.getByTestId('task-feed-card').first(); - await taskCard.waitFor({ state: 'visible', timeout: 15_000 }); - await taskCard.click(); - - await approveTaskFromDetails(dataConsumerPage); - await toastNotification(dataConsumerPage, /Task resolved successfully/); -}; - -export const createUserApprovalWorkflowWithOwners = async ( - apiContext: APIRequestContext, - entityType = 'table', - workflowName?: string -) => { - const id = uuid(); - const name = workflowName ?? `pw-user-approval-owners-${entityType}-${id}`; - - const workflowPayload = { - name, - displayName: name, - description: 'User approval workflow for data asset entities using owners', - type: 'eventBasedEntity', - trigger: { - type: 'eventBasedEntity', - config: { - entityTypes: [entityType], - events: ['Created', 'Updated'], - exclude: [], - }, - output: ['relatedEntity', 'updatedBy'], - }, - nodes: [ - { - type: 'startEvent', - subType: 'startEvent', - name: 'start', - displayName: 'start', - }, - { - type: 'automatedTask', - subType: 'checkEntityAttributesTask', - name: 'checkEntityAttributesTask_1', - displayName: 'Check Condition', - input: ['relatedEntity'], - output: ['result'], - branches: ['true', 'false'], - config: { - rules: '{"and":[{"!=":[{"var":"description"},null]}]}', - }, - inputNamespaceMap: { - relatedEntity: 'global', - }, - }, - { - type: 'automatedTask', - subType: 'setEntityAttributeTask', - name: 'setEntityAttributeTask_1', - displayName: 'Set Action', - input: ['relatedEntity', 'updatedBy'], - output: [], - config: { - fieldName: 'status', - fieldValue: 'In Review', - }, - inputNamespaceMap: { - relatedEntity: 'global', - updatedBy: 'global', - }, - }, - { - type: 'endEvent', - subType: 'endEvent', - name: 'endEvent_1', - displayName: 'End', - }, - { - type: 'userTask', - subType: 'userApprovalTask', - name: 'userApprovalTask_1', - displayName: 'Request Approval', - input: ['relatedEntity'], - output: ['updatedBy'], - branches: ['true', 'false'], - config: { - assignees: { - addReviewers: false, - addOwners: true, - candidates: [], - }, - approvalThreshold: 1, - rejectionThreshold: 1, - }, - inputNamespaceMap: { - relatedEntity: 'global', - }, - }, - { - type: 'automatedTask', - subType: 'setEntityAttributeTask', - name: 'setEntityAttributeTask_2', - displayName: 'Set Action', - input: ['relatedEntity', 'updatedBy'], - output: [], - config: { - fieldName: 'status', - fieldValue: 'Approved', - }, - inputNamespaceMap: { - relatedEntity: 'global', - updatedBy: 'userApprovalTask_1', - }, - }, - { - type: 'endEvent', - subType: 'endEvent', - name: 'endEvent_2', - displayName: 'End', - }, - { - type: 'endEvent', - subType: 'endEvent', - name: 'endEvent_3', - displayName: 'End', - }, - ], - edges: [ - { - from: 'start', - to: 'checkEntityAttributesTask_1', - }, - { - from: 'checkEntityAttributesTask_1', - to: 'setEntityAttributeTask_1', - condition: 'true', - }, - { - from: 'checkEntityAttributesTask_1', - to: 'endEvent_1', - condition: 'false', - }, - { - from: 'setEntityAttributeTask_1', - to: 'userApprovalTask_1', - }, - { - from: 'userApprovalTask_1', - to: 'setEntityAttributeTask_2', - condition: 'true', - }, - { - from: 'userApprovalTask_1', - to: 'endEvent_2', - condition: 'false', - }, - { - from: 'setEntityAttributeTask_2', - to: 'endEvent_3', - }, - ], - config: { - storeStageStatus: true, - }, - }; - - const response = await apiContext.post( - '/api/v1/governance/workflowDefinitions', - { data: workflowPayload } - ); - - return response.json(); -}; - -const getFQN = (entity: EntityWithFQN): string => { - if (entity instanceof TagClass && entity.responseData) { - return entity.responseData.fullyQualifiedName || ''; - } - if (entity instanceof DataProduct && entity.data) { - return entity.data.fullyQualifiedName || ''; - } - if ('fullyQualifiedName' in entity) { - return entity.fullyQualifiedName || ''; - } - - return ''; -}; - -type EntityWithFQN = DataProduct | TagClass | { fullyQualifiedName: string }; - -export const verifyTaskStatus = async ( - page: Page, - statusBadge: string, - entity: EntityWithFQN, - statusLabel: string, - apiContext: APIRequestContext, - entityType: 'dataProduct' | 'tag' | 'knowledgeCenter' = 'dataProduct' -): Promise => { - const entityFQN = encodeURIComponent(getFQN(entity)); - - const apiEndpoints = { - dataProduct: `/api/v1/dataProducts/name/${entityFQN}?fields=reviewers`, - tag: `/api/v1/tags/name/${entityFQN}?fields=reviewers`, - knowledgeCenter: `/api/v1/contextCenter/pages/name/${entityFQN}?fields=reviewers`, - }; - - await expect - .poll( - async () => { - const response = await apiContext - .get(apiEndpoints[entityType]) - .then((res) => res.json()); - - return response?.entityStatus; - }, - { - message: `Wait for ${entityType} status to be ${statusLabel}`, - timeout: 300_000, - intervals: [60_000, 40_000, 20_000], - } - ) - .toBe(statusLabel); - - await redirectToHomePage(page); - - if (entityType === 'dataProduct') { - await sidebarClick(page, SidebarItem.DATA_PRODUCT); - await selectDataProduct(page, (entity as DataProduct).data); - } else if (entityType === 'tag') { - await sidebarClick(page, SidebarItem.TAGS); - await (entity as TagClass).visitPage(page); - } else { - await page.goto(`/context-center/articles/${entityFQN}`); - } - await waitForAllLoadersToDisappear(page); - await expect( - page.locator(`.status-badge.${statusBadge} .status-badge-label`) - ).toContainText(statusLabel); -}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx index fc7433100300..d79aaa7f272b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx @@ -135,7 +135,7 @@ const ContextCenterRouter = () => { } path={ROUTES.CONTEXT_CENTER_ARCHIVE.replace(ROUTES.CONTEXT_CENTER, '')} - /> + /> ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx index 28515b462503..68a8b0a1dbe7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/CreateFolderModal/CreateFolderModal.component.tsx @@ -89,7 +89,7 @@ const CreateFolderModal: FC = ({
{t('label.entity-name', { entity: t('label.folder') })} diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json index 0f674cca21ac..7a81e9aa124c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json @@ -159,7 +159,10 @@ "approver-plural": "الموافقون", "april": "أبريل", "archive": "أرشيف", + "archive-file-plural": "ملفات الأرشيف", + "archive-plural": "أرشيف", "archived": "مؤرشف", + "archived-by": "مؤرشف بواسطة {{name}}", "argument-plural": "وسائط", "arrow-symbol": "→", "article": "مقال", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 8766b5405a8d..9de31679c411 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -159,7 +159,10 @@ "approver-plural": "Genehmiger", "april": "April", "archive": "Archiv", + "archive-file-plural": "Archivdateien", + "archive-plural": "Archive", "archived": "Archiviert", + "archived-by": "Archiviert von {{name}}", "argument-plural": "Argumente", "arrow-symbol": "→", "article": "Artikel", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index eb7a754a6fd8..116afb7a9f0a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -2594,7 +2594,6 @@ "connection-test-successful": "Connection test was successful.", "connection-test-warning": "Test connection partially successful: Some steps had failures, we will only ingest partial metadata.", "consumer-aligned-domain-type-description": "Domains that are user-facing where the end product of a combination of data from various domains are available for business users or data citizens for data-driven decision-making.", - "context-center-archive-subtitle": "Archived articles and documents. Restore or permanently delete items.", "context-center-archive-subtitle": "View archived articles and documents", "context-center-dashboard-subtitle": "Overview of your knowledge base and document library", "context-center-documents-subtitle": "Manage and organize your uploaded documents", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 198840fd2141..c4e17e59b661 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -159,7 +159,10 @@ "approver-plural": "Aprobadores", "april": "Abril", "archive": "Archivo", + "archive-file-plural": "Archivos", + "archive-plural": "Archivos", "archived": "Archivado", + "archived-by": "Archivado por {{name}}", "argument-plural": "Argumentos", "arrow-symbol": "→", "article": "Artículo", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index ce9fd991133e..2eaf6aad2fe5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -159,7 +159,10 @@ "approver-plural": "Approbateurs", "april": "Avril", "archive": "Archive", + "archive-file-plural": "Fichiers d'archive", + "archive-plural": "Archives", "archived": "Archivé", + "archived-by": "Archivé par {{name}}", "argument-plural": "Arguments", "arrow-symbol": "→", "article": "Article", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index ff7847ad136c..9540e36e10a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -159,7 +159,10 @@ "approver-plural": "Aprobadores", "april": "Abril", "archive": "Arquivo", + "archive-file-plural": "Ficheiros de arquivo", + "archive-plural": "Arquivos", "archived": "Arquivado", + "archived-by": "Arquivado por {{name}}", "argument-plural": "Argumentos", "arrow-symbol": "→", "article": "Artigo", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index a1bec9e3b5b7..e445bb4b01e3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -159,7 +159,10 @@ "approver-plural": "מאשרים", "april": "אפריל", "archive": "ארכיון", + "archive-file-plural": "קבצי ארכיון", + "archive-plural": "ארכיונים", "archived": "בארכיון", + "archived-by": "בארכיון על ידי {{name}}", "argument-plural": "ארגומנטים", "arrow-symbol": "→", "article": "מאמר", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index ae557271dd69..99d0ae79be2b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -159,7 +159,10 @@ "approver-plural": "承認者", "april": "4月", "archive": "アーカイブ", + "archive-file-plural": "アーカイブファイル", + "archive-plural": "アーカイブ", "archived": "アーカイブ済み", + "archived-by": "{{name}} によってアーカイブされました", "argument-plural": "引数", "arrow-symbol": "→", "article": "記事", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index 87755ace93ab..0026d0b0783e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -159,7 +159,10 @@ "approver-plural": "승인자", "april": "4월", "archive": "아카이브", + "archive-file-plural": "아카이브 파일", + "archive-plural": "아카이브", "archived": "보관됨", + "archived-by": "{{name}} 님이 아카이브함", "argument-plural": "인자들", "arrow-symbol": "→", "article": "기사", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index 39ec173fee22..168f225173d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -159,7 +159,10 @@ "approver-plural": "मंजूरीदार", "april": "एप्रिल", "archive": "संग्रहण", + "archive-file-plural": "संग्रहण फायली", + "archive-plural": "संग्रह", "archived": "संग्रहित", + "archived-by": "{{name}} द्वारे संग्रहित", "argument-plural": "आर्ग्युमेंट्स", "arrow-symbol": "→", "article": "लेख", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 721419820f10..c00202bf7740 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -159,7 +159,10 @@ "approver-plural": "Goedkeurders", "april": "April", "archive": "Archief", + "archive-file-plural": "Archiefbestanden", + "archive-plural": "Archieven", "archived": "Gearchiveerd", + "archived-by": "Gearchiveerd door {{name}}", "argument-plural": "Argumenten", "arrow-symbol": "→", "article": "Artikel", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index bccca43b84c5..bab34b6e04ca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -159,7 +159,10 @@ "approver-plural": "تأییدکنندگان", "april": "آوریل", "archive": "Arquive", + "archive-file-plural": "فایل‌های بایگانی", + "archive-plural": "بایگانی‌ها", "archived": "بایگانی شده", + "archived-by": "بایگانی شده توسط {{name}}", "argument-plural": "آرگومان‌ها", "arrow-symbol": "→", "article": "مقاله", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index 56ea2c12fd8c..dff9c615e722 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -159,7 +159,10 @@ "approver-plural": "Aprovadores", "april": "Abril", "archive": "Arquivo", + "archive-file-plural": "Arquivos de arquivo", + "archive-plural": "Arquivos", "archived": "Arquivado", + "archived-by": "Arquivado por {{name}}", "argument-plural": "Argumentos", "arrow-symbol": "→", "article": "Artigo", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index 56555cf9fbc1..d59a37f540b1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -159,7 +159,10 @@ "approver-plural": "Aprovadores", "april": "Abril", "archive": "Arquivo", + "archive-file-plural": "Ficheiros de Arquivo", + "archive-plural": "Arquivos", "archived": "Arquivado", + "archived-by": "Arquivado por {{name}}", "argument-plural": "Argumentos", "arrow-symbol": "→", "article": "Artigo", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 8c4c3e949aa0..06db3b770a61 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -159,7 +159,10 @@ "approver-plural": "Утверждающие", "april": "Апрель", "archive": "Архив", + "archive-file-plural": "Архивные файлы", + "archive-plural": "Архивы", "archived": "В архиве", + "archived-by": "В архиве: {{name}}", "argument-plural": "Аргументы", "arrow-symbol": "→", "article": "Статья", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index d598c79a396d..13e82dae83e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -159,7 +159,10 @@ "approver-plural": "ผู้อนุมัติ", "april": "เมษายน", "archive": "เก็บถาวร", + "archive-file-plural": "ไฟล์เก็บถาวร", + "archive-plural": "คลังเก็บถาวร", "archived": "เก็บถาวร", + "archived-by": "เก็บถาวรโดย {{name}}", "argument-plural": "อาร์กิวเมนต์", "arrow-symbol": "→", "article": "บทความ", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json index cb72461881f3..f6049e8ec94a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json @@ -159,7 +159,10 @@ "approver-plural": "Onaylayıcılar", "april": "Nisan", "archive": "Arşiv", + "archive-file-plural": "Arşiv Dosyaları", + "archive-plural": "Arşivler", "archived": "Arşivlendi", + "archived-by": "{{name}} tarafından arşivlendi", "argument-plural": "Argümanlar", "arrow-symbol": "→", "article": "Makale", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 3e5290a11f20..86133b7b7a4f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -159,7 +159,10 @@ "approver-plural": "审批人", "april": "四月", "archive": "归档", + "archive-file-plural": "归档文件", + "archive-plural": "归档", "archived": "已归档", + "archived-by": "由 {{name}} 归档", "argument-plural": "参数", "arrow-symbol": "→", "article": "文章", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json index 98c9eca9fce4..31ae42efebec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json @@ -159,7 +159,10 @@ "approver-plural": "审批人", "april": "四月", "archive": "歸檔", + "archive-file-plural": "歸檔文件", + "archive-plural": "歸檔", "archived": "已封存", + "archived-by": "由 {{name}} 歸檔", "argument-plural": "引數", "arrow-symbol": "→", "article": "文章", From bec6dc8183b0485a6a985043b456973815c78b62 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 22 May 2026 15:17:33 +0530 Subject: [PATCH 09/17] code refator --- .../ContextCenterRouter.tsx | 20 ------------ .../ContextCenterHeader.component.tsx | 2 +- .../DocumentFolderView.component.tsx | 4 +-- .../DocumentsView/DocumentsView.component.tsx | 4 +-- .../ui/src/constants/LeftSidebar.constants.ts | 8 ----- .../ContextCenterArchivePage.tsx | 23 +++++++++++-- .../ContextCenterMemoriesPage.tsx | 32 +++++++------------ 7 files changed, 36 insertions(+), 57 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx index d79aaa7f272b..0548233f6bb9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/ContextCenterRouter/ContextCenterRouter.tsx @@ -60,18 +60,6 @@ const KnowledgeCenterFilterPage = withSuspenseFallback( ) ) ); -{ - /* TODO: In progress */ -} - -// const ContextCenterIntegrationsPage = withSuspenseFallback( -// React.lazy( -// () => -// import( -// '../../../pages/ContextCenterPage/ContextCenterIntegrationsPage/ContextCenterIntegrationsPage' -// ) -// ) -// ); const ContextCenterArchivePage = withSuspenseFallback( React.lazy( @@ -124,14 +112,6 @@ const ContextCenterRouter = () => { element={} path={ROUTES.CONTEXT_CENTER_FILTER.replace(ROUTES.CONTEXT_CENTER, '')} /> - {/* TODO: In progress */} - {/* } - path={ROUTES.CONTEXT_CENTER_INTEGRATIONS.replace( - ROUTES.CONTEXT_CENTER, - '' - )} - />*/} } path={ROUTES.CONTEXT_CENTER_ARCHIVE.replace(ROUTES.CONTEXT_CENTER, '')} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx index 3634667b734e..fc08127e4410 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component.tsx @@ -100,7 +100,7 @@ const ContextCenterHeader: FC = ({ ; - const message = axiosErr?.response?.data?.message ?? ''; - showErrorToast(message); + showErrorToast(err as AxiosError); } finally { setFolderToDelete(undefined); setIsDeletingFolder(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index adf3e77abc7f..6a7e8eb10559 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -157,9 +157,9 @@ const FileActions: FC = ({ onAction={() => handleMoveToFolder(folder.id)}> {() => (
- + {folder.name} - +
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts index 9679b1aec0b6..5b2c9b91e5d5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/LeftSidebar.constants.ts @@ -243,14 +243,6 @@ export const SIDEBAR_LIST: Array = [ icon: createIconWithStroke(Lightbulb03 as UntitledIconType, 1.2), dataTestId: `app-bar-item-${SidebarItem.MEMORIES}`, }, - // TODO: In progress - // { - // key: ROUTES.CONTEXT_CENTER_INTEGRATIONS, - // title: 'label.integration-plural', - // redirect_url: ROUTES.CONTEXT_CENTER_INTEGRATIONS, - // icon: IntegrationIcon, - // dataTestId: `app-bar-item-context-center-integrations`, - // }, { key: ROUTES.CONTEXT_CENTER_ARCHIVE, title: 'label.archive', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx index a2eafa46b2e6..de2e4d3497de 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx @@ -17,7 +17,8 @@ import { Tabs, Typography, } from '@openmetadata/ui-core-components'; -import { Home02 } from '@untitledui/icons'; +import { File06, Home02 } from '@untitledui/icons'; +import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; import { AxiosError } from 'axios'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -224,8 +225,24 @@ const ContextCenterArchivePage: FC = () => { items={[ { id: 'all', label: t('label.all') }, { id: 'mine', label: t('label.created-by-me') }, - { id: 'article', label: t('label.article-plural') }, - { id: 'document', label: t('label.document-plural') }, + { + id: 'article', + label: ( + + + {t('label.article-plural')} + + ), + }, + { + id: 'document', + label: ( + + + {t('label.document-plural')} + + ), + }, ]} type="button-brand"> {(tab) => ( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index 8d04baa32fdc..719273d9c078 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -15,7 +15,6 @@ import { Button, Card, Dropdown, - Input, PaginationCardMinimal, Tabs, Typography, @@ -25,7 +24,6 @@ import { FilterLines, Home02, Plus, - SearchLg, } from '@untitledui/icons'; import { AxiosError } from 'axios'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; @@ -283,6 +281,7 @@ const ContextCenterMemoriesPage: FC = () => { setCurrentPage(1); }, []); + const handleDeleteMemory = useCallback((memory: MemoryItem) => { setMemoryToDelete(memory); }, []); @@ -349,24 +348,14 @@ const ContextCenterMemoriesPage: FC = () => { ); const headerActions = ( -
- - -
+ ); return ( @@ -393,8 +382,11 @@ const ContextCenterMemoriesPage: FC = () => { url: '', }, ]} + searchPlaceholder={t('label.search-memories')} + searchQuery={searchValue} subtitle={t('message.context-center-memories-subtitle')} title={t('label.memory-plural')} + onSearch={handleSearchChange} /> {/* Stats cards */} From 128b7f17dc49b821472d6ce0ce4479412b481913 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 22 May 2026 15:20:56 +0530 Subject: [PATCH 10/17] lint fix --- .../DocumentsView/DocumentsView.component.tsx | 4 +++- .../ContextCenterArchivePage/ContextCenterArchivePage.tsx | 2 +- .../ContextCenterMemoriesPage.tsx | 8 +------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 6a7e8eb10559..258f196049ec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -157,7 +157,9 @@ const FileActions: FC = ({ onAction={() => handleMoveToFolder(folder.id)}> {() => (
- + {folder.name}
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx index de2e4d3497de..aae3d92bf98f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx @@ -18,10 +18,10 @@ import { Typography, } from '@openmetadata/ui-core-components'; import { File06, Home02 } from '@untitledui/icons'; -import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; import { AxiosError } from 'axios'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; import DeleteModal from '../../../components/common/DeleteModal/DeleteModal'; import ArchiveView from '../../../components/ContextCenter/ArchiveView/ArchiveView.component'; import { ArchiveItem } from '../../../components/ContextCenter/ArchiveView/ArchiveView.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx index 719273d9c078..79dd19b4970c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterMemoriesPage/ContextCenterMemoriesPage.tsx @@ -19,12 +19,7 @@ import { Tabs, Typography, } from '@openmetadata/ui-core-components'; -import { - ChevronDown, - FilterLines, - Home02, - Plus, -} from '@untitledui/icons'; +import { ChevronDown, FilterLines, Home02, Plus } from '@untitledui/icons'; import { AxiosError } from 'axios'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Button as AriaButton } from 'react-aria-components'; @@ -281,7 +276,6 @@ const ContextCenterMemoriesPage: FC = () => { setCurrentPage(1); }, []); - const handleDeleteMemory = useCallback((memory: MemoryItem) => { setMemoryToDelete(memory); }, []); From 6cc4a5562b11a08329d5350e47fa8b20c8161278 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 22 May 2026 16:01:31 +0530 Subject: [PATCH 11/17] unit test fix --- .../e2e/Features/ContextCenter.spec.ts | 8 ----- .../DocumentsView/DocumentsView.test.tsx | 31 +++++++++++++++++++ .../UploadDocumentModal.test.tsx | 2 +- .../KnowledgeCard/KnowledgeCard.tsx | 2 +- .../ContextCenterArchivePage.tsx | 7 ++++- .../ContextCenterDocumentsPage.tsx | 2 +- .../ui/src/utils/ContextCenterUtils.test.ts | 10 +++--- .../ui/src/utils/ContextCenterUtils.tsx | 5 ++- 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts index 683631e9cf7d..56b8c042cf35 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts @@ -141,14 +141,6 @@ test.describe('Context Center', () => { // Upload a document via API so document-related tests have data const fileContent = Buffer.from('Playwright seed document'); - const formData = new FormData(); - formData.append( - 'file', - new Blob([fileContent], { type: 'text/plain' }), - 'seed-document.txt' - ); - formData.append('entityLink', '<#E::page::contextCenter.documents>'); - formData.append('assetType', 'External'); await apiContext.post('/api/v1/contextCenter/drive/files/upload', { multipart: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.test.tsx index 08c40433b909..78c4e2ce6286 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.test.tsx @@ -20,6 +20,37 @@ jest.mock( () => jest.fn(() =>
) ); +jest.mock('react-aria-components', () => ({ + Menu: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + MenuItem: jest.fn( + ({ + children, + onAction, + 'data-testid': testId, + }: { + children: React.ReactNode | (() => React.ReactNode); + onAction?: () => void; + 'data-testid'?: string; + }) => ( +
+ {typeof children === 'function' ? children() : children} +
+ ) + ), + Popover: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), + SubmenuTrigger: jest.fn(({ children }: { children: React.ReactNode }) => ( +
{children}
+ )), +})); + jest.mock('@openmetadata/ui-core-components', () => ({ ButtonUtility: jest.fn( ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx index a01ec1c8f41e..080f829ca800 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/UploadDocumentModal/UploadDocumentModal.test.tsx @@ -143,7 +143,7 @@ jest.mock('@openmetadata/ui-core-components', () => ({ const defaultProps = { isOpen: true, - entityLink: 'entity::link', + folderFqn: undefined, onClose: jest.fn(), onUploaded: jest.fn(), }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx index cf1a6b0eb7f5..a9db13d45de3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx @@ -64,10 +64,10 @@ import { QuickLinkFormModalFormData, } from '../QuickLinkFormModal/QuickLinkFormModal'; -import { getEntityName } from 'utils/EntityUtils'; import { useCurrentUserPreferences } from '../../../hooks/currentUserStore/useCurrentUserStore'; import { deleteKnowledgePage } from '../../../rest/knowledgeCenterAPI'; import contextCenterClassBase from '../../../utils/ContextCenterClassBase'; +import { getEntityName } from '../../../utils/EntityUtils'; import './knowledge-card.less'; export interface KnowledgeCardProps { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx index aae3d92bf98f..b8767c962cff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx @@ -33,6 +33,7 @@ import { } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { Include } from '../../../generated/type/include'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; +import { PageType } from '../../../interface/knowledge-center.interface'; import { deleteDriveFile, listArchivedContextFiles, @@ -77,7 +78,11 @@ const ContextCenterArchivePage: FC = () => { setIsLoading(true); try { const [pagesResponse, files] = await Promise.all([ - getListKnowledgePages({ include: Include.Deleted, limit: 100 }), + getListKnowledgePages({ + include: Include.Deleted, + limit: 100, + pageType: PageType.ARTICLE, + }), listArchivedContextFiles(), ]); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index adfd68f7ab9d..04a107c00d82 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -13,7 +13,6 @@ import { Home02 } from '@untitledui/icons'; import { AxiosError } from 'axios'; -import DocumentFolderView from 'components/ContextCenter/DocumentsView/DocumentFolderView.component'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; @@ -21,6 +20,7 @@ import AlertBar from '../../../components/AlertBar/AlertBar'; import DeleteModal from '../../../components/common/DeleteModal/DeleteModal'; import '../../../components/common/ResizablePanels/resizable-panels.less'; import ContextCenterHeader from '../../../components/ContextCenter/ContextCenterHeader/ContextCenterHeader.component'; +import DocumentFolderView from '../../../components/ContextCenter/DocumentsView/DocumentFolderView.component'; import DocumentsView from '../../../components/ContextCenter/DocumentsView/DocumentsView.component'; import { DocFile, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.test.ts index f85037d9d7af..7f9add5a4302 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.test.ts @@ -15,7 +15,7 @@ import { AxiosError } from 'axios'; import { ROUTES } from '../constants/constants'; import { Asset } from '../generated/attachments/asset'; import { PageType } from '../interface/knowledge-center.interface'; -import { downloadAsset } from '../rest/assetAPI'; +import { downloadDriveFile } from '../rest/assetAPI'; import { assetToDocumentItem, extensionToFileType, @@ -31,7 +31,7 @@ jest.mock('./ToastUtils', () => ({ })); jest.mock('../rest/assetAPI', () => ({ - downloadAsset: jest.fn(), + downloadDriveFile: jest.fn(), })); jest.mock('./KnowledgePageUtils', () => ({ @@ -183,7 +183,7 @@ describe('handleAssetDownload', () => { }); it('should download asset successfully', async () => { - (downloadAsset as jest.Mock).mockResolvedValue(mockBlob); + (downloadDriveFile as jest.Mock).mockResolvedValue(mockBlob); const clickMock = jest.fn(); const removeMock = jest.fn(); @@ -199,7 +199,7 @@ describe('handleAssetDownload', () => { await handleAssetDownload(mockFile as any); - expect(downloadAsset).toHaveBeenCalledWith('123'); + expect(downloadDriveFile).toHaveBeenCalledWith('123'); expect(URL.createObjectURL).toHaveBeenCalledWith(mockBlob); @@ -215,7 +215,7 @@ describe('handleAssetDownload', () => { it('should show error toast when download fails', async () => { const error = new Error('Download failed'); - (downloadAsset as jest.Mock).mockRejectedValue(error); + (downloadDriveFile as jest.Mock).mockRejectedValue(error); await handleAssetDownload(mockFile as any); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx index 7749e4f815ae..de2ce7f5e1ad 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx @@ -21,7 +21,10 @@ import { ReactComponent as ImageIcon } from '../assets/svg/ic-image.svg'; import { ReactComponent as PDFIcon } from '../assets/svg/ic-pdf.svg'; import { ReactComponent as XLSIcon } from '../assets/svg/ic-xls.svg'; import { ArticleCardItem } from '../components/ContextCenter/ArticleCard/ArticleCard.interface'; -import { DocFile, DocFileType } from '../components/ContextCenter/DocumentsView/DocumentsView.interface'; +import { + DocFile, + DocFileType, +} from '../components/ContextCenter/DocumentsView/DocumentsView.interface'; import { UploadedDocumentItem } from '../components/ContextCenter/UploadedDocumentCard/UploadedDocumentCard.interface'; import { CREATE_PAGE_HASH } from '../constants/constants'; import { FILE_TYPE_STYLES } from '../constants/ContextCenter.constants'; From a8e415b50634a827cd38e4f5957b70d240c01477 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Fri, 22 May 2026 21:24:58 +0530 Subject: [PATCH 12/17] fixed minor issues --- .../e2e/Features/ContextCenter.spec.ts | 2 +- .../KnowledgePageDetailComponent.tsx | 6 +++--- .../QuickLinkFormModal/QuickLinkFormModal.tsx | 16 +++++++++++++++- .../main/resources/ui/src/utils/TagClassBase.ts | 4 ++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts index 56b8c042cf35..02d78369c6a3 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ContextCenter.spec.ts @@ -1046,7 +1046,7 @@ test.describe('Context Center', () => { await expect(downloadBtn).toBeVisible(); }); - test('download button triggers file download', async ({ page }) => { + test.fixme('download button triggers file download', async ({ page }) => { await navigateToDocuments(page); const view = page.getByTestId('documents-view'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx index fe175428de1a..8b447463f98e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePageDetailComponent/KnowledgePageDetailComponent.tsx @@ -24,7 +24,7 @@ import { useState, } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { TagClassBase } from 'utils/TagClassBase'; +import tagClassBase from 'utils/TagClassBase'; import { useActivityFeedProvider } from '../../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; import { ActivityFeedTab } from '../../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import { ActivityFeedLayoutType } from '../../../components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface'; @@ -698,10 +698,10 @@ const KnowledgePageDetailComponent: FC = ({ ); useEffect(() => { - TagClassBase.filterClassification = []; + tagClassBase.setFilterClassification([]); return () => { - TagClassBase.filterClassification = [KNOWLEDGE_CENTER_CLASSIFICATION]; + tagClassBase.setFilterClassification([KNOWLEDGE_CENTER_CLASSIFICATION]); }; }, []); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx index c9511917b307..aa4c506476bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx @@ -20,10 +20,11 @@ import { Form } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { cloneDeep, isEqual, isNil, isUndefined } from 'lodash'; -import { FC, useMemo, useState } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import DataAssetAsyncSelectList from '../../../components/DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList'; import { DataAssetOption } from '../../../components/DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList.interface'; +import { KNOWLEDGE_CENTER_CLASSIFICATION } from '../../../constants/constants'; import { getKnowledgePageFields } from '../../../constants/KnowledgeCenter.constant'; import { OperationPermission } from '../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityReference } from '../../../generated/entity/type'; @@ -46,6 +47,7 @@ import { import i18n from '../../../utils/i18next/LocalUtil'; import { getFilterTags } from '../../../utils/TableTags/TableTags.utils'; import { getTagsWithoutTier } from '../../../utils/TableUtils'; +import tagClassBase from '../../../utils/TagClassBase'; import { showErrorToast } from '../../../utils/ToastUtils'; export interface QuickLinkFormModalFormData @@ -76,6 +78,18 @@ export const QuickLinkFormModal: FC = ({ const [isUpdating, setIsUpdating] = useState(false); + useEffect(() => { + if (isOpen) { + tagClassBase.setFilterClassification([]); + } else { + tagClassBase.setFilterClassification([KNOWLEDGE_CENTER_CLASSIFICATION]); + } + + return () => { + tagClassBase.setFilterClassification([KNOWLEDGE_CENTER_CLASSIFICATION]); + }; + }, [isOpen]); + const { initialValues, initialDataAssetsOptions, restRelatedDataAssets } = useMemo(() => { if (isUndefined(quickLink)) { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts index 502195ec6130..c04ea4ba745b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts @@ -50,6 +50,10 @@ class TagClassBase { static filterClassification: string[] = []; defaultWidgetHeight: Record; + public setFilterClassification(value: string[]) { + TagClassBase.filterClassification = value; + } + constructor() { this.defaultWidgetHeight = { [DetailPageWidgetKeys.DESCRIPTION]: 4, From 3cac35d0cee8a079ab504514edb26e2be2d10eef Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 25 May 2026 12:59:44 +0530 Subject: [PATCH 13/17] fixed minor issues --- .../ArticleDetailHeader/ArticleDetailHeader.component.tsx | 1 - .../DocumentsView/DocumentsView.component.tsx | 5 ++++- .../UploadedDocumentCard.interface.ts | 1 + .../KnowledgePageDetailComponent.tsx | 2 +- .../resources/ui/src/constants/LeftSidebar.constants.ts | 2 +- .../ContextCenterArchivePage/ContextCenterArchivePage.tsx | 2 +- .../ContextCenterDocumentsPage.tsx | 2 +- .../src/main/resources/ui/src/rest/assetAPI.ts | 2 +- .../src/main/resources/ui/src/rest/knowledgeCenterAPI.ts | 8 -------- .../main/resources/ui/src/utils/ContextCenterUtils.tsx | 1 + 10 files changed, 11 insertions(+), 15 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx index 002eae9500f7..6f94e0c67ce1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx @@ -192,7 +192,6 @@ const ArticleDetailHeader: FC = ({ knowledgePage, recentlyViewed, fetchKnowledgePageHierarchy, - onToggleDelete, ]); const handleVersionClick = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 258f196049ec..859fa2e1b3c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -93,12 +93,15 @@ const FileActions: FC = ({ - + { if (key === 'share') { onShareFile?.(file); } + else if (key === 'delete') { + onDeleteFile?.(file); + } }}> = [ key: ROUTES.CONTEXT_CENTER_ARCHIVE, title: 'label.archive', redirect_url: ROUTES.CONTEXT_CENTER_ARCHIVE, - icon: Archive, + icon: createIconWithStroke(Archive as UntitledIconType, 1.2), dataTestId: `app-bar-item-context-center-archive`, }, ], diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx index b8767c962cff..03e0dc80f26b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx @@ -213,7 +213,7 @@ const ContextCenterArchivePage: FC = () => { />
- + {t('label.archive-file-plural')} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index 04a107c00d82..b12d2273bf3f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -258,8 +258,8 @@ const ContextCenterDocumentsPage: FC = () => { onClose={() => setIsUploadModalOpen(false)} onUploaded={(newFiles) => setAllDocuments((prev) => [ - ...prev, ...newFiles.map(contextFileToDocumentItem), + ...prev, ]) } /> diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts index f4ba85d18240..ca98f5918cfa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts @@ -153,7 +153,7 @@ export const restoreDriveFile = async (id: string): Promise => { export const downloadDriveFile = async (id: string): Promise => { const response = await APIClient.get( `/contextCenter/drive/files/${id}/download`, - { params: { redirect: false, expiry: 300 }, responseType: 'blob' } + { params: { redirect: true, expiry: 300 }, responseType: 'blob' } ); return response.data; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts index 7497c58a3b63..ea020a2c85b1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/knowledgeCenterAPI.ts @@ -173,14 +173,6 @@ export const unFollowKnowledgePage = async ( return response.data; }; -export const deleteKnowledgePage = async (id: string, recursive = true) => { - const response = await APIClient.delete( - `/contextCenter/pages/${id}?hardDelete=true&recursive=${recursive}` - ); - - return response.data; -}; - export const getKnowledgePageVersionsList = async (id: string) => { const url = `contextCenter/pages/${id}/versions`; const response = await APIClient.get(url); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx index de2ce7f5e1ad..25b9c40d8f54 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ContextCenterUtils.tsx @@ -139,6 +139,7 @@ export const contextFileToDocumentItem = (file: ContextFile): DocFile => ({ export const contextFileToUploadedDocumentItem = ( file: ContextFile ): UploadedDocumentItem => ({ + driveFileId: file.id, fileType: extensionToFileType(file.displayName ?? file.name), id: file.assetId ?? file.id, name: file.displayName ?? file.name, From ea26f5a7d0650ed5965327ee1aface65f0dbda76 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 25 May 2026 13:02:41 +0530 Subject: [PATCH 14/17] lint fix --- .../ArticleDetailHeader/ArticleDetailHeader.component.tsx | 6 +----- .../ContextCenter/DocumentsView/DocumentsView.component.tsx | 3 +-- .../KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx | 2 +- .../QuickLinkFormModal/QuickLinkFormModal.tsx | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx index 6f94e0c67ce1..5d50c28b649d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArticleDetailHeader/ArticleDetailHeader.component.tsx @@ -188,11 +188,7 @@ const ArticleDetailHeader: FC = ({ } finally { setIsDeleting(false); } - }, [ - knowledgePage, - recentlyViewed, - fetchKnowledgePageHierarchy, - ]); + }, [knowledgePage, recentlyViewed, fetchKnowledgePageHierarchy]); const handleVersionClick = () => { navigate(contextCenterClassBase.getArticleVersionPath(fqn, version)); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 859fa2e1b3c6..1e5ec60faa2c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -98,8 +98,7 @@ const FileActions: FC = ({ onAction={(key) => { if (key === 'share') { onShareFile?.(file); - } - else if (key === 'delete') { + } else if (key === 'delete') { onDeleteFile?.(file); } }}> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx index 8ff2efeda505..4b00a5f7cb9b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/KnowledgePagesHierarchy/KnowledgePagesHierarchy.tsx @@ -11,11 +11,11 @@ * limitations under the License. */ import { - Typography as AntTypography, Button, Modal, Skeleton, Tree, + Typography as AntTypography, } from 'antd'; import { DataNode } from 'antd/es/tree'; import { AntTreeNodeProps, DirectoryTreeProps, TreeProps } from 'antd/lib/tree'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx index aa4c506476bf..128ee2a22c12 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/KnowledgeCenter/QuickLinkFormModal/QuickLinkFormModal.tsx @@ -78,7 +78,7 @@ export const QuickLinkFormModal: FC = ({ const [isUpdating, setIsUpdating] = useState(false); - useEffect(() => { + useEffect(() => { if (isOpen) { tagClassBase.setFilterClassification([]); } else { From c11dcab1eb15cb5044ea7a6293fa72a775553e1d Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 25 May 2026 13:24:03 +0530 Subject: [PATCH 15/17] addressed gitar comment --- .../ContextCenterArchivePage/ContextCenterArchivePage.tsx | 2 +- .../ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx | 2 +- openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx index 03e0dc80f26b..95b405b7c64d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterArchivePage/ContextCenterArchivePage.tsx @@ -80,7 +80,7 @@ const ContextCenterArchivePage: FC = () => { const [pagesResponse, files] = await Promise.all([ getListKnowledgePages({ include: Include.Deleted, - limit: 100, + limit: 1000, pageType: PageType.ARTICLE, }), listArchivedContextFiles(), diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx index b12d2273bf3f..b2a461ab6ca5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContextCenterPage/ContextCenterDocumentsPage/ContextCenterDocumentsPage.tsx @@ -176,7 +176,7 @@ const ContextCenterDocumentsPage: FC = () => { (file: DocFile, targetFolderId: string) => { setAllDocuments((prev) => prev.map((d) => - d.driveFileId === file.driveFileId + d.id === file.id ? { ...d, folderId: targetFolderId } : d ) diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts index ca98f5918cfa..26c1f610a8fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/assetAPI.ts @@ -135,7 +135,7 @@ export const deleteDriveFile = async ( export const listArchivedContextFiles = async (): Promise => { const response = await APIClient.get<{ data: ContextFile[] }>( '/contextCenter/drive/files', - { params: { include: 'deleted', limit: 100 } } + { params: { include: 'deleted', limit: 1000 } } ); return response.data.data ?? []; From 470efbc1ea400bed0071d71c70c360ccf9125f69 Mon Sep 17 00:00:00 2001 From: Rohit0301 Date: Mon, 25 May 2026 14:59:35 +0530 Subject: [PATCH 16/17] fixed minor issues --- .../ArchiveView/ArchiveView.component.tsx | 2 +- .../DocumentsView/DocumentFolderView.component.tsx | 4 ++-- .../DocumentsView/DocumentFolderView.test.tsx | 2 +- .../DocumentsView/DocumentsView.component.tsx | 8 ++++---- .../KnowledgeCenter/KnowledgeCard/KnowledgeCard.tsx | 2 +- .../KnowledgePagesHierarchy.tsx | 2 +- .../ContextCenterArchivePage.tsx | 10 +++++----- .../ContextCenterDocumentsPage.tsx | 2 +- .../main/resources/ui/src/rest/knowledgeCenterAPI.ts | 3 ++- .../main/resources/ui/src/utils/ContextCenterUtils.tsx | 2 +- 10 files changed, 19 insertions(+), 18 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx index 9683d9d123fb..3571ec4c1b7b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/ArchiveView/ArchiveView.component.tsx @@ -123,7 +123,7 @@ const ArchiveView: FC = ({ }) => { if (isLoading) { return ( - + {Array.from({ length: 8 }).map((_, idx) => ( ))} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx index a70f89d85eb9..20b6d8eef3ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentFolderView.component.tsx @@ -26,7 +26,7 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as FolderIcon } from '../../../assets/svg/ic-folder-new.svg'; import DeleteModal from '../../../components/common/DeleteModal/DeleteModal'; import { deleteFolder, listFolders } from '../../../rest/assetAPI'; -import { FileTypeBadge } from '../../../utils/ContextCenterUtils'; +import { FileTypeLabel } from '../../../utils/ContextCenterUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import CreateFolderModal from '../CreateFolderModal/CreateFolderModal.component'; import { DocFile } from './DocumentsView.interface'; @@ -202,7 +202,7 @@ const DocumentFolderView = ({ key={file.id} textValue={file.name}> - + ); jest.mock('utils/ContextCenterUtils', () => ({ - FileTypeBadge: jest.fn(({ fileType }: { fileType: string }) => ( + FileTypeLabel: jest.fn(({ fileType }: { fileType: string }) => ( {fileType} )), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx index 1e5ec60faa2c..e1b8ab1336f8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContextCenter/DocumentsView/DocumentsView.component.tsx @@ -39,7 +39,7 @@ import { useTranslation } from 'react-i18next'; import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { moveFileToFolder } from '../../../rest/assetAPI'; -import { FileTypeBadge } from '../../../utils/ContextCenterUtils'; +import { FileTypeLabel } from '../../../utils/ContextCenterUtils'; import { getShortRelativeTime } from '../../../utils/date-time/DateTimeUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { @@ -130,9 +130,9 @@ const FileActions: FC = ({ aria-hidden="true" className="tw:size-4 tw:shrink-0 tw:stroke-[2.25px] tw:text-fg-quaternary" /> - + {t('label.move-to-folder')} - +