diff --git a/src/app/services/actions/autocomplete-action-creators.spec.ts b/src/app/services/actions/autocomplete-action-creators.spec.ts index e9b932fafa..3703655089 100644 --- a/src/app/services/actions/autocomplete-action-creators.spec.ts +++ b/src/app/services/actions/autocomplete-action-creators.spec.ts @@ -103,14 +103,20 @@ const mockState: ApplicationState = { permissions: [], error: null }, - collections: [], + collections: { + collections: [], + saved: false + }, proxyUrl: '' } store.getState = () => ({ ...mockState, proxyUrl: '', - collections: [], + collections: { + collections: [], + saved: false + }, graphExplorerMode: Mode.Complete, queryRunnerStatus: null, samples: { diff --git a/src/app/services/actions/permissions-action-creator.spec.ts b/src/app/services/actions/permissions-action-creator.spec.ts index 999336a6b6..7fc00df23e 100644 --- a/src/app/services/actions/permissions-action-creator.spec.ts +++ b/src/app/services/actions/permissions-action-creator.spec.ts @@ -124,7 +124,10 @@ const mockState: ApplicationState = { permissions: [], error: null }, - collections: [], + collections: { + collections: [], + saved: false + }, proxyUrl: '' } const currentState = store.getState(); diff --git a/src/app/services/actions/resource-explorer-action-creators.spec.ts b/src/app/services/actions/resource-explorer-action-creators.spec.ts index edba19e182..95b2fc9ee0 100644 --- a/src/app/services/actions/resource-explorer-action-creators.spec.ts +++ b/src/app/services/actions/resource-explorer-action-creators.spec.ts @@ -99,7 +99,10 @@ const mockState: ApplicationState = { data: {}, error: null }, - collections: [], + collections: { + collections: [], + saved: false + }, proxyUrl: '' } diff --git a/src/app/services/context/collection-permissions/CollectionPermissionsContext.ts b/src/app/services/context/collection-permissions/CollectionPermissionsContext.ts new file mode 100644 index 0000000000..9ab8ed1b89 --- /dev/null +++ b/src/app/services/context/collection-permissions/CollectionPermissionsContext.ts @@ -0,0 +1,14 @@ +import { createContext } from 'react'; + +import { CollectionPermission, ResourcePath } from '../../../../types/resources'; + +interface CollectionPermissionsContext { + getPermissions: (paths: ResourcePath[]) => Promise; + permissions?: { [key: string]: CollectionPermission[] }; + isFetching?: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const CollectionPermissionsContext = createContext( + {} as CollectionPermissionsContext +); \ No newline at end of file diff --git a/src/app/services/context/collection-permissions/CollectionPermissionsProvider.tsx b/src/app/services/context/collection-permissions/CollectionPermissionsProvider.tsx new file mode 100644 index 0000000000..a46a404e03 --- /dev/null +++ b/src/app/services/context/collection-permissions/CollectionPermissionsProvider.tsx @@ -0,0 +1,91 @@ +import { ReactNode, useMemo, useState } from 'react'; + +import { CollectionPermission, Method, ResourcePath } from '../../../../types/resources'; +import { + getScopesFromPaths, getVersionsFromPaths, scopeOptions +} from '../../../views/sidebar/resource-explorer/collection/collection.util'; +import { CollectionPermissionsContext } from './CollectionPermissionsContext'; +import { useAppSelector } from '../../../../store'; + +interface CollectionRequest { + method: Method; + requestUrl: string; +} + +function getRequestsFromPaths(paths: ResourcePath[], version: string, scope: string) { + const requests: CollectionRequest[] = []; + paths.forEach(path => { + const { method, url } = path; + const pathScope = path.scope ?? scopeOptions[0].key; + if (version === path.version && scope === pathScope) { + requests.push({ + method: method as Method, + requestUrl: url + }); + } + }); + return requests; +} + +async function getCollectionPermissions(permissionsUrl: string, paths: ResourcePath[]): +Promise<{ [key: string]: CollectionPermission[] }> { + const versions = getVersionsFromPaths(paths); + const scopes = getScopesFromPaths(paths); + const collectionPermissions: { [key: string]: CollectionPermission[] } = {}; + + for (const version of versions) { + for (const scope of scopes) { + const requestPaths = getRequestsFromPaths(paths, version, scope); + if (requestPaths.length === 0) { + continue; + } + const url = `${permissionsUrl}?version=${version}&scopeType=${scope}`; + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestPaths) + }); + const perms = await response.json(); + collectionPermissions[`${version}-${scope}`] = (perms.results) ? perms.results : []; + } + } + return collectionPermissions; +} + +const CollectionPermissionsProvider = ({ children }: { children: ReactNode }) => { + const { baseUrl } = useAppSelector((state) => state.devxApi); + const [permissions, setPermissions] = useState<{ [key: string]: CollectionPermission[] } | undefined>(undefined); + const [isFetching, setIsFetching] = useState(false); + const [code, setCode] = useState(''); + + const getPermissions = async (items: ResourcePath[]): Promise => { + const hashCode = window.btoa(JSON.stringify([...items])); + if (hashCode !== code) { + try { + setIsFetching(true); + const perms = await getCollectionPermissions(`${baseUrl}/permissions`, items); + setPermissions(perms); + setCode(hashCode); + } catch (error) { + setPermissions(undefined); + } finally { + setIsFetching(false); + } + } + }; + + const contextValue = useMemo( + () => ({ getPermissions, permissions, isFetching }), + [getPermissions, permissions, isFetching] + ); + + return ( + + {children} + + ); +}; + +export default CollectionPermissionsProvider; \ No newline at end of file diff --git a/src/app/services/graph-constants.ts b/src/app/services/graph-constants.ts index bdc5315221..f123835959 100644 --- a/src/app/services/graph-constants.ts +++ b/src/app/services/graph-constants.ts @@ -32,4 +32,4 @@ export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/s // eslint-disable-next-line max-len export const CONSENT_TYPE_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant?view=graph-rest-1.0#:~:text=(eq%20only).-,consentType,-String' export const CURRENT_THEME='CURRENT_THEME'; -export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas' \ No newline at end of file +export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas'; diff --git a/src/app/services/hooks/useCollectionPermissions.ts b/src/app/services/hooks/useCollectionPermissions.ts new file mode 100644 index 0000000000..97410f23d9 --- /dev/null +++ b/src/app/services/hooks/useCollectionPermissions.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react'; + +import { CollectionPermissionsContext } from '../context/collection-permissions/CollectionPermissionsContext'; + +export const useCollectionPermissions = () => { + return useContext(CollectionPermissionsContext); +}; \ No newline at end of file diff --git a/src/app/services/slices/collections.slice.ts b/src/app/services/slices/collections.slice.ts index b7c5a37b6e..5b8e148baf 100644 --- a/src/app/services/slices/collections.slice.ts +++ b/src/app/services/slices/collections.slice.ts @@ -1,38 +1,66 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Collection, ResourcePath } from '../../../types/resources'; -const initialState: Collection[] = []; +interface CollectionsState { + collections: Collection[]; + saved: boolean; +} + +const initialState: CollectionsState = { + collections: [], + saved: false +}; const collections = createSlice({ name: 'collections', initialState, reducers: { createCollection: (state, action: PayloadAction) => { - state.push(action.payload); - return state + state.collections.push(action.payload); + state.saved = false; }, - addResourcePaths:(state, action: PayloadAction) => { - const index = state.findIndex(collection => collection.isDefault); + addResourcePaths: (state, action: PayloadAction) => { + const index = state.collections.findIndex(collection => collection.isDefault); if (index > -1) { - state[index].paths.push(...action.payload) + state.collections[index].paths.push(...action.payload); + state.saved = false; + } + }, + updateResourcePaths: (state, action: PayloadAction) => { + const collectionIndex = state.collections.findIndex(k => k.isDefault); + if (collectionIndex > -1) { + state.collections[collectionIndex] = { + ...state.collections[collectionIndex], + paths: action.payload + }; + state.saved = true; } }, - removeResourcePaths: (state, action: PayloadAction)=>{ - const index = state.findIndex(collection => collection.isDefault); - if(index > -1) { - const defaultResourcePaths = [...state[index].paths]; - action.payload.forEach((resourcePath: ResourcePath)=>{ - const delIndex = defaultResourcePaths.findIndex(p=>p.key === resourcePath.key) + removeResourcePaths: (state, action: PayloadAction) => { + const index = state.collections.findIndex(collection => collection.isDefault); + if (index > -1) { + const defaultResourcePaths = [...state.collections[index].paths]; + action.payload.forEach((resourcePath: ResourcePath) => { + const delIndex = defaultResourcePaths.findIndex(p => p.key === resourcePath.key); if (delIndex > -1) { - defaultResourcePaths.splice(delIndex, 1) + defaultResourcePaths.splice(delIndex, 1); } - }) - state[index].paths = defaultResourcePaths; + }); + state.collections[index].paths = defaultResourcePaths; + state.saved = false; } + }, + resetSaveState: (state) => { + state.saved = false; } } -}) +}); -export const {createCollection, addResourcePaths, removeResourcePaths} = collections.actions +export const + { createCollection, + addResourcePaths, + updateResourcePaths, + removeResourcePaths, + resetSaveState } = collections.actions; -export default collections.reducer \ No newline at end of file +export default collections.reducer; diff --git a/src/app/utils/searchbox.styles.ts b/src/app/utils/searchbox.styles.ts index 949280ab16..913ce17caa 100644 --- a/src/app/utils/searchbox.styles.ts +++ b/src/app/utils/searchbox.styles.ts @@ -1,6 +1,6 @@ export const searchBoxStyles: any = () => ({ root: { - width: '97%' + width: '100%' }, field: [ { diff --git a/src/app/views/App.tsx b/src/app/views/App.tsx index 004a0cff12..12074bd988 100644 --- a/src/app/views/App.tsx +++ b/src/app/views/App.tsx @@ -14,6 +14,7 @@ import { Mode } from '../../types/enums'; import { IInitMessage, IQuery, IThemeChangedMessage } from '../../types/query-runner'; import { ISharedQueryParams } from '../../types/share-query'; import { ISidebarProps } from '../../types/sidebar'; +import CollectionPermissionsProvider from '../services/context/collection-permissions/CollectionPermissionsProvider'; import { PopupsProvider } from '../services/context/popups-context'; import { ValidationProvider } from '../services/context/validation-context/ValidationProvider'; import { GRAPH_URL } from '../services/graph-constants'; @@ -26,12 +27,11 @@ import { changeTheme } from '../services/slices/theme.slice'; import { parseSampleUrl } from '../utils/sample-url-generation'; import { substituteTokens } from '../utils/token-helpers'; import { translateMessage } from '../utils/translate-messages'; -import { TermsOfUseMessage } from './app-sections'; +import { StatusMessages, TermsOfUseMessage } from './app-sections'; import { headerMessaging } from './app-sections/HeaderMessaging'; import { appStyles } from './App.styles'; import { classNames } from './classnames'; import { KeyboardCopyEvent } from './common/copy-button/KeyboardCopyEvent'; -import { StatusMessages } from './common/lazy-loader/component-registry'; import PopupsWrapper from './common/popups/PopupsWrapper'; import { createShareLink } from './common/share'; import { MainHeader } from './main-header/MainHeader'; @@ -492,7 +492,9 @@ class App extends Component { - + + + ); diff --git a/src/app/views/common/download.ts b/src/app/views/common/download.ts index 431a0643aa..8d585522e5 100644 --- a/src/app/views/common/download.ts +++ b/src/app/views/common/download.ts @@ -1,11 +1,10 @@ -import { telemetry, eventTypes, componentNames } from '../../../telemetry'; +import { telemetry, eventTypes } from '../../../telemetry'; -export function downloadToLocal(content: any, filename: string) { +function downloadToLocal(content: any, filename: string) { const blob = new Blob([JSON.stringify(content, null, 4)], { type: 'text/json' }); download(blob, filename); - trackDownload(filename); } function download(blob: Blob, filename: string) { @@ -17,9 +16,14 @@ function download(blob: Blob, filename: string) { document.body.removeChild(elem); } -function trackDownload(filename: string) { +function trackDownload(filename: string, componentName: string) { telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT, { - ComponentName: componentNames.DOWNLOAD_POSTMAN_COLLECTION_BUTTON, + componentName, filename }); } + +export { + downloadToLocal, + trackDownload +}; diff --git a/src/app/views/common/lazy-loader/component-registry/index.tsx b/src/app/views/common/lazy-loader/component-registry/index.tsx index faf435c229..08218c510b 100644 --- a/src/app/views/common/lazy-loader/component-registry/index.tsx +++ b/src/app/views/common/lazy-loader/component-registry/index.tsx @@ -76,5 +76,4 @@ export const ResourceExplorer = (props?: any) => { return ( ) -} - +} \ No newline at end of file diff --git a/src/app/views/common/lazy-loader/component-registry/popups.tsx b/src/app/views/common/lazy-loader/component-registry/popups.tsx index 732f62b88f..c2e49a9216 100644 --- a/src/app/views/common/lazy-loader/component-registry/popups.tsx +++ b/src/app/views/common/lazy-loader/component-registry/popups.tsx @@ -3,12 +3,18 @@ import { lazy } from 'react'; export const popups = new Map([ ['share-query', lazy(() => import('../../../query-runner/query-input/share-query/ShareQuery'))], ['theme-chooser', lazy(() => import('../../../main-header/settings/ThemeChooser'))], - ['preview-collection', lazy(() => import('../../../sidebar/resource-explorer/collection/PreviewCollection'))], - ['full-permissions', lazy(() => import('../../../query-runner/request/permissions/Permissions.Full'))] + ['preview-collection', lazy(() => import('../../../sidebar/resource-explorer/collection/APICollection'))], + ['full-permissions', lazy(() => import('../../../query-runner/request/permissions/Permissions.Full'))], + ['collection-permissions', lazy(() => import('../../../sidebar/resource-explorer/collection/CollectionPermissions'))], + ['edit-collection-panel', lazy(() => import('../../../sidebar/resource-explorer/collection/EditCollectionPanel'))], + ['edit-scope-panel', lazy(() => import('../../../sidebar/resource-explorer/collection/EditScopePanel'))] ]); export type PopupItem = 'share-query' | 'theme-chooser' | 'preview-collection' | - 'full-permissions'; \ No newline at end of file + 'full-permissions' | + 'collection-permissions' | + 'edit-collection-panel' | + 'edit-scope-panel' \ No newline at end of file diff --git a/src/app/views/common/popups/PanelWrapper.tsx b/src/app/views/common/popups/PanelWrapper.tsx index f5c60d07aa..a7e20a1b0f 100644 --- a/src/app/views/common/popups/PanelWrapper.tsx +++ b/src/app/views/common/popups/PanelWrapper.tsx @@ -1,11 +1,12 @@ -import { getTheme, IOverlayProps, Panel, PanelType, Spinner } from '@fluentui/react'; +import { getTheme, IconButton, IOverlayProps, Panel, PanelType, Spinner } from '@fluentui/react'; import { Suspense } from 'react'; import { useAppSelector } from '../../../../store'; +import { translateMessage } from '../../../utils/translate-messages'; import { WrapperProps } from './popups.types'; export function PanelWrapper(props: WrapperProps) { - const { theme: appTheme } = useAppSelector((state) => state); + const appTheme = useAppSelector((state) => state.theme); const theme = getTheme(); const { isOpen, dismissPopup, Component, popupsProps, closePopup } = props; const { title, renderFooter } = popupsProps.settings; @@ -42,22 +43,52 @@ export function PanelWrapper(props: WrapperProps) { const panelType = getPanelType(); const onRenderFooterContent = (): JSX.Element | null => { - return renderFooter? renderFooter() : null; + return renderFooter ? renderFooter() : null; } + const showBackButton = title === 'Edit Scope' || title === 'Edit Collection' || title === 'Preview Permissions'; + + const onRenderHeader = (): JSX.Element => ( +
+ dismissPopup()} + styles={{ root: { marginRight: 8 } }} + /> + + {title} + +
+ ); + return (
dismissPopup()} - hasCloseButton={true} + hasCloseButton={false} type={panelType} headerText={headerText.toString()} isFooterAtBottom={true} + isBlocking={true} + isLightDismiss={false} closeButtonAriaLabel='Close' overlayProps={panelOverlayProps} onRenderFooterContent={onRenderFooterContent} + onRenderHeader={showBackButton ? onRenderHeader: undefined} > + dismissPopup()} + />
{ }> diff --git a/src/app/views/query-runner/query-input/QueryInput.styles.ts b/src/app/views/query-runner/query-input/QueryInput.styles.ts index e505274e49..c4d16e4545 100644 --- a/src/app/views/query-runner/query-input/QueryInput.styles.ts +++ b/src/app/views/query-runner/query-input/QueryInput.styles.ts @@ -1,7 +1,7 @@ import { ITheme } from '@fluentui/react'; export const queryInputStyles = (theme: ITheme) => { - const controlWidth = '94%'; + const controlWidth = '90%'; return { autoComplete: { input: { diff --git a/src/app/views/query-runner/query-input/QueryInput.tsx b/src/app/views/query-runner/query-input/QueryInput.tsx index 6adb5f9827..35ec782e15 100644 --- a/src/app/views/query-runner/query-input/QueryInput.tsx +++ b/src/app/views/query-runner/query-input/QueryInput.tsx @@ -13,7 +13,6 @@ import SubmitButton from '../../../views/common/submit-button/SubmitButton'; import { shouldRunQuery } from '../../sidebar/sample-queries/sample-query-utils'; import { queryRunnerStyles } from '../QueryRunner.styles'; import { AutoComplete } from './auto-complete'; -import { ShareButton } from './share-query'; const QueryInput = (props: IQueryInputProps) => { const { @@ -25,6 +24,7 @@ const QueryInput = (props: IQueryInputProps) => { const dispatch = useAppDispatch(); const validation = useContext(ValidationContext); + const urlVersions: IDropdownOption[] = []; GRAPH_API_VERSIONS.forEach(version => { urlVersions.push({ @@ -44,7 +44,7 @@ const QueryInput = (props: IQueryInputProps) => { method: sampleQuery.selectedVerb, authenticated, url: sampleQuery.sampleUrl }); - const { queryButtonStyles, verbSelector, shareQueryButtonStyles } = queryRunnerStyles(); + const { queryButtonStyles, verbSelector } = queryRunnerStyles(); verbSelector.title = { ...verbSelector.title, background: getStyleFor(sampleQuery.selectedVerb) @@ -79,13 +79,13 @@ const QueryInput = (props: IQueryInputProps) => { }; const queryInputStackTokens: IStackTokens = { - childrenGap: 7 + childrenGap: 10 }; return ( <> - + { onChange={(event, method) => handleOnVersionChange(method)} /> - + { allowDisabledFocus={true} /> - - + + diff --git a/src/app/views/query-runner/query-input/auto-complete/suffix/SuffixRenderer.tsx b/src/app/views/query-runner/query-input/auto-complete/suffix/SuffixRenderer.tsx index 05a5cc2874..00d4265efc 100644 --- a/src/app/views/query-runner/query-input/auto-complete/suffix/SuffixRenderer.tsx +++ b/src/app/views/query-runner/query-input/auto-complete/suffix/SuffixRenderer.tsx @@ -11,6 +11,7 @@ import { parseSampleUrl } from '../../../../../utils/sample-url-generation'; import { translateMessage } from '../../../../../utils/translate-messages'; import DocumentationService from './documentation'; import { styles } from './suffix.styles'; +import ShareButton from '../../share-query/ShareButton'; const SuffixRenderer = () => { const sampleQuery = useAppSelector((state)=> state.sampleQuery); @@ -91,6 +92,7 @@ const SuffixRenderer = () => { disabled={!documentationLinkAvailable} /> + ); } diff --git a/src/app/views/query-runner/query-input/share-query/ShareButton.tsx b/src/app/views/query-runner/query-input/share-query/ShareButton.tsx index 3d7989024c..7a9270341f 100644 --- a/src/app/views/query-runner/query-input/share-query/ShareButton.tsx +++ b/src/app/views/query-runner/query-input/share-query/ShareButton.tsx @@ -1,11 +1,10 @@ import { - DirectionalHint, - IconButton, IIconProps, TooltipHost + IconButton, IIconProps, ITooltipHostStyles, TooltipHost } from '@fluentui/react'; import { usePopups } from '../../../../services/hooks'; import { translateMessage } from '../../../../utils/translate-messages'; -import { shareQueryStyles } from './ShareQuery.styles'; +import { styles } from '../auto-complete/suffix/suffix.styles'; const ShareButton = () => { @@ -15,7 +14,7 @@ const ShareButton = () => { iconName: 'Share' } - const shareButtonStyles = shareQueryStyles().iconButton; + const shareButtonStyles: Partial = { root: { display: 'inline-block' } }; const content =
{translateMessage('Share Query')}
const calloutProps = { @@ -27,7 +26,7 @@ const ShareButton = () => { showShareQuery({ @@ -37,8 +36,7 @@ const ShareButton = () => { } })} iconProps={iconProps} - styles={shareButtonStyles} - role={'button'} + className={styles.iconButton} ariaLabel={translateMessage('Share Query')} /> diff --git a/src/app/views/sidebar/history/History.tsx b/src/app/views/sidebar/history/History.tsx index a89b03e768..379233fad6 100644 --- a/src/app/views/sidebar/history/History.tsx +++ b/src/app/views/sidebar/history/History.tsx @@ -29,6 +29,8 @@ import { classNames } from '../../classnames'; import { NoResultsFound } from '../sidebar-utils/SearchResult'; import { sidebarStyles } from '../Sidebar.styles'; import { createHarEntry, exportQuery, generateHar } from './har-utils'; +import { ResourceLinkType } from '../../../../types/resources'; +import { addResourcePaths, removeResourcePaths } from '../../../services/slices/collections.slice'; const columns = [ { key: 'button', name: '', fieldName: '', minWidth: 20, maxWidth: 20 }, @@ -96,6 +98,7 @@ const History = (props: any) => { const [category, setCategory] = useState(''); const [groups, setGroups] = useState([]); const [searchStarted, setSearchStarted] = useState(false); + const {collections} = useAppSelector((state) => state.collections); const shouldGenerateGroups = useRef(true); @@ -120,6 +123,15 @@ const History = (props: any) => { return NoResultsFound('We did not find any history items'); } + const isInCollection = (item: IHistoryItem) => { + const defaultCollection = collections.find((collection) => collection.isDefault); + if (!defaultCollection) { return false; } + return defaultCollection.paths.some((path) => { + const { relativeUrl } = processUrlAndVersion(item.url); + return path.url === relativeUrl && path.method === item.method; + }); + }; + const searchValueChanged = (_event: any, value?: string): void => { shouldGenerateGroups.current = true; setSearchStarted(searchStatus => !searchStatus); @@ -146,6 +158,7 @@ const History = (props: any) => { ); }; + const renderItemColumn = (item: any, index: number | undefined, column: IColumn | undefined) => { const hostId: string = getId('tooltipHost'); const currentTheme = getTheme(); @@ -155,6 +168,7 @@ const History = (props: any) => { const viewText = translateMessage('view'); const removeText = translateMessage('Delete'); const exportQueryText = translateMessage('Export'); + const inCollection = isInCollection(item); if (column) { const queryContent = item[column.fieldName as keyof any] as string; @@ -202,6 +216,23 @@ const History = (props: any) => { }, onClick: () => onExportQuery(item) }, + ...(inCollection + ? [ + { + key: 'removeFromCollection', + text: translateMessage('Remove from Collection'), + iconProps: { iconName: 'BoxSubtractSolid' }, + onClick: () => handleRemoveFromCollection(item) + } + ] + : [ + { + key: 'addToCollection', + text: translateMessage('Add to Collection'), + iconProps: { iconName: 'BoxAdditionSolid' }, + onClick: () => handleAddToCollection(item) + } + ]), { key: 'remove', text: removeText, @@ -478,7 +509,43 @@ const History = (props: any) => { ItemIndex: query.index, QuerySignature: `${query.method} ${sanitizedUrl}` }); - } + }; + + const processUrlAndVersion = (url: string) => { + let version = 'v1.0'; + if (url.includes('graph.microsoft.com/beta')) { + version = 'beta'; + url = url.replace('https://graph.microsoft.com/beta', ''); + } else { + url = url.replace('https://graph.microsoft.com/v1.0', ''); + } + return { relativeUrl: url, version }; + }; + const formatHistoryItem = (item: IHistoryItem) => { + const { relativeUrl, version } = processUrlAndVersion(item.url); + const pathSegments = relativeUrl.split('/').filter(Boolean); + const name = pathSegments[pathSegments.length - 1] || relativeUrl; + + return { + paths: pathSegments, + name, + type: ResourceLinkType.PATH, + version, + method: item.method, + url: relativeUrl, + key: `${item.index}-${item.url}` + }; + }; + + const handleAddToCollection = (item: IHistoryItem) => { + const resourcePath = formatHistoryItem(item); + dispatch(addResourcePaths([resourcePath])); + }; + + const handleRemoveFromCollection = (item: IHistoryItem) => { + const resourcePath = formatHistoryItem(item); + dispatch(removeResourcePaths([resourcePath])); + }; return ( diff --git a/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx b/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx index 068366881c..a78fd3ae9b 100644 --- a/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx +++ b/src/app/views/sidebar/resource-explorer/ResourceExplorer.tsx @@ -1,9 +1,8 @@ import { - INavLink, INavLinkGroup, Label, Nav, SearchBox, Spinner, SpinnerSize, - Stack, - styled, - Toggle -} from '@fluentui/react'; + DefaultButton, + INavLink, INavLinkGroup, Label, + Nav, SearchBox, Spinner, SpinnerSize, Stack, styled, Toggle, + useTheme} from '@fluentui/react'; import debouce from 'lodash.debounce'; import { useEffect, useMemo, useState } from 'react'; @@ -20,23 +19,20 @@ import { translateMessage } from '../../../utils/translate-messages'; import { classNames } from '../../classnames'; import { NoResultsFound } from '../sidebar-utils/SearchResult'; import { sidebarStyles } from '../Sidebar.styles'; -import { UploadPostmanCollection } from './collection/UploadCollection'; -import CommandOptions from './command-options/CommandOptions'; -import { - createResourcesList, getResourcePaths, - getUrlFromLink -} from './resource-explorer.utils'; +import { createResourcesList, getResourcePaths, getUrlFromLink } from './resource-explorer.utils'; import ResourceLink from './ResourceLink'; -import { navStyles } from './resources.styles'; +import { navStyles, resourceExplorerStyles } from './resources.styles'; +import { usePopups } from '../../../services/hooks/usePopups'; const UnstyledResourceExplorer = (props: any) => { - const { resources: { data, pending }, collections } = useAppSelector( - (state) => state - ); + const { data, pending } = useAppSelector((state) => state.resources); + const { collections } = useAppSelector((state) => state.collections); const dispatch: AppDispatch = useAppDispatch(); const classes = classNames(props); - const selectedLinks = collections && collections.length > 0 ? collections.find(k => k.isDefault)!.paths : []; + const theme = useTheme(); + const styles = resourceExplorerStyles(theme); + const selectedLinks = collections && collections.length > 0 ? collections.find(k => k.isDefault)!.paths : []; const versions: { key: string, text: string }[] = [ { key: 'v1.0', text: 'v1.0' }, { key: 'beta', text: 'beta' } @@ -48,10 +44,10 @@ const UnstyledResourceExplorer = (props: any) => { ? data[version].children : []; const [searchText, setSearchText] = useState(''); - const filteredPayload = searchText ? searchResources(resourcesToUse!, searchText) : resourcesToUse!; + const filteredPayload = searchText ? searchResources(resourcesToUse, searchText) : resourcesToUse; const navigationGroup = createResourcesList(filteredPayload, version, searchText); - const [items, setItems] = useState(navigationGroup); + const { show: previewCollection } = usePopups('preview-collection', 'panel'); useEffect(() => { setItems(navigationGroup); @@ -118,6 +114,15 @@ const UnstyledResourceExplorer = (props: any) => { }); } + const openPreviewCollection = () => { + previewCollection({ + settings: { + title: translateMessage('My API collection'), + width: 'xl' + } + }) + } + if (pending) { return ( { onChange={debouncedSearch} styles={searchBoxStyles} /> -
- - - < UploadPostmanCollection /> + + + +
+ {selectedLinks.length > 0 ? `(${selectedLinks.length})` : ''} +
+
+
+
+ + + + + + - - - {selectedLinks && selectedLinks.length > 0 && <> - - - - } - - - {items[0].links.length > 0 && - } - { items[0].links.length === 0 ? NoResultsFound('No resources found', { paddingBottom: '20px' }) : (