Skip to content

Commit

Permalink
feat: show topics in left nav (microsoft#6987)
Browse files Browse the repository at this point in the history
* display topics in left nav

* only show external link icon on hover

* sort system topics at end of list

* show tooltip when hovering on pva topic

* show system topic icon

* collapse topic list with Enter

Co-authored-by: Ben Yackley <[email protected]>
Co-authored-by: Chris Whitten <[email protected]>
  • Loading branch information
3 people authored Apr 16, 2021
1 parent 0a50f34 commit 1ce457b
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 59 deletions.
4 changes: 2 additions & 2 deletions Composer/packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

import React, { Fragment, useEffect } from 'react';
import { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
import { useRecoilValue } from 'recoil';

import { Header } from './components/Header';
Expand All @@ -11,8 +10,9 @@ import { MainContainer } from './components/AppComponents/MainContainer';
import { dispatcherState, userSettingsState } from './recoilModel';
import { loadLocale } from './utils/fileUtil';
import { useInitializeLogger } from './telemetry/useInitializeLogger';
import { setupIcons } from './setupIcons';

initializeIcons(undefined, { disableWarnings: true });
setupIcons();

const Logger = () => {
useInitializeLogger();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ import { getBaseName } from '../../utils/fileUtil';

import { TreeItem } from './treeItem';
import { ExpandableNode } from './ExpandableNode';
import { INDENT_PER_LEVEL } from './constants';
import { INDENT_PER_LEVEL, LEVEL_PADDING, TREE_PADDING } from './constants';
import { ProjectTreeHeader, ProjectTreeHeaderMenuItem } from './ProjectTreeHeader';
import { isChildTriggerLinkSelected, doesLinkMatch } from './helpers';
import { ProjectHeader } from './ProjectHeader';
import { ProjectTreeOptions, TreeLink, TreeMenuItem } from './types';
import { TopicsList } from './TopicsList';

// -------------------- Styles -------------------- //

Expand All @@ -59,7 +60,7 @@ const tree = css`
label: tree;
`;

const headerCSS = (label: string, isActive?: boolean) => css`
export const headerCSS = (label: string, isActive?: boolean) => css`
margin-top: -6px;
width: 100%;
label: ${label};
Expand Down Expand Up @@ -111,9 +112,6 @@ type Props = {
headerPlaceholder?: string;
};

const TREE_PADDING = 100; // the horizontal space taken up by stuff in the tree other than text or indentation
const LEVEL_PADDING = 44; // the size of a reveal-triangle and the space around it

export const ProjectTree: React.FC<Props> = ({
headerMenu = [],
onBotDeleteDialog = () => {},
Expand Down Expand Up @@ -624,6 +622,7 @@ export const ProjectTree: React.FC<Props> = ({
const createDetailsTree = (bot: TreeDataPerProject, startDepth: number) => {
const { projectId, lgImportsList, luImportsList } = bot;
const dialogs = bot.sortedDialogs;
const topics = bot.topics ?? [];

const filteredDialogs =
filter == null || filter.length === 0
Expand All @@ -632,6 +631,9 @@ export const ProjectTree: React.FC<Props> = ({
(dialog) =>
filterMatch(dialog.displayName) || dialog.triggers.some((trigger) => filterMatch(getTriggerName(trigger)))
);
// eventually we will filter on topic trigger phrases
const filteredTopics =
filter == null || filter.length === 0 ? topics : topics.filter((topic) => filterMatch(topic.displayName));
const commonLink = options.showCommonLinks ? [renderCommonDialogHeader(projectId, 1)] : [];

const importedLgLinks = options.showLgImports
Expand Down Expand Up @@ -701,6 +703,15 @@ export const ProjectTree: React.FC<Props> = ({
return renderDialogHeader(projectId, dialog, 1, bot.isPvaSchema).summaryElement;
}
}),
filteredTopics.length > 0 && (
<TopicsList
key={`pva-topics-${projectId}`}
projectId={projectId}
textWidth={leftSplitWidth - TREE_PADDING}
topics={filteredTopics}
onToggle={(newState) => setPageElement('pva-topics', newState)}
/>
),
];
};

Expand Down
84 changes: 84 additions & 0 deletions Composer/packages/client/src/components/ProjectTree/TopicsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx } from '@emotion/core';
import React from 'react';
import { DialogInfo } from '@bfc/shared';
import formatMessage from 'format-message';
import get from 'lodash/get';

import { ExpandableNode } from './ExpandableNode';
import { TreeItem } from './treeItem';
import { LEVEL_PADDING, INDENT_PER_LEVEL } from './constants';
import { headerCSS } from './ProjectTree';

type TopicsListProps = {
onToggle: (newState: boolean) => void;
topics: DialogInfo[];
textWidth: number;
projectId: string;
};

export const TopicsList: React.FC<TopicsListProps> = ({ topics, onToggle, textWidth, projectId }) => {
const linkTooltip = formatMessage('Open in Power Virtual Agents');

const renderTopic = (topic: DialogInfo) => {
const isSystemTopic = get(topic.content, 'isSystemTopic', false);

return (
<TreeItem
key={topic.id}
dialogName={topic.displayName}
extraSpace={INDENT_PER_LEVEL}
isActive={false}
isMenuOpen={false}
itemType={isSystemTopic ? 'system topic' : 'topic'}
link={{
projectId,
dialogId: topic.id,
displayName: topic.displayName,
href: get(topic.content, '$designer.link'),
tooltip: linkTooltip,
}}
marginLeft={1 * INDENT_PER_LEVEL}
role="treeitem"
textWidth={textWidth}
onSelect={(link) => {
if (link.href) {
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(link.href, '_blank');
}
}}
/>
);
};

return (
<ExpandableNode
key="pva-topics"
depth={1}
summary={
<span css={headerCSS('pva-topics')}>
<TreeItem
hasChildren
isActive={false}
isChildSelected={false}
isMenuOpen={false}
itemType="topic"
link={{
displayName: formatMessage('Power Virtual Agents Topics ({count})', { count: topics.length }),
projectId,
}}
padLeft={0 * LEVEL_PADDING}
showErrors={false}
textWidth={textWidth}
/>
</span>
}
onToggle={onToggle}
>
<div>{topics.map(renderTopic)}</div>
</ExpandableNode>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React from 'react';
import { TooltipHost, DirectionalHint } from 'office-ui-fabric-react/lib/Tooltip';

type TreeItemContentProps = {
tooltip?: string | JSX.Element | JSX.Element[];
};

export const TreeItemContent: React.FC<TreeItemContentProps> = ({ children, tooltip }) => {
if (tooltip) {
return (
<TooltipHost content={tooltip} directionalHint={DirectionalHint.bottomCenter}>
{children}
</TooltipHost>
);
}

return <React.Fragment>{children}</React.Fragment>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const SUMMARY_ARROW_SPACE = 28; // the rough pixel size of the dropdown a
export const INDENT_PER_LEVEL = 16;
export const ACTION_ICON_WIDTH = 28;
export const THREE_DOTS_ICON_WIDTH = 28;
export const TREE_PADDING = 100; // the horizontal space taken up by stuff in the tree other than text or indentation
export const LEVEL_PADDING = 44; // the size of a reveal-triangle and the space around it
117 changes: 73 additions & 44 deletions Composer/packages/client/src/components/ProjectTree/treeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import uniqueId from 'lodash/uniqueId';

import { SUMMARY_ARROW_SPACE, THREE_DOTS_ICON_WIDTH } from './constants';
import { TreeLink, TreeMenuItem } from './types';
import { TreeItemContent } from './TreeItemContent';

// -------------------- Styles -------------------- //

Expand Down Expand Up @@ -112,7 +113,10 @@ const navContainer = (
.treeItem-text {
max-width: ${textWidth}px;
}
}`};
.external-link {
visibility: visible;
}
}`};
background: ${isActive ? NeutralColors.gray30 : menuOpenHere ? '#f2f2f2' : 'transparent'};
Expand Down Expand Up @@ -214,7 +218,7 @@ const diagnosticWarningIcon = {
color: '#8A8780',
background: '#FFF4CE',
};
const itemName = (nameWidth: number) => css`
export const itemName = (nameWidth: number) => css`
max-width: ${nameWidth}px;
overflow: hidden;
text-overflow: ellipsis;
Expand All @@ -228,6 +232,8 @@ const calloutRootStyle = css`
type TreeObject =
| 'bot'
| 'dialog'
| 'topic'
| 'system topic'
| 'trigger' // basic ProjectTree elements
| 'trigger group'
| 'form dialog'
Expand All @@ -241,6 +247,8 @@ const TreeIcons: { [key in TreeObject]: string | null } = {
bot: Icons.BOT,
dialog: Icons.DIALOG,
trigger: Icons.TRIGGER,
topic: Icons.TOPIC,
'system topic': Icons.SYSTEM_TOPIC,
'trigger group': null,
'form dialog': Icons.FORM_DIALOG,
'form field': Icons.FORM_FIELD, // x in parentheses
Expand All @@ -253,6 +261,8 @@ const TreeIcons: { [key in TreeObject]: string | null } = {
const objectNames: { [key in TreeObject]: () => string } = {
trigger: () => formatMessage('Trigger'),
dialog: () => formatMessage('Dialog'),
topic: () => formatMessage('User Topic'),
'system topic': () => formatMessage('System Topic'),
'trigger group': () => formatMessage('Trigger group'),
'form dialog': () => formatMessage('Form dialog'),
'form field': () => formatMessage('Form field'),
Expand Down Expand Up @@ -428,6 +438,7 @@ export const TreeItem: React.FC<ITreeItemProps> = ({

const ariaLabel = `${objectNames[itemType]()} ${link.displayName}`;
const dataTestId = `${dialogName ?? '$Root'}_${link.displayName}`;
const isExternal = Boolean(link.href);

const overflowMenu = menu.map(renderTreeMenuItem(link));

Expand Down Expand Up @@ -460,41 +471,50 @@ export const TreeItem: React.FC<ITreeItemProps> = ({
}

return (
<div
data-is-focusable
aria-label={`${ariaLabel} ${warningContent} ${errorContent}`}
css={projectTreeItemContainer}
tabIndex={0}
onBlur={item.onBlur}
onFocus={item.onFocus}
>
<div css={projectTreeItem} role="presentation" tabIndex={-1}>
{item.itemType != null && TreeIcons[item.itemType] != null && (
<Icon
iconName={TreeIcons[item.itemType]}
styles={{
root: {
width: '12px',
marginRight: '8px',
outline: 'none',
},
}}
tabIndex={-1}
/>
)}
<span className={'treeItem-text'} css={itemName(maxTextWidth)}>
{item.displayName}
</span>
{showErrors && (
<DiagnosticIcons
diagnostics={diagnostics}
projectId={projectId}
skillId={skillId}
onErrorClick={onErrorClick}
/>
)}
<TreeItemContent tooltip={link.tooltip}>
<div
data-is-focusable
aria-label={`${ariaLabel} ${warningContent} ${errorContent}`}
css={projectTreeItemContainer}
tabIndex={0}
onBlur={item.onBlur}
onFocus={item.onFocus}
>
<div css={projectTreeItem} role="presentation" tabIndex={-1}>
{item.itemType != null && TreeIcons[item.itemType] != null && (
<Icon
iconName={TreeIcons[item.itemType]}
styles={{
root: {
width: '12px',
marginRight: '8px',
outline: 'none',
},
}}
tabIndex={-1}
/>
)}
<span className={'treeItem-text'} css={itemName(maxTextWidth)}>
{item.displayName}
</span>
{isExternal && (
<Icon
className="external-link"
iconName="NavigateExternalInline"
styles={{ root: { visibility: 'hidden', width: '12px', marginLeft: '4px', outline: 'none' } }}
/>
)}
{showErrors && (
<DiagnosticIcons
diagnostics={diagnostics}
projectId={projectId}
skillId={skillId}
onErrorClick={onErrorClick}
/>
)}
</div>
</div>
</div>
</TreeItemContent>
);
},
[textWidth, spacerWidth, extraSpace, overflowIconWidthActiveOrChildSelected, showErrors]
Expand Down Expand Up @@ -566,14 +586,23 @@ export const TreeItem: React.FC<ITreeItemProps> = ({
data-testid={dataTestId}
role={role}
tabIndex={0}
onClick={() => {
onSelect?.(link);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onSelect?.(link);
}
}}
onClick={
onSelect
? () => {
onSelect(link);
}
: undefined
}
onKeyDown={
onSelect
? (e) => {
if (e.key === 'Enter') {
onSelect(link);
e.stopPropagation();
}
}
: undefined
}
>
<div style={{ minWidth: `${spacerWidth}px` }} />
<OverflowSet
Expand Down
Loading

0 comments on commit 1ce457b

Please sign in to comment.