From f6fb4df12ff999053bb7ba7ecadda36fce6814bc Mon Sep 17 00:00:00 2001 From: Klaus Keller Date: Wed, 27 Sep 2023 08:11:10 +0200 Subject: [PATCH 1/3] feat: switch to API v7 --- packages/core/src/guided-answers-api.ts | 2 +- packages/types/src/types.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/src/guided-answers-api.ts b/packages/core/src/guided-answers-api.ts index 45b29a8f..59ee97cb 100644 --- a/packages/core/src/guided-answers-api.ts +++ b/packages/core/src/guided-answers-api.ts @@ -27,7 +27,7 @@ import type { import { HTML_ENHANCEMENT_DATA_ATTR_MARKER } from '@sap/guided-answers-extension-types'; const API_HOST = 'https://ga.support.sap.com'; -const VERSION = 'v6'; +const VERSION = 'v7'; const NODE_PATH = `/dtp/api/${VERSION}/nodes/`; const TREE_PATH = `/dtp/api/${VERSION}/trees/`; const IMG_PREFIX = '/dtp/viewer/'; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 46d76163..40dec2ed 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -28,7 +28,15 @@ export interface GuidedAnswersQueryPagingOptions { offset: number; } -export type GuidedAnswerTreeSearchHit = GuidedAnswerTree & { SCORE: number }; +export interface GuidedAnswerTreeSearchAction { + DETAIL: string; + NODE_ID: GuidedAnswerNodeId; + SCORE: number; + TITLE: string; + TREE_ID: GuidedAnswerTreeId; +} + +export type GuidedAnswerTreeSearchHit = GuidedAnswerTree & { ACTION: GuidedAnswerTreeSearchAction; SCORE: number }; export type ProductFilter = { PRODUCT: string; COUNT: number }; From 933f28399fe35790994fd69e1920665d15957052 Mon Sep 17 00:00:00 2001 From: Klaus Keller Date: Wed, 27 Sep 2023 23:06:06 +0200 Subject: [PATCH 2/3] fix: unit test --- packages/core/test/guided-answers-api.test.ts | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/core/test/guided-answers-api.test.ts b/packages/core/test/guided-answers-api.test.ts index f22d6693..7a081bb6 100644 --- a/packages/core/test/guided-answers-api.test.ts +++ b/packages/core/test/guided-answers-api.test.ts @@ -40,7 +40,14 @@ describe('Guided Answers Api: getTrees()', () => { FIRST_NODE_ID: 100, SCORE: 0.1, COMPONENT: 'C1', - PRODUCT: 'P_one' + PRODUCT: 'P_one', + ACTION: { + DETAIL: 'detail', + NODE_ID: 11, + SCORE: 0.11, + TITLE: 'Node', + TREE_ID: 1 + } }, { TREE_ID: 2, @@ -50,7 +57,14 @@ describe('Guided Answers Api: getTrees()', () => { FIRST_NODE_ID: 200, SCORE: 0.2, COMPONENT: 'C2', - PRODUCT: 'P_two' + PRODUCT: 'P_two', + ACTION: { + DETAIL: 'detail', + NODE_ID: 12, + SCORE: 0.12, + TITLE: 'Node', + TREE_ID: 2 + } }, { TREE_ID: 3, @@ -60,7 +74,14 @@ describe('Guided Answers Api: getTrees()', () => { FIRST_NODE_ID: 300, SCORE: 0.3, COMPONENT: 'C3', - PRODUCT: 'P_three' + PRODUCT: 'P_three', + ACTION: { + DETAIL: 'detail', + NODE_ID: 13, + SCORE: 0.13, + TITLE: 'Node', + TREE_ID: 3 + } } ], resultSize: 3, @@ -90,7 +111,14 @@ describe('Guided Answers Api: getTrees()', () => { FIRST_NODE_ID: 100, SCORE: 0.1, COMPONENT: 'C1', - PRODUCT: 'P_one' + PRODUCT: 'P_one', + ACTION: { + DETAIL: 'detail', + NODE_ID: 11, + SCORE: 0.11, + TITLE: 'Node', + TREE_ID: 1 + } }, { TREE_ID: 2, @@ -100,7 +128,14 @@ describe('Guided Answers Api: getTrees()', () => { FIRST_NODE_ID: 200, SCORE: 0.2, COMPONENT: 'C2', - PRODUCT: 'P_two' + PRODUCT: 'P_two', + ACTION: { + DETAIL: 'detail', + NODE_ID: 12, + SCORE: 0.12, + TITLE: 'Node', + TREE_ID: 2 + } }, { TREE_ID: 3, @@ -110,7 +145,14 @@ describe('Guided Answers Api: getTrees()', () => { FIRST_NODE_ID: 300, SCORE: 0.3, COMPONENT: 'C3', - PRODUCT: 'P_three' + PRODUCT: 'P_three', + ACTION: { + DETAIL: 'detail', + NODE_ID: 13, + SCORE: 0.13, + TITLE: 'Node', + TREE_ID: 3 + } } ], resultSize: 3, From fbf0a85762c9c224df117e91d31ae52bf6a23fb6 Mon Sep 17 00:00:00 2001 From: Olivers Berzs Date: Fri, 6 Oct 2023 14:17:22 +0300 Subject: [PATCH 3/3] feat(309): update search results page --- packages/core/test/guided-answers-api.test.ts | 48 +- packages/types/src/actions.ts | 11 +- packages/types/src/types.ts | 24 +- packages/webapp/src/webview/i18n/i18n.json | 5 +- packages/webapp/src/webview/state/actions.ts | 5 +- packages/webapp/src/webview/state/reducers.ts | 56 ++- packages/webapp/src/webview/types.ts | 6 +- .../src/webview/ui/components/App/App.tsx | 83 +--- .../CollapseButtons/CollapseButtons.tsx | 37 ++ .../Header/CollapseButtons/index.ts | 1 + .../ui/components/Header/Filters/Filters.scss | 16 +- .../ui/components/Header/Filters/Filters.tsx | 78 ++-- .../webview/ui/components/Header/Header.scss | 3 +- .../Header/SearchField/SearchField.tsx | 13 +- .../SearchResults/SearchResults.scss | 95 ++++ .../SearchResults/SearchResults.tsx | 59 +++ .../SearchResults/SearchResultsNode.tsx | 54 +++ .../SearchResults/SearchResultsTree.tsx | 75 ++++ .../ui/components/SearchResults/index.ts | 2 + .../test/Header/CollapseButtons.test.tsx | 75 ++++ .../CollapseButtons.test.tsx.snap | 87 ++++ .../__snapshots__/Filters.test.tsx.snap | 230 +++++----- .../__snapshots__/SearchField.test.tsx.snap | 118 +++-- .../test/SearchResults/SearchResults.test.tsx | 94 ++++ .../SearchResults/SearchResultsTree.test.tsx | 157 +++++++ .../__snapshots__/SearchResults.test.tsx.snap | 253 +++++++++++ .../SearchResultsTree.test.tsx.snap | 424 ++++++++++++++++++ packages/webapp/test/State/Reducers.test.ts | 107 ++++- 28 files changed, 1866 insertions(+), 350 deletions(-) create mode 100644 packages/webapp/src/webview/ui/components/Header/CollapseButtons/CollapseButtons.tsx create mode 100644 packages/webapp/src/webview/ui/components/Header/CollapseButtons/index.ts create mode 100644 packages/webapp/src/webview/ui/components/SearchResults/SearchResults.scss create mode 100644 packages/webapp/src/webview/ui/components/SearchResults/SearchResults.tsx create mode 100644 packages/webapp/src/webview/ui/components/SearchResults/SearchResultsNode.tsx create mode 100644 packages/webapp/src/webview/ui/components/SearchResults/SearchResultsTree.tsx create mode 100644 packages/webapp/src/webview/ui/components/SearchResults/index.ts create mode 100644 packages/webapp/test/Header/CollapseButtons.test.tsx create mode 100644 packages/webapp/test/Header/__snapshots__/CollapseButtons.test.tsx.snap create mode 100644 packages/webapp/test/SearchResults/SearchResults.test.tsx create mode 100644 packages/webapp/test/SearchResults/SearchResultsTree.test.tsx create mode 100644 packages/webapp/test/SearchResults/__snapshots__/SearchResults.test.tsx.snap create mode 100644 packages/webapp/test/SearchResults/__snapshots__/SearchResultsTree.test.tsx.snap diff --git a/packages/core/test/guided-answers-api.test.ts b/packages/core/test/guided-answers-api.test.ts index 7a081bb6..0d723d3f 100644 --- a/packages/core/test/guided-answers-api.test.ts +++ b/packages/core/test/guided-answers-api.test.ts @@ -41,13 +41,7 @@ describe('Guided Answers Api: getTrees()', () => { SCORE: 0.1, COMPONENT: 'C1', PRODUCT: 'P_one', - ACTION: { - DETAIL: 'detail', - NODE_ID: 11, - SCORE: 0.11, - TITLE: 'Node', - TREE_ID: 1 - } + ACTIONS: [] }, { TREE_ID: 2, @@ -58,13 +52,7 @@ describe('Guided Answers Api: getTrees()', () => { SCORE: 0.2, COMPONENT: 'C2', PRODUCT: 'P_two', - ACTION: { - DETAIL: 'detail', - NODE_ID: 12, - SCORE: 0.12, - TITLE: 'Node', - TREE_ID: 2 - } + ACTIONS: [] }, { TREE_ID: 3, @@ -75,13 +63,7 @@ describe('Guided Answers Api: getTrees()', () => { SCORE: 0.3, COMPONENT: 'C3', PRODUCT: 'P_three', - ACTION: { - DETAIL: 'detail', - NODE_ID: 13, - SCORE: 0.13, - TITLE: 'Node', - TREE_ID: 3 - } + ACTIONS: [] } ], resultSize: 3, @@ -112,13 +94,7 @@ describe('Guided Answers Api: getTrees()', () => { SCORE: 0.1, COMPONENT: 'C1', PRODUCT: 'P_one', - ACTION: { - DETAIL: 'detail', - NODE_ID: 11, - SCORE: 0.11, - TITLE: 'Node', - TREE_ID: 1 - } + ACTIONS: [] }, { TREE_ID: 2, @@ -129,13 +105,7 @@ describe('Guided Answers Api: getTrees()', () => { SCORE: 0.2, COMPONENT: 'C2', PRODUCT: 'P_two', - ACTION: { - DETAIL: 'detail', - NODE_ID: 12, - SCORE: 0.12, - TITLE: 'Node', - TREE_ID: 2 - } + ACTIONS: [] }, { TREE_ID: 3, @@ -146,13 +116,7 @@ describe('Guided Answers Api: getTrees()', () => { SCORE: 0.3, COMPONENT: 'C3', PRODUCT: 'P_three', - ACTION: { - DETAIL: 'detail', - NODE_ID: 13, - SCORE: 0.13, - TITLE: 'Node', - TREE_ID: 3 - } + ACTIONS: [] } ], resultSize: 3, diff --git a/packages/types/src/actions.ts b/packages/types/src/actions.ts index 96123c77..52dcd7ed 100644 --- a/packages/types/src/actions.ts +++ b/packages/types/src/actions.ts @@ -84,7 +84,10 @@ import { GET_LAST_VISITED_GUIDES, UPDATE_LAST_VISITED_GUIDES, GO_TO_HOME_PAGE, - SET_QUICK_FILTERS + SET_QUICK_FILTERS, + EXPAND_ALL_SEARCH_NODES, + COLLAPSE_ALL_SEARCH_NODES, + EXPAND_SEARCH_NODES_FOR_TREE } from './types'; export const updateGuidedAnswerTrees = (payload: UpdateGuidedAnswerTrees['payload']): UpdateGuidedAnswerTrees => ({ @@ -247,3 +250,9 @@ export const setQuickFilters = (payload: GuidedAnswersQueryFilterOptions[]): Set type: SET_QUICK_FILTERS, payload }); + +export const expandAllSearchNodes = () => ({ type: EXPAND_ALL_SEARCH_NODES }); + +export const collapseAllSearchNodes = () => ({ type: COLLAPSE_ALL_SEARCH_NODES }); + +export const expandSearchNodesForTree = (payload: number) => ({ type: EXPAND_SEARCH_NODES_FOR_TREE, payload }); diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 40dec2ed..3f0c6d65 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -36,7 +36,7 @@ export interface GuidedAnswerTreeSearchAction { TREE_ID: GuidedAnswerTreeId; } -export type GuidedAnswerTreeSearchHit = GuidedAnswerTree & { ACTION: GuidedAnswerTreeSearchAction; SCORE: number }; +export type GuidedAnswerTreeSearchHit = GuidedAnswerTree & { ACTIONS: GuidedAnswerTreeSearchAction[]; SCORE: number }; export type ProductFilter = { PRODUCT: string; COUNT: number }; @@ -227,7 +227,10 @@ export type GuidedAnswerActions = | UpdateGuidedAnswerTrees | UpdateNetworkStatus | WebviewReady - | SetQuickFilters; + | SetQuickFilters + | ExpandAllSearchNodes + | CollapseAllSearchNodes + | ExpandSearchNodesForTree; export type NetworkStatus = 'OK' | 'LOADING' | 'ERROR'; @@ -249,6 +252,7 @@ export interface AppState { activeScreen: 'HOME' | 'SEARCH' | 'NODE'; lastVisitedGuides: LastVisitedGuide[]; quickFilters: GuidedAnswersQueryFilterOptions[]; + searchResultVisibleNodeCount: Record; } export const UPDATE_GUIDED_ANSWER_TREES = 'UPDATE_GUIDED_ANSWER_TREES'; @@ -460,3 +464,19 @@ export interface SetQuickFilters { type: typeof SET_QUICK_FILTERS; payload: GuidedAnswersQueryFilterOptions[]; } + +export const EXPAND_ALL_SEARCH_NODES = 'EXPAND_ALL_SEARCH_NODES'; +export interface ExpandAllSearchNodes { + type: typeof EXPAND_ALL_SEARCH_NODES; +} + +export const COLLAPSE_ALL_SEARCH_NODES = 'COLLAPSE_ALL_SEARCH_NODES'; +export interface CollapseAllSearchNodes { + type: typeof COLLAPSE_ALL_SEARCH_NODES; +} + +export const EXPAND_SEARCH_NODES_FOR_TREE = 'EXPAND_SEARCH_NODES_FOR_TREE'; +export interface ExpandSearchNodesForTree { + type: typeof EXPAND_SEARCH_NODES_FOR_TREE; + payload: number; +} diff --git a/packages/webapp/src/webview/i18n/i18n.json b/packages/webapp/src/webview/i18n/i18n.json index e48d4b61..176bf201 100644 --- a/packages/webapp/src/webview/i18n/i18n.json +++ b/packages/webapp/src/webview/i18n/i18n.json @@ -39,5 +39,8 @@ "COPIED_TO_CLIPBOARD_DESC": "Copy this link to share via email or messages. When pasted into the search input field of the Guided Answers extension, it will navigate you straight to this node.", "VIEW_ON_WEBSITE": "View on the GA website", "COPY_WITH_INSTRUCTIONS": "Copy link with instructions", - "COPY_WITH_INSTRUCTIONS_TEXT": "To resolve your reported issue, open Guided Answers extension by SAP, and follow the steps mentioned in the guide.\n If your IDE is VSCode, please click on the following link: {{-extensionLink}} .\n If your IDE is SAP Business Application Studio, then launch Guided Answers via the command \"SAP: Open Guided Answers\" and paste the following guide shortlink into the search input field: {{-extensionLink}} ." + "COPY_WITH_INSTRUCTIONS_TEXT": "To resolve your reported issue, open Guided Answers extension by SAP, and follow the steps mentioned in the guide.\n If your IDE is VSCode, please click on the following link: {{-extensionLink}} .\n If your IDE is SAP Business Application Studio, then launch Guided Answers via the command \"SAP: Open Guided Answers\" and paste the following guide shortlink into the search input field: {{-extensionLink}} .", + "VIEW_MORE_RESULTS": "View {{-count}} more results", + "EXPAND_ALL_TREES": "Expand all trees", + "COLLAPSE_ALL_TREES": "Collapse all trees" } diff --git a/packages/webapp/src/webview/state/actions.ts b/packages/webapp/src/webview/state/actions.ts index 0c21cae6..112bf414 100644 --- a/packages/webapp/src/webview/state/actions.ts +++ b/packages/webapp/src/webview/state/actions.ts @@ -30,5 +30,8 @@ export { getLastVisitedGuides, updateBookmark, synchronizeBookmark, - updateLastVisitedGuide + updateLastVisitedGuide, + expandAllSearchNodes, + collapseAllSearchNodes, + expandSearchNodesForTree } from '@sap/guided-answers-extension-types'; diff --git a/packages/webapp/src/webview/state/reducers.ts b/packages/webapp/src/webview/state/reducers.ts index 5b7d8375..fec3574c 100644 --- a/packages/webapp/src/webview/state/reducers.ts +++ b/packages/webapp/src/webview/state/reducers.ts @@ -19,7 +19,8 @@ import type { FeedbackStatus, GuidedAnswerNode, GetLastVisitedGuides, - SetQuickFilters + SetQuickFilters, + ExpandSearchNodesForTree } from '@sap/guided-answers-extension-types'; import i18next from 'i18next'; import type { Reducer } from 'redux'; @@ -52,7 +53,8 @@ export function getInitialState(): AppState { bookmarks: {}, activeScreen: 'HOME', lastVisitedGuides: [], - quickFilters: [] + quickFilters: [], + searchResultVisibleNodeCount: {} }; } @@ -99,7 +101,10 @@ const reducers: Partial = { GET_BOOKMARKS: getBookmarksReducer, UPDATE_BOOKMARKS: updateBookmarksReducer, GET_LAST_VISITED_GUIDES: getLastVisitedGuidesReducer, - SET_QUICK_FILTERS: setQuickFiltersReducer + SET_QUICK_FILTERS: setQuickFiltersReducer, + EXPAND_ALL_SEARCH_NODES: expandAllSearchNodes, + COLLAPSE_ALL_SEARCH_NODES: collapseAllSearchNodes, + EXPAND_SEARCH_NODES_FOR_TREE: expandSearchNodesForTree }; /** @@ -143,6 +148,9 @@ function updateGuidedAnswerTreesReducer(newState: AppState, action: UpdateGuided if ((action.payload?.pagingOptions?.offset ?? 0) > 0) { newState.guidedAnswerTreeSearchResult.trees.unshift(...trees); } + for (const tree of newState.guidedAnswerTreeSearchResult.trees) { + newState.searchResultVisibleNodeCount[tree.TREE_ID] = Math.min(tree.ACTIONS.length, 2); + } delete newState.activeGuidedAnswer; newState.activeScreen = 'SEARCH'; return newState; @@ -289,6 +297,7 @@ function setQueryValueReducer(newState: AppState, action: SetQueryValue): AppSta productFilters: [], trees: [] }; + newState.searchResultVisibleNodeCount = {}; newState.query = action.payload; return newState; } @@ -463,6 +472,47 @@ function setQuickFiltersReducer(newState: AppState, action: SetQuickFilters): Ap return newState; } +/** + * Expand all search nodes. + * + * @param newState - already cloned state that is modified and returned + * @returns new state with changes + */ +function expandAllSearchNodes(newState: AppState): AppState { + for (const tree of newState.guidedAnswerTreeSearchResult.trees) { + newState.searchResultVisibleNodeCount[tree.TREE_ID] = tree.ACTIONS.length; + } + return newState; +} + +/** + * Collapse all search nodes. + * + * @param newState - already cloned state that is modified and returned + * @returns new state with changes + */ +function collapseAllSearchNodes(newState: AppState): AppState { + for (const tree of newState.guidedAnswerTreeSearchResult.trees) { + newState.searchResultVisibleNodeCount[tree.TREE_ID] = 0; + } + return newState; +} + +/** + * Expand search nodes for a specific tree. + * + * @param newState - already cloned state that is modified and returned + * @param action - action with payload + * @returns new state with changes + */ +function expandSearchNodesForTree(newState: AppState, action: ExpandSearchNodesForTree): AppState { + const tree = newState.guidedAnswerTreeSearchResult.trees.find((t) => t.TREE_ID === action.payload); + if (tree) { + newState.searchResultVisibleNodeCount[tree.TREE_ID] = tree.ACTIONS.length; + } + return newState; +} + /** * Restore the state, happens after deserializing of the webview panel. * diff --git a/packages/webapp/src/webview/types.ts b/packages/webapp/src/webview/types.ts index ff9fceeb..1ef925b0 100644 --- a/packages/webapp/src/webview/types.ts +++ b/packages/webapp/src/webview/types.ts @@ -1 +1,5 @@ -export type { AppState } from '@sap/guided-answers-extension-types'; +export type { + AppState, + GuidedAnswerTreeSearchHit, + GuidedAnswerTreeSearchAction +} from '@sap/guided-answers-extension-types'; diff --git a/packages/webapp/src/webview/ui/components/App/App.tsx b/packages/webapp/src/webview/ui/components/App/App.tsx index 1ca10fb8..0a5b589e 100644 --- a/packages/webapp/src/webview/ui/components/App/App.tsx +++ b/packages/webapp/src/webview/ui/components/App/App.tsx @@ -7,14 +7,12 @@ import { GuidedAnswerNode } from '../GuidedAnswerNode'; import { Header } from '../Header'; import { ErrorScreen } from '../ErrorScreen'; import { FiltersRibbon } from '../Header/Filters'; -import { FocusZone, FocusZoneDirection } from '@fluentui/react-focus'; -import './App.scss'; -import { initIcons, UILoader, UIIcon, UiIcons } from '@sap-ux/ui-components'; -import InfiniteScroll from 'react-infinite-scroll-component'; +import { initIcons, UILoader } from '@sap-ux/ui-components'; import { SpinnerSize } from '@fluentui/react'; import i18next from 'i18next'; -import { TreeItemBottomSection } from '../TreeItemBottomSection'; import { HomeGrid } from '../HomeGrid'; +import { SearchResults } from '../SearchResults'; +import './App.scss'; initIcons(); @@ -50,33 +48,6 @@ export function App(): ReactElement { }; }, []); - function fetchData() { - if (appState.guidedAnswerTreeSearchResult.resultSize > appState.pageSize) { - actions.searchTree({ - query: appState.query, - filters: { - product: appState.selectedProductFilters, - component: appState.selectedComponentFilters - }, - paging: { - responseSize: appState.pageSize, - offset: appState.guidedAnswerTreeSearchResult.trees.length - } - }); - } - } - - /** - * Check if a tree is bookmarked. - * - * @param treeId - id of the tree - * @param treeId.toString - id a string - * @returns boolean - */ - function isBookmark(treeId: { toString: () => string }): boolean { - return !!Object.keys(appState.bookmarks).find((bookmarkKey) => bookmarkKey.startsWith(`${treeId}-`)); - } - let content; if (appState.networkStatus === 'LOADING') { content = ; @@ -87,53 +58,7 @@ export function App(): ReactElement { } else if (appState.activeScreen === 'HOME') { content = ; } else { - content = - appState.guidedAnswerTreeSearchResult.resultSize === 0 ? ( - - ) : ( - -
    - } - hasMore={ - appState.guidedAnswerTreeSearchResult.trees.length < - appState.guidedAnswerTreeSearchResult.resultSize - }> - {appState.guidedAnswerTreeSearchResult.trees.map((tree) => { - return ( -
  • - -
  • - ); - })} -
    -
-
- ); + content = ; } return (
diff --git a/packages/webapp/src/webview/ui/components/Header/CollapseButtons/CollapseButtons.tsx b/packages/webapp/src/webview/ui/components/Header/CollapseButtons/CollapseButtons.tsx new file mode 100644 index 00000000..cce7a799 --- /dev/null +++ b/packages/webapp/src/webview/ui/components/Header/CollapseButtons/CollapseButtons.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import i18next from 'i18next'; +import { useSelector } from 'react-redux'; +import { UIIconButton, UiIcons } from '@sap-ux/ui-components'; +import { actions } from '../../../../state'; +import type { AppState } from '../../../../types'; + +/** + * + * @returns Collapse buttons for the header + */ +export function CollapseButtons() { + const appState = useSelector((state) => state); + + return ( + <> + actions.expandAllSearchNodes()} + primary> + actions.collapseAllSearchNodes()} + primary> + + ); +} diff --git a/packages/webapp/src/webview/ui/components/Header/CollapseButtons/index.ts b/packages/webapp/src/webview/ui/components/Header/CollapseButtons/index.ts new file mode 100644 index 00000000..765074b6 --- /dev/null +++ b/packages/webapp/src/webview/ui/components/Header/CollapseButtons/index.ts @@ -0,0 +1 @@ +export * from './CollapseButtons'; diff --git a/packages/webapp/src/webview/ui/components/Header/Filters/Filters.scss b/packages/webapp/src/webview/ui/components/Header/Filters/Filters.scss index 5fd7d870..5120311d 100644 --- a/packages/webapp/src/webview/ui/components/Header/Filters/Filters.scss +++ b/packages/webapp/src/webview/ui/components/Header/Filters/Filters.scss @@ -10,11 +10,19 @@ ul.filter-list { } .filter-button { - width: 18px; - height: 18px; - border-radius: 2px; - margin-left: 8px; + padding: 5px; + + svg { + width: 16px; + height: 16px; + min-width: 16px; + } + + i { + height: 16px; + } } + .filter-button:focus-visible { outline: 1px solid var(--vscode-focusBorder, #007fd4); outline-offset: -1px; diff --git a/packages/webapp/src/webview/ui/components/Header/Filters/Filters.tsx b/packages/webapp/src/webview/ui/components/Header/Filters/Filters.tsx index e32e0838..cd5f59c4 100644 --- a/packages/webapp/src/webview/ui/components/Header/Filters/Filters.tsx +++ b/packages/webapp/src/webview/ui/components/Header/Filters/Filters.tsx @@ -227,46 +227,44 @@ export function Filters() { }; return ( <> -
- toggleFilters(PRODUCTS)} - disabled={appState.guidedAnswerTreeSearchResult.productFilters.length === 0} - className={`filter-button ${selectedProductFilters.length > 0 ? 'filter-button-selected' : ''}`} - primary - title="Filter Products"> - toggleFilters(COMPONENTS)} - disabled={appState.guidedAnswerTreeSearchResult.componentFilters.length === 0} - primary - title="Filter Components" - className={`filter-button ${ - selectedComponentFilters.length > 0 ? 'filter-button-selected' : '' - }`}> - -
+ toggleFilters(PRODUCTS)} + disabled={appState.guidedAnswerTreeSearchResult.productFilters.length === 0} + className={`filter-button ${selectedProductFilters.length > 0 ? 'filter-button-selected' : ''}`} + primary + title="Filter Products"> + toggleFilters(COMPONENTS)} + disabled={appState.guidedAnswerTreeSearchResult.componentFilters.length === 0} + primary + title="Filter Components" + className={`filter-button ${ + selectedComponentFilters.length > 0 ? 'filter-button-selected' : '' + }`}> + ); } diff --git a/packages/webapp/src/webview/ui/components/Header/Header.scss b/packages/webapp/src/webview/ui/components/Header/Header.scss index 990fd42e..a37abfa7 100644 --- a/packages/webapp/src/webview/ui/components/Header/Header.scss +++ b/packages/webapp/src/webview/ui/components/Header/Header.scss @@ -102,8 +102,9 @@ &__searchField { display: flex; - align-items: start; + align-items: center; flex-direction: row; + gap: 5px; margin-top: 40px; width: 66%; diff --git a/packages/webapp/src/webview/ui/components/Header/SearchField/SearchField.tsx b/packages/webapp/src/webview/ui/components/Header/SearchField/SearchField.tsx index 6f76687c..56838837 100644 --- a/packages/webapp/src/webview/ui/components/Header/SearchField/SearchField.tsx +++ b/packages/webapp/src/webview/ui/components/Header/SearchField/SearchField.tsx @@ -3,8 +3,8 @@ import { useSelector } from 'react-redux'; import type { AppState } from '../../../../types'; import { actions } from '../../../../state'; import { UISearchBox } from '@sap-ux/ui-components'; - import { Filters } from '../Filters'; +import { CollapseButtons } from '../CollapseButtons'; let timer: NodeJS.Timeout; /** @@ -42,7 +42,16 @@ export function SearchField() { id="search-field" onClear={() => onChange('')} onChange={(e: any) => onChange(e?.target?.value || '')}> - {appState.activeScreen === 'SEARCH' && } + + {appState.activeScreen === 'SEARCH' && ( + <> + +
+ + + )}
); } diff --git a/packages/webapp/src/webview/ui/components/SearchResults/SearchResults.scss b/packages/webapp/src/webview/ui/components/SearchResults/SearchResults.scss new file mode 100644 index 00000000..49f80f60 --- /dev/null +++ b/packages/webapp/src/webview/ui/components/SearchResults/SearchResults.scss @@ -0,0 +1,95 @@ +.guided-answer__search-results { + &__node-list { + padding-bottom: 10px; + + >ul { + padding: 0; + } + } + + &__separator { + margin: 0 15px 5px 15px; + border: none; + border-top: 1px solid var(--vscode-editorWidget-border); + } + + &__view-more { + margin: 10px 0 0 30px; + cursor: pointer; + + &:hover { + >span { + text-decoration-line: none; + } + } + + >i { + height: 16px; + margin-right: 5px; + vertical-align: middle; + } + + >span { + color: var(--vscode-foreground); + font-size: 11px; + font-weight: 400; + line-height: 16px; + text-decoration-line: underline; + vertical-align: middle; + } + } + + &__node { + list-style-type: none; + + >button { + display: flex; + width: 100%; + padding: 5px 15px 5px 15px; + + &:hover { + background-color: var(--vscode-list-hoverBackground); + outline: 1px solid var(--vscode-focusBorder, #007fd4); + outline-offset: -1px; + } + + &:focus-visible { + outline: 1px solid var(--vscode-focusBorder, #007fd4); + outline-offset: -2px; + } + } + + &__indent { + width: 10px; + height: 5px; + margin: 3px 5px 0 0; + border-bottom: 1px solid var(--vscode-editorWidget-border); + border-left: 1px solid var(--vscode-editorWidget-border); + } + + &__title { + display: flex; + align-items: center; + gap: 5px; + margin: 0 0 5px 0; + padding: 0; + font-size: 11px; + line-height: 16px; + color: var(--vscode-settings-headerForeground); + text-align: left; + + >i { + height: 16px; + } + } + + &__detail { + margin: 0; + padding: 0; + font-size: 11px; + line-height: 16px; + color: var(--vscode-foreground); + text-align: left; + } + } +} diff --git a/packages/webapp/src/webview/ui/components/SearchResults/SearchResults.tsx b/packages/webapp/src/webview/ui/components/SearchResults/SearchResults.tsx new file mode 100644 index 00000000..1bc0a2f8 --- /dev/null +++ b/packages/webapp/src/webview/ui/components/SearchResults/SearchResults.tsx @@ -0,0 +1,59 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import i18next from 'i18next'; +import { FocusZone, FocusZoneDirection } from '@fluentui/react-focus'; +import { SpinnerSize } from '@fluentui/react'; +import { UILoader } from '@sap-ux/ui-components'; +import { actions } from '../../../state'; +import type { AppState } from '../../../types'; +import { ErrorScreen } from '../ErrorScreen'; +import { SearchResultsTree } from './SearchResultsTree'; +import './SearchResults.scss'; + +/** + * Search results screen for Guided Answers Extension app. + * + * @returns - react elements for the search results + */ +export function SearchResults(): ReactElement { + const appState = useSelector((state) => state); + + const fetchData = () => { + if (appState.guidedAnswerTreeSearchResult.resultSize > appState.pageSize) { + actions.searchTree({ + query: appState.query, + filters: { + product: appState.selectedProductFilters, + component: appState.selectedComponentFilters + }, + paging: { + responseSize: appState.pageSize, + offset: appState.guidedAnswerTreeSearchResult.trees.length + } + }); + } + }; + + return appState.guidedAnswerTreeSearchResult.resultSize === 0 ? ( + + ) : ( + +
    + } + hasMore={ + appState.guidedAnswerTreeSearchResult.trees.length < + appState.guidedAnswerTreeSearchResult.resultSize + }> + {appState.guidedAnswerTreeSearchResult.trees.map((tree) => ( + + ))} + +
+
+ ); +} diff --git a/packages/webapp/src/webview/ui/components/SearchResults/SearchResultsNode.tsx b/packages/webapp/src/webview/ui/components/SearchResults/SearchResultsNode.tsx new file mode 100644 index 00000000..6ad80c5c --- /dev/null +++ b/packages/webapp/src/webview/ui/components/SearchResults/SearchResultsNode.tsx @@ -0,0 +1,54 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import { UIIcon, UiIcons } from '@sap-ux/ui-components'; +import type { AppState, GuidedAnswerTreeSearchAction } from '../../../types'; + +/** + * Search results node item for Guided Answers Extension app. + * + * @param props - properties containing node + * @param props.node - Guided Answers Node search hit + * @param props.onClick - Node onClick handler + * @returns - react elements for the search results node item + */ +export function SearchResultsNode({ + node, + onClick +}: { + node: GuidedAnswerTreeSearchAction; + onClick: React.MouseEventHandler; +}): ReactElement { + const appState = useSelector((state) => state); + const isBookmark = !!Object.keys(appState.bookmarks).find( + (key) => key.startsWith(`${node.TREE_ID}-`) && key.endsWith(`${node.NODE_ID}`) + ); + + console.log(node.DETAIL); + + // the API as of now returns descriptions with HTML tags, complete and incomplete, + // we should remove them + // const trimmed = node.DETAIL.replace('') + // const cleanerDiv = document.createElement('div'); + // cleanerDiv.innerHTML = node.DETAIL; + // const cleanedDetail = cleanerDiv.innerText; + // console.log('cleaned:', cleanedDetail); + + return ( +
  • + +
  • + ); +} diff --git a/packages/webapp/src/webview/ui/components/SearchResults/SearchResultsTree.tsx b/packages/webapp/src/webview/ui/components/SearchResults/SearchResultsTree.tsx new file mode 100644 index 00000000..31c560c9 --- /dev/null +++ b/packages/webapp/src/webview/ui/components/SearchResults/SearchResultsTree.tsx @@ -0,0 +1,75 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { useSelector } from 'react-redux'; +import i18next from 'i18next'; +import { UIIcon, UiIcons } from '@sap-ux/ui-components'; +import type { AppState, GuidedAnswerTreeSearchHit } from '../../../types'; +import { TreeItemBottomSection } from '../TreeItemBottomSection'; +import { actions } from '../../../state'; +import { SearchResultsNode } from './SearchResultsNode'; + +/** + * Search results tree item for Guided Answers Extension app. + * + * @param props - properties containing tree + * @param props.tree - Guided Answers Tree search hit + * @returns - react elements for the search results tree item + */ +export function SearchResultsTree({ tree }: { tree: GuidedAnswerTreeSearchHit }): ReactElement { + const appState = useSelector((state) => state); + const isBookmark = !!Object.keys(appState.bookmarks).find((key) => key.startsWith(`${tree.TREE_ID}-`)); + const visibleNodeCount = appState.searchResultVisibleNodeCount[tree.TREE_ID]; + const hiddenNodeCount = tree.ACTIONS.length - visibleNodeCount; + + const goToNode = (nodeId: number) => { + actions.setActiveTree(tree); + actions.selectNode(nodeId); + document.body.focus(); + }; + + return ( +
  • + + + {tree.ACTIONS.length > 0 && ( +
    +
    + +
      + {tree.ACTIONS.slice(0, visibleNodeCount).map((node) => ( + goToNode(node.NODE_ID)} + /> + ))} +
    + + {hiddenNodeCount > 0 && ( + actions.expandSearchNodesForTree(tree.TREE_ID)}> + + {i18next.t('VIEW_MORE_RESULTS', { count: hiddenNodeCount })} + + )} +
    + )} +
  • + ); +} diff --git a/packages/webapp/src/webview/ui/components/SearchResults/index.ts b/packages/webapp/src/webview/ui/components/SearchResults/index.ts new file mode 100644 index 00000000..d8418fcd --- /dev/null +++ b/packages/webapp/src/webview/ui/components/SearchResults/index.ts @@ -0,0 +1,2 @@ +export * from './SearchResults'; +export * from './SearchResultsTree'; diff --git a/packages/webapp/test/Header/CollapseButtons.test.tsx b/packages/webapp/test/Header/CollapseButtons.test.tsx new file mode 100644 index 00000000..9e1986e9 --- /dev/null +++ b/packages/webapp/test/Header/CollapseButtons.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import { fireEvent } from '@testing-library/dom'; +import { useSelector } from 'react-redux'; +import { actions } from '../../src/webview/state'; +import { CollapseButtons } from '../../src/webview/ui/components/Header/CollapseButtons'; + +jest.mock('../../src/webview/state', () => ({ + actions: { + expandAllSearchNodes: jest.fn(), + collapseAllSearchNodes: jest.fn() + } +})); + +jest.mock('react-redux', () => ({ + useSelector: jest.fn() +})); + +describe('', () => { + afterEach(cleanup); + + it('renders without crashing', () => { + (useSelector as jest.Mock).mockImplementation((selector) => + selector({ + guidedAnswerTreeSearchResult: { + resultSize: 1 + } + }) + ); + + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('renders without crashing, disabled buttons', () => { + (useSelector as jest.Mock).mockImplementation((selector) => + selector({ + guidedAnswerTreeSearchResult: { + resultSize: 0 + } + }) + ); + + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('expands all trees', () => { + (useSelector as jest.Mock).mockImplementation((selector) => + selector({ + guidedAnswerTreeSearchResult: { + resultSize: 1 + } + }) + ); + + render(); + fireEvent.click(screen.getByTestId('expand-all-button')); + expect(actions.expandAllSearchNodes).toBeCalledTimes(1); + }); + + it('collapse all trees', () => { + (useSelector as jest.Mock).mockImplementation((selector) => + selector({ + guidedAnswerTreeSearchResult: { + resultSize: 1 + } + }) + ); + + render(); + fireEvent.click(screen.getByTestId('collapse-all-button')); + expect(actions.collapseAllSearchNodes).toBeCalledTimes(1); + }); +}); diff --git a/packages/webapp/test/Header/__snapshots__/CollapseButtons.test.tsx.snap b/packages/webapp/test/Header/__snapshots__/CollapseButtons.test.tsx.snap new file mode 100644 index 00000000..0ea54b46 --- /dev/null +++ b/packages/webapp/test/Header/__snapshots__/CollapseButtons.test.tsx.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders without crashing 1`] = ` +
    +