diff --git a/package-lock.json b/package-lock.json
index acb1190b6..c1a017803 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2934,6 +2934,15 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw=="
},
+ "@types/lodash.debounce": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz",
+ "integrity": "sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==",
+ "dev": true,
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
"@types/lodash.isequal": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz",
@@ -4037,7 +4046,8 @@
"dev": true
},
"fsevents": {
- "version": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="
},
"has-flag": {
diff --git a/package.json b/package.json
index d2b64a20e..fe66da6d1 100644
--- a/package.json
+++ b/package.json
@@ -93,6 +93,7 @@
"@types/enzyme-adapter-react-16": "1.0.5",
"@types/isomorphic-fetch": "0.0.35",
"@types/jest": "24.0.6",
+ "@types/lodash.debounce": "4.0.6",
"@types/react": "17.0.35",
"@types/react-dom": "17.0.11",
"@types/react-intl": "3.0.0",
@@ -123,4 +124,4 @@
"reportFile": "test-report.xml",
"indent": 4
}
-}
+}
\ No newline at end of file
diff --git a/src/app/utils/string-operations.ts b/src/app/utils/string-operations.ts
index 51cc30252..4ef0d1d2e 100644
--- a/src/app/utils/string-operations.ts
+++ b/src/app/utils/string-operations.ts
@@ -1,11 +1,24 @@
declare global {
interface String {
- toSentenceCase(): String;
+ toSentenceCase(): string;
+ contains(searchText: string): boolean;
}
}
-String.prototype.toSentenceCase = function () {
+/**
+ * Converts the first character to uppercase if character is alphanumeric and the rest to lowercase
+ */
+String.prototype.toSentenceCase = function (): string {
return `${this.charAt(0).toUpperCase()}${this.toLowerCase().slice(1)}`;
};
+/**
+ * Performs a case-insenstive search of a substring within a string and
+ * returns true if searchString appears as a substring of a string
+ * @param searchString search string
+ */
+String.prototype.contains = function (searchString: string): boolean {
+ return this.toLowerCase().includes(searchString.toLowerCase());
+};
+
export {};
diff --git a/src/app/views/sidebar/Sidebar.tsx b/src/app/views/sidebar/Sidebar.tsx
index a7497e7b8..ae6ff2529 100644
--- a/src/app/views/sidebar/Sidebar.tsx
+++ b/src/app/views/sidebar/Sidebar.tsx
@@ -4,6 +4,7 @@ import React from 'react';
import { telemetry } from '../../../telemetry';
import { translateMessage } from '../../utils/translate-messages';
import History from './history/History';
+import { ResourceExplorer } from './resource-explorer';
import SampleQueries from './sample-queries/SampleQueries';
export const Sidebar = () => {
return (
@@ -19,6 +20,16 @@ export const Sidebar = () => {
>
+
+
+
{
{ key: 'beta', text: 'beta', iconProps: { iconName: 'PartlyCloudyNight' } }
];
const [version, setVersion] = useState(versions[0].key);
- const filteredPayload = getResourcesSupportedByVersion(data, version);
- const navigationGroup = createResourcesList(filteredPayload.children, version);
+ const [searchText, setSearchText] = useState('');
+ const filteredPayload = getResourcesSupportedByVersion(data.children, version, searchText);
+ const navigationGroup = createResourcesList(filteredPayload, version, searchText);
- const [resourceItems, setResourceItems] = useState(filteredPayload.children);
+ const [resourceItems, setResourceItems] = useState(filteredPayload);
const [items, setItems] = useState(navigationGroup);
useEffect(() => {
setItems(navigationGroup);
- setResourceItems(filteredPayload.children)
- }, [filteredPayload.children.length]);
+ setResourceItems(filteredPayload)
+ }, [filteredPayload.length]);
const [isolated, setIsolated] = useState(null);
- const [searchText, setSearchText] = useState('');
-
- const performSearch = (needle: string, haystack: IResource[]) => {
- const keyword = needle.toLowerCase();
- return haystack.filter((sample: IResource) => {
- const name = sample.segment.toLowerCase();
- return name.toLowerCase().includes(keyword);
- });
- }
+ const [linkLevel, setLinkLevel] = useState(-1);
const generateBreadCrumbs = () => {
if (!!isolated && isolated.paths.length > 0) {
@@ -81,31 +76,20 @@ const unstyledResourceExplorer = (props: any) => {
dispatch(addResourcePaths(getResourcePaths(item, version)));
}
- const changeVersion = (ev: React.FormEvent | undefined,
- option: IChoiceGroupOption | undefined): void => {
- const selectedVersion = option!.key;
+ const changeVersion = (_event: React.MouseEvent, checked?: boolean | undefined): void => {
+ const selectedVersion = checked ? versions[1].key : versions[0].key;
setVersion(selectedVersion);
- const list = getResourcesSupportedByVersion(data, selectedVersion);
- const dataSet = (searchText) ? performSearch(searchText, list.children) : list.children;
- setResourceItems(dataSet);
- setItems(createResourcesList(dataSet, selectedVersion));
}
const changeSearchValue = (event: any, value?: string) => {
- let filtered: any[] = [...data.children];
- setSearchText(value || '');
- if (value) {
- filtered = performSearch(value, filtered);
- }
- const dataSet = getResourcesSupportedByVersion({
- children: filtered,
- labels: data.labels,
- segment: data.segment
- }, version).children;
- setResourceItems(dataSet);
- setItems(createResourcesList(dataSet, version));
+ const trimmedSearchText = value ? value.trim() : '';
+ setSearchText(trimmedSearchText);
}
+ const debouncedSearch = useMemo(() => {
+ return debouce(changeSearchValue, 300);
+ }, []);
+
const navigateToBreadCrumb = (ev?: any, item?: IBreadcrumbItem): void => {
const iterator = item!.key;
if (iterator === '/') {
@@ -130,6 +114,7 @@ const unstyledResourceExplorer = (props: any) => {
];
setItems(tree);
setIsolated(navLink);
+ setLinkLevel(navLink.level);
telemetry.trackEvent(eventTypes.LISTITEM_CLICK_EVENT,
{
ComponentName: componentNames.RESOURCES_ISOLATE_QUERY_LIST_ITEM,
@@ -140,8 +125,9 @@ const unstyledResourceExplorer = (props: any) => {
const disableIsolation = (): void => {
setIsolated(null);
setSearchText('');
- const filtered = getResourcesSupportedByVersion(data, version);
- setItems(createResourcesList(filtered.children, version));
+ const filtered = getResourcesSupportedByVersion(data.children, version);
+ setLinkLevel(-1);
+ setItems(createResourcesList(filtered, version));
}
const clickLink = (ev?: React.MouseEvent, item?: INavLink) => {
@@ -196,17 +182,17 @@ const unstyledResourceExplorer = (props: any) => {
{!isolated && <>
-
>}
@@ -246,6 +232,7 @@ const unstyledResourceExplorer = (props: any) => {
link={link}
isolateTree={isolateTree}
resourceOptionSelected={(activity: string, context: unknown) => resourceOptionSelected(activity, context)}
+ linkLevel={linkLevel}
classes={classes}
/>
}}
diff --git a/src/app/views/sidebar/resource-explorer/ResourceLink.tsx b/src/app/views/sidebar/resource-explorer/ResourceLink.tsx
index af719761e..e6b2bd0d6 100644
--- a/src/app/views/sidebar/resource-explorer/ResourceLink.tsx
+++ b/src/app/views/sidebar/resource-explorer/ResourceLink.tsx
@@ -8,11 +8,11 @@ import { FormattedMessage } from 'react-intl';
import { ResourceLinkType, ResourceOptions } from '../../../../types/resources';
import { getStyleFor } from '../../../utils/http-methods.utils';
import { translateMessage } from '../../../utils/translate-messages';
-
interface IResourceLinkProps {
link: any;
isolateTree: Function;
resourceOptionSelected: Function;
+ linkLevel: number;
classes: any;
}
@@ -22,9 +22,10 @@ const ResourceLink = (props: IResourceLinkProps) => {
const tooltipId = getId('tooltip');
const buttonId = getId('targetButton');
+
const iconButtonStyles = {
- root: { paddingBottom: 10 },
- menuIcon: { fontSize: 20, padding: 10 }
+ root: { paddingBottom:10, marginTop: -5, marginRight: 2 },
+ menuIcon: { fontSize: 20, padding: 5 }
};
const methodButtonStyles: CSSProperties = {
@@ -44,8 +45,12 @@ const ResourceLink = (props: IResourceLinkProps) => {
>
{resourceLink.method}
}
- {resourceLink.name}
+
+
+ {resourceLink.name}
+
+
{items.length > 0 &&
{
role='button'
id={buttonId}
aria-describedby={tooltipId}
- className={linkStyle.button}
styles={iconButtonStyles}
menuIconProps={{ iconName: 'MoreVertical' }}
title={translateMessage('More actions')}
@@ -77,7 +81,7 @@ const ResourceLink = (props: IResourceLinkProps) => {
/>
}
- ;
+
function getMenuItems() {
const menuItems: IContextualMenuItem[] = [];
@@ -108,8 +112,9 @@ const ResourceLink = (props: IResourceLinkProps) => {
const linkStyle = mergeStyleSets(
{
- link: { display: 'flex', lineHeight: 'normal' },
- button: { float: 'right', position: 'absolute', right: 0 }
+ link: { display: 'flex', lineHeight: 'normal', width: '100%', overflow: 'hidden' },
+ resourceLinkNameContainer: { textAlign: 'left', flex: '1', overflow:'hidden', display: 'flex' },
+ resourceLinkText: { textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }
}
);
diff --git a/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx b/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx
index 69b36fb49..6949902cd 100644
--- a/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx
+++ b/src/app/views/sidebar/resource-explorer/command-options/CommandOptions.tsx
@@ -1,44 +1,83 @@
-import { CommandBar, CommandBarButton, getTheme, IButtonProps, ICommandBarItemProps } from '@fluentui/react';
+import {
+ CommandBar, CommandBarButton, DefaultButton, Dialog, DialogFooter, DialogType,
+ getId, getTheme, IButtonProps, ICommandBarItemProps, PrimaryButton
+} from '@fluentui/react';
import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { IRootState } from '../../../../../types/root';
+import { removeResourcePaths } from '../../../../services/actions/resource-explorer-action-creators';
import { translateMessage } from '../../../../utils/translate-messages';
import PathsReview from '../panels/PathsReview';
import { resourceExplorerStyles } from '../resources.styles';
+
interface ICommandOptions {
version: string;
}
const CommandOptions = (props: ICommandOptions) => {
+ const dispatch = useDispatch();
const [isOpen, setIsOpen] = useState(false);
+ const [isDialogHidden, setIsDialogHidden] = useState(true);
const { version } = props;
const theme = getTheme();
+ const { resources: { paths } } = useSelector((state: IRootState) => state);
const itemStyles = resourceExplorerStyles(theme).itemStyles;
const commandStyles = resourceExplorerStyles(theme).commandBarStyles;
const options: ICommandBarItemProps[] = [
{
key: 'preview',
text: translateMessage('Preview collection'),
+ ariaLabel: translateMessage('Preview collection'),
iconProps: { iconName: 'View' },
onClick: () => toggleSelectedResourcesPreview()
}
];
+ const farItems: ICommandBarItemProps[] = [
+ {
+ key: 'delete-all',
+ iconProps: { iconName: 'Delete' },
+ style: {
+ marginLeft: 30
+ },
+ ariaLabel: translateMessage('Delete'),
+ onClick: () => toggleIsDialogHidden()
+ }
+ ]
+
const toggleSelectedResourcesPreview = () => {
let open = isOpen;
open = !open;
setIsOpen(open);
}
+ const removeAllResources = () => {
+ dispatch(removeResourcePaths(paths));
+ }
+
const CustomButton: React.FunctionComponent = (props_: any) => {
- return ;
+ return ;
+ };
+
+ const deleteResourcesDialogProps = {
+ type: DialogType.normal,
+ title: translateMessage('Delete collection'),
+ closeButtonAriaLabel: 'Close',
+ subText: translateMessage('Do you want to remove all the items you have added to the collection?')
};
+ const toggleIsDialogHidden = () => {
+ setIsDialogHidden(!isDialogHidden);
+ }
+
return (
{
version={version}
toggleSelectedResourcesPreview={toggleSelectedResourcesPreview}
/>
+
)
}
diff --git a/src/app/views/sidebar/resource-explorer/panels/postman.util.spec.ts b/src/app/views/sidebar/resource-explorer/panels/postman.util.spec.ts
index c214a37a7..06fe4535e 100644
--- a/src/app/views/sidebar/resource-explorer/panels/postman.util.spec.ts
+++ b/src/app/views/sidebar/resource-explorer/panels/postman.util.spec.ts
@@ -12,7 +12,6 @@ describe('Postman collection should', () => {
const item: any = filtered.links[0];
const paths = getResourcePaths(item, version);
const collection = generatePostmanCollection(paths);
- const folderItems: any = collection.item[0];
- expect(folderItems.item.length).toBe(33);
+ expect(collection.item.length).toBe(33);
});
});
diff --git a/src/app/views/sidebar/resource-explorer/panels/postman.util.ts b/src/app/views/sidebar/resource-explorer/panels/postman.util.ts
index cc93b2b74..801324e32 100644
--- a/src/app/views/sidebar/resource-explorer/panels/postman.util.ts
+++ b/src/app/views/sidebar/resource-explorer/panels/postman.util.ts
@@ -23,37 +23,36 @@ export function generatePostmanCollection(
}
function generateItemsFromPaths(resources: IResourceLink[]): Item[] {
- const folderNames = resources
- .map((resource) => {
- if (resource.paths.length > 1) {
- return resource.paths[1];
- }
- })
- .filter((value, i, arr) => arr.indexOf(value) === i) // selects distinct folder names
- .sort();
+ const list: Item[] = [];
+ resources.forEach(resource => {
+ const {
+ method,
+ name,
+ url,
+ version,
+ paths: path
+ } = resource;
+
+ path.shift();
+ path.unshift(version!);
- const folderItems: any = folderNames.map((folder) => {
- const items = resources
- .filter((resource) => resource.url.match(`^/${folder}/?`))
- .map((resource) => {
- const { method, url, version, paths: path } = resource;
- path.shift();
- path.unshift(version!);
- const item: Item = {
- name: url,
- request: {
- method: method!,
- url: {
- raw: `${GRAPH_URL}/${version}${url}`,
- protocol: 'https',
- host: ['graph', 'microsoft', 'com'],
- path
- }
- }
- };
- return item;
- });
- return { name: folder, item: items };
+ const item: Item = {
+ name: `${name}-${version}`,
+ request: {
+ method: method!,
+ url: {
+ raw: `${GRAPH_URL}/${version}${url}`,
+ protocol: 'https',
+ host: [
+ 'graph',
+ 'microsoft',
+ 'com'
+ ],
+ path
+ }
+ }
+ }
+ list.push(item);
});
- return folderItems;
+ return list;
}
diff --git a/src/app/views/sidebar/resource-explorer/resource-explorer.utils.spec.ts b/src/app/views/sidebar/resource-explorer/resource-explorer.utils.spec.ts
index 2ec512241..2cf81f3b7 100644
--- a/src/app/views/sidebar/resource-explorer/resource-explorer.utils.spec.ts
+++ b/src/app/views/sidebar/resource-explorer/resource-explorer.utils.spec.ts
@@ -15,8 +15,8 @@ describe('Resource payload should', () => {
});
it('return children with version v1.0', async () => {
- const resources = getResourcesSupportedByVersion(resource, 'v1.0');
- expect(resources.children.length).toBe(64);
+ const resources = getResourcesSupportedByVersion(resource.children, 'v1.0');
+ expect(resources.length).toBe(64);
});
it('return links with version v1.0', async () => {
diff --git a/src/app/views/sidebar/resource-explorer/resource-explorer.utils.ts b/src/app/views/sidebar/resource-explorer/resource-explorer.utils.ts
index 958143292..94b59a96c 100644
--- a/src/app/views/sidebar/resource-explorer/resource-explorer.utils.ts
+++ b/src/app/views/sidebar/resource-explorer/resource-explorer.utils.ts
@@ -6,7 +6,6 @@ import {
IResourceLink,
ResourceLinkType
} from '../../../../types/resources';
-
interface ITreeFilter {
paths: string[];
level: number;
@@ -16,7 +15,8 @@ interface ITreeFilter {
export function createResourcesList(
source: IResource[],
- version: string
+ version: string,
+ searchText?: string
): INavLinkGroup[] {
function getLinkType({ segment, links }: any): ResourceLinkType {
const isGraphFunction = segment.startsWith('microsoft.graph');
@@ -34,21 +34,27 @@ export function createResourcesList(
): IResourceLink[] {
const { segment, children } = parent;
const links: IResourceLink[] = [];
+ const childPaths = [...paths, segment];
if (methods.length > 1) {
- methods.forEach((method) => {
- links.push(
- createNavLink(
- {
+ if (
+ !searchText ||
+ (searchText && childPaths.some((path) => path.contains(searchText)))
+ ) {
+ methods.forEach((method) => {
+ links.push(
+ createNavLink(
+ {
+ segment,
+ labels: [],
+ children: []
+ },
segment,
- labels: [],
- children: []
- },
- segment,
- [...paths, segment],
- method.toUpperCase()
- )
- );
- });
+ childPaths,
+ method.toUpperCase()
+ )
+ );
+ });
+ }
}
// versioned children
@@ -56,19 +62,17 @@ export function createResourcesList(
children
.filter((child) => versionExists(child, version))
.forEach((versionedChild) => {
- links.push(
- createNavLink(versionedChild, segment, [...paths, segment])
- );
+ links.push(createNavLink(versionedChild, segment, childPaths));
});
return links;
}
function sortResourceLinks(a: IResourceLink, b: IResourceLink): number {
- if (a.links.length === 0 && a.links.length < b.links.length) {
+ if (a.links.length === 0 && b.links.length > 0) {
return -1;
}
- if (b.links.length === 0 && a.links.length > b.links.length) {
+ if (b.links.length === 0 && a.links.length > 0) {
return 1;
}
return 0;
@@ -103,12 +107,19 @@ export function createResourcesList(
? ` (${versionedChildren.length})`
: '';
+ // if segment name does not contain search text, then found text is in child, so expand this link
+ const isExpanded =
+ searchText &&
+ ![...paths, segment].some((path) => path.contains(searchText))
+ ? true
+ : false;
+
return {
key,
url: key,
name: `${segment}${enclosedCounter}`,
labels,
- isExpanded: false,
+ isExpanded,
parent,
level,
paths,
@@ -160,23 +171,46 @@ export function removeCounter(title: string): string {
}
export function getResourcesSupportedByVersion(
- content: IResource,
- version: string
-): IResource {
- const resources: IResource = { ...content };
- const children: IResource[] = [];
+ resources: IResource[],
+ version: string,
+ searchText?: string
+): IResource[] {
+ const versionedResources: IResource[] = [];
+ const resourcesList = JSON.parse(JSON.stringify(resources)); // deep copy
+ resourcesList.forEach((resource: IResource) => {
+ if (versionExists(resource, version)) {
+ resource.children = getResourcesSupportedByVersion(
+ resource.children || [],
+ version
+ );
+ versionedResources.push(resource);
+ }
+ });
+ return searchText
+ ? searchResources(versionedResources, searchText)
+ : versionedResources;
+}
- resources.children.forEach((child: IResource) => {
- if (versionExists(child, version)) {
- children.push(child);
+function searchResources(haystack: IResource[], needle: string): IResource[] {
+ const foundResources: IResource[] = [];
+ haystack.forEach((resource: IResource) => {
+ if (resource.segment.contains(needle)) {
+ foundResources.push(resource);
+ return;
+ }
+ if (resource.children) {
+ const foundChildResources = searchResources(resource.children, needle);
+ if (foundChildResources.length > 0) {
+ resource.children = foundChildResources;
+ foundResources.push(resource);
+ }
}
});
- resources.children = children;
- return resources;
+ return foundResources;
}
-export function versionExists(child: IResource, version: string): boolean {
- return !!child.labels.find((k) => k.name === version);
+export function versionExists(resource: IResource, version: string): boolean {
+ return resource.labels.some((k) => k.name === version);
}
export function getAvailableMethods(
@@ -214,6 +248,7 @@ export function getResourcePaths(
content.forEach((element: IResourceLink) => {
element.version = version;
element.url = `${getUrlFromLink(element)}`;
+ element.key = element.key?.includes(version) ? element.key : `${element.key}-${element.version}`
});
}
return content;
@@ -229,3 +264,5 @@ function flatten(content: IResourceLink[]): IResourceLink[] {
});
return result;
}
+
+
diff --git a/src/app/views/sidebar/resource-explorer/resources.styles.ts b/src/app/views/sidebar/resource-explorer/resources.styles.ts
index 50fd22912..5b0cd4823 100644
--- a/src/app/views/sidebar/resource-explorer/resources.styles.ts
+++ b/src/app/views/sidebar/resource-explorer/resources.styles.ts
@@ -20,10 +20,14 @@ export const resourceExplorerStyles = (theme: ITheme) => {
export const navStyles: any = (properties: any) => ({
chevronIcon: [
properties.isExpanded && {
- transform: 'rotate(0deg)'
+ transform: 'rotate(0deg)',
+ position: 'relative',
+ top: '-3px'
},
!properties.isExpanded && {
- transform: 'rotate(-90deg)'
+ transform: 'rotate(-90deg)',
+ position: 'relative',
+ top: '-3px'
}
],
chevronButton: [
diff --git a/src/messages/GE.json b/src/messages/GE.json
index a4ab6dddc..6f15ece2f 100644
--- a/src/messages/GE.json
+++ b/src/messages/GE.json
@@ -431,5 +431,7 @@
"Invalid version in URL": "Invalid version in URL",
"More info": "More info",
"Could not connect to the sandbox": "Could not connect to the sandbox",
- "Failed to get profile information": "Failed to get profile information"
+ "Failed to get profile information": "Failed to get profile information",
+ "Do you want to remove all the items you have added to the collection?": "Do you want to remove all the items you have added to the collection?",
+ "Delete collection": "Delete collection"
}
\ No newline at end of file