Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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