diff --git a/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx b/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx
index 35785a0368..ec293483c8 100644
--- a/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx
+++ b/Composer/packages/client/src/components/ProjectTree/ExpandableNode.tsx
@@ -4,6 +4,7 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { useState, MouseEvent, KeyboardEvent } from 'react';
+import { NeutralColors } from '@uifabric/fluent-theme';
import { INDENT_PER_LEVEL } from './constants';
@@ -14,18 +15,22 @@ type Props = {
detailsRef?: (el: HTMLElement | null) => void;
onToggle?: (newState: boolean) => void;
defaultState?: boolean;
+ isActive?: boolean;
};
-const summaryStyle = css`
+const summaryStyle = (depth: number, isActive: boolean) => css`
label: summary;
display: flex;
- padding-left: 12px;
+ padding-left: ${depth * INDENT_PER_LEVEL + 12}px;
padding-top: 6px;
+ :hover {
+ background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20};
+ }
+ background: ${isActive ? NeutralColors.gray30 : NeutralColors.white};
`;
-const nodeStyle = (depth: number) => css`
+const nodeStyle = css`
margin-top: 2px;
- margin-left: ${depth * INDENT_PER_LEVEL}px;
`;
const TRIANGLE_SCALE = 0.6;
@@ -42,7 +47,15 @@ const detailsStyle = css`
}
`;
-export const ExpandableNode = ({ children, summary, detailsRef, depth = 0, onToggle, defaultState = true }: Props) => {
+export const ExpandableNode = ({
+ children,
+ summary,
+ detailsRef,
+ depth = 0,
+ onToggle,
+ defaultState = true,
+ isActive = false,
+}: Props) => {
const [isExpanded, setExpanded] = useState(defaultState);
function setExpandedWithCallback(newState: boolean) {
@@ -62,11 +75,11 @@ export const ExpandableNode = ({ children, summary, detailsRef, depth = 0, onTog
}
return (
-
+
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-noninteractive-tabindex */}
{
hasChildren={!isRemote}
icon={isRemote ? icons.EXTERNAL_SKILL : icons.BOT}
isActive={doesLinkMatch(link, selectedLink)}
+ isChildSelected={isChildDialogLinkSelected(link, selectedLink)}
isMenuOpen={isMenuOpen}
link={link}
menu={options.showMenu ? menu : []}
diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx
index c1fb2a85a6..bd5c6c9e8e 100644
--- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx
+++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx
@@ -3,6 +3,7 @@
/** @jsx jsx */
import React, { useCallback, useState, useRef } from 'react';
+import { NeutralColors } from '@uifabric/fluent-theme';
import { jsx, css } from '@emotion/core';
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone';
@@ -32,7 +33,7 @@ import { TreeItem } from './treeItem';
import { ExpandableNode } from './ExpandableNode';
import { INDENT_PER_LEVEL } from './constants';
import { ProjectTreeHeader, ProjectTreeHeaderMenuItem } from './ProjectTreeHeader';
-import { doesLinkMatch } from './helpers';
+import { isChildTriggerLinkSelected, doesLinkMatch } from './helpers';
import { ProjectHeader } from './ProjectHeader';
// -------------------- Styles -------------------- //
@@ -71,10 +72,14 @@ const tree = css`
label: tree;
`;
-const headerCSS = (label: string) => css`
+const headerCSS = (label: string, isActive?: boolean) => css`
margin-top: -6px;
width: 100%;
label: ${label};
+ :hover {
+ background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20};
+ }
+ background: ${isActive ? NeutralColors.gray30 : NeutralColors.white};
`;
// -------------------- Helper functions -------------------- //
@@ -225,7 +230,7 @@ export const ProjectTree: React.FC = ({
// TODO Refactor to make sure tree is not generated until a new trigger/dialog is added. #5462
const createSubtree = useCallback(() => {
return projectCollection.map(createBotSubtree);
- }, [projectCollection, selectedLink]);
+ }, [projectCollection, selectedLink, leftSplitWidth]);
if (rootProjectId == null) {
// this should only happen before a project is loaded in, so it won't last very long
@@ -315,7 +320,7 @@ export const ProjectTree: React.FC = ({
@@ -323,6 +328,7 @@ export const ProjectTree: React.FC = ({
hasChildren
icon={isFormDialog ? icons.FORM_DIALOG : icons.DIALOG}
isActive={doesLinkMatch(dialogLink, selectedLink)}
+ isChildSelected={isChildTriggerLinkSelected(dialogLink, selectedLink)}
isMenuOpen={isMenuOpen}
link={dialogLink}
menu={options.showMenu ? menu : options.showQnAMenu ? [QnAMenuItem] : []}
@@ -376,7 +382,8 @@ export const ProjectTree: React.FC = ({
},
dialog: DialogInfo,
projectId: string,
- dialogLink: TreeLink
+ dialogLink: TreeLink,
+ depth: number
): React.ReactNode => {
const link: TreeLink = {
projectId: rootProjectId,
@@ -399,6 +406,7 @@ export const ProjectTree: React.FC = ({
isActive={doesLinkMatch(link, selectedLink)}
isMenuOpen={isMenuOpen}
link={link}
+ marginLeft={depth * INDENT_PER_LEVEL}
menu={
options.showDelete
? [
@@ -430,7 +438,13 @@ export const ProjectTree: React.FC = ({
return scope.toLowerCase().includes(filter.toLowerCase());
};
- const renderTriggerList = (triggers: ITrigger[], dialog: DialogInfo, projectId: string, dialogLink: TreeLink) => {
+ const renderTriggerList = (
+ triggers: ITrigger[],
+ dialog: DialogInfo,
+ projectId: string,
+ dialogLink: TreeLink,
+ depth: number
+ ) => {
return triggers
.filter((tr) => filterMatch(dialog.displayName) || filterMatch(getTriggerName(tr)))
.map((tr) => {
@@ -443,7 +457,8 @@ export const ProjectTree: React.FC = ({
{ ...tr, index, displayName: getTriggerName(tr), warningContent, errorContent },
dialog,
projectId,
- dialogLink
+ dialogLink,
+ depth
);
});
};
@@ -499,7 +514,7 @@ export const ProjectTree: React.FC = ({
summary={renderTriggerGroupHeader(groupDisplayName, dialog, projectId)}
onToggle={(newState) => setPageElement(key, newState)}
>
- {renderTriggerList(triggers, dialog, projectId, link)}
+ {renderTriggerList(triggers, dialog, projectId, link, 1)}
);
};
@@ -520,7 +535,7 @@ export const ProjectTree: React.FC = ({
const renderDialogTriggers = (dialog: DialogInfo, projectId: string, startDepth: number, dialogLink: TreeLink) => {
return dialogIsFormDialog(dialog)
? renderDialogTriggersByProperty(dialog, projectId, startDepth + 1)
- : renderTriggerList(dialog.triggers, dialog, projectId, dialogLink);
+ : renderTriggerList(dialog.triggers, dialog, projectId, dialogLink, 1);
};
const renderLgImport = (
@@ -650,6 +665,7 @@ export const ProjectTree: React.FC = ({
defaultState={getPageElement(key)}
depth={startDepth}
detailsRef={dialog.isRoot ? addMainDialogRef : undefined}
+ isActive={doesLinkMatch(dialogLink, selectedLink)}
summary={summaryElement}
onToggle={(newState) => setPageElement(key, newState)}
>
diff --git a/Composer/packages/client/src/components/ProjectTree/constants.ts b/Composer/packages/client/src/components/ProjectTree/constants.ts
index a447dd3f8e..307b7eb639 100644
--- a/Composer/packages/client/src/components/ProjectTree/constants.ts
+++ b/Composer/packages/client/src/components/ProjectTree/constants.ts
@@ -3,3 +3,5 @@
export const SUMMARY_ARROW_SPACE = 28; // the rough pixel size of the dropdown arrow to the left of a Details/Summary element
export const INDENT_PER_LEVEL = 16;
+export const ACTION_ICON_WIDTH = 28;
+export const THREE_DOTS_ICON_WIDTH = 28;
diff --git a/Composer/packages/client/src/components/ProjectTree/helpers.ts b/Composer/packages/client/src/components/ProjectTree/helpers.ts
index 673dbf4f6d..45e1f88201 100644
--- a/Composer/packages/client/src/components/ProjectTree/helpers.ts
+++ b/Composer/packages/client/src/components/ProjectTree/helpers.ts
@@ -13,3 +13,13 @@ export const doesLinkMatch = (linkInTree?: Partial, selectedLink?: Par
linkInTree.luFileId === selectedLink.luFileId
);
};
+
+export const isChildTriggerLinkSelected = (linkInTree?: Partial, selectedLink?: Partial) => {
+ if (linkInTree == null || selectedLink == null) return false;
+ return linkInTree.skillId === selectedLink.skillId && linkInTree.dialogId === selectedLink.dialogId;
+};
+
+export const isChildDialogLinkSelected = (linkInTree?: Partial, selectedLink?: Partial) => {
+ if (linkInTree == null || selectedLink == null) return false;
+ return linkInTree.skillId === selectedLink.skillId;
+};
diff --git a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx
index 899a0086a4..9d80b3729a 100644
--- a/Composer/packages/client/src/components/ProjectTree/treeItem.tsx
+++ b/Composer/packages/client/src/components/ProjectTree/treeItem.tsx
@@ -3,9 +3,9 @@
/** @jsx jsx */
import { jsx, css } from '@emotion/core';
-import React, { useState } from 'react';
-import { FontWeights } from '@uifabric/styling';
+import React, { useState, useCallback } from 'react';
import { FontSizes } from '@uifabric/fluent-theme';
+import { DefaultPalette } from '@uifabric/styling';
import { OverflowSet, IOverflowSetItemProps } from 'office-ui-fabric-react/lib/OverflowSet';
import { TooltipHost, DirectionalHint } from 'office-ui-fabric-react/lib/Tooltip';
import { ContextualMenuItemType, IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';
@@ -22,7 +22,7 @@ import isEmpty from 'lodash/isEmpty';
import uniqueId from 'lodash/uniqueId';
import { TreeLink, TreeMenuItem } from './ProjectTree';
-import { SUMMARY_ARROW_SPACE } from './constants';
+import { SUMMARY_ARROW_SPACE, THREE_DOTS_ICON_WIDTH } from './constants';
// -------------------- Styles -------------------- //
@@ -67,51 +67,62 @@ export const menuStyle: Partial = {
export const moreButton = (isActive: boolean): IButtonStyles => {
return {
root: {
- padding: '4px 4px 0 4px',
alignSelf: 'stretch',
visibility: isActive ? 'visible' : 'hidden',
- height: 'auto',
- width: '16px',
+ height: 24,
+ width: 24,
color: '#000',
},
menuIcon: {
fontSize: '12px',
- color: '#000',
+ color: NeutralColors.gray160,
+ },
+ rootHovered: {
+ color: DefaultPalette.accent,
+ selectors: {
+ '.ms-Button-menuIcon': {
+ fontWeight: 600,
+ },
+ },
},
};
};
-const navItem = (
- isActive: boolean,
- isBroken: boolean,
- padLeft: number,
- isAnyMenuOpen: boolean,
- menuOpenHere: boolean
-) => css`
+const navContainer = (isAnyMenuOpen: boolean, isActive: boolean, menuOpenHere: boolean, textWidth: number) => css`
+ ${isAnyMenuOpen
+ ? ''
+ : `&:hover {
+ background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20};
+
+ .dialog-more-btn {
+ visibility: visible;
+ }
+ .action-btn {
+ visibility: visible;
+ }
+ .treeItem-text {
+ max-width: ${textWidth}px;
+ }
+ }`};
+ background: ${isActive ? NeutralColors.gray30 : menuOpenHere ? '#f2f2f2' : 'transparent'};
+`;
+
+const navItem = (isBroken: boolean, padLeft: number, marginLeft: number, isActive: boolean) => css`
label: navItem;
position: relative;
height: 24px;
font-size: 12px;
padding-left: ${padLeft}px;
- color: ${isActive ? NeutralColors.white : '#545454'};
- background: ${isActive ? '#0078d4' : menuOpenHere ? '#f2f2f2' : 'transparent'};
+ margin-left: ${marginLeft}px;
opacity: ${isBroken ? 0.5 : 1};
- font-weight: ${isActive ? FontWeights.semibold : FontWeights.regular};
-
display: flex;
flex-direction: row;
align-items: center;
- ${isAnyMenuOpen
- ? ''
- : `&:hover {
- color: #545454;
- background: #f2f2f2;
-
- .dialog-more-btn {
- visibility: visible;
- }
- }`}
+ :hover {
+ background: ${isActive ? NeutralColors.gray40 : NeutralColors.gray20};
+ }
+ background: ${isActive ? NeutralColors.gray30 : NeutralColors.white};
&:focus {
outline: none;
@@ -143,7 +154,6 @@ export const overflowSet = (isBroken: boolean) => css`
width: 100%;
height: 100%;
box-sizing: border-box;
- line-height: 24px;
justify-content: space-between;
display: flex;
i {
@@ -154,6 +164,7 @@ export const overflowSet = (isBroken: boolean) => css`
const moreButtonContainer = {
root: {
lineHeight: '1',
+ display: 'flex' as 'flex',
},
};
@@ -201,11 +212,13 @@ const itemName = (nameWidth: number) => css`
const calloutRootStyle = css`
padding: 11px;
`;
+
// -------------------- TreeItem -------------------- //
-interface ITreeItemProps {
+type ITreeItemProps = {
link: TreeLink;
isActive?: boolean;
+ isChildSelected?: boolean;
isSubItemActive?: boolean;
onSelect?: (link: TreeLink) => void;
icon?: string;
@@ -213,12 +226,13 @@ interface ITreeItemProps {
textWidth?: number;
extraSpace?: number;
padLeft?: number;
+ marginLeft?: number;
hasChildren?: boolean;
menu?: TreeMenuItem[];
menuOpenCallback?: (cb: boolean) => void;
isMenuOpen?: boolean;
showErrors?: boolean;
-}
+};
const renderTreeMenuItem = (link: TreeLink) => (item: TreeMenuItem) => {
if (item.label === '') {
@@ -341,105 +355,10 @@ const DiagnosticIcons = (props: {
);
};
-const onRenderItem = (textWidth: number, showErrors: boolean) => (item: IOverflowSetItemProps) => {
- const { diagnostics = [], projectId, skillId, onErrorClick } = item;
-
- let warningContent = '';
- let errorContent = '';
-
- if (showErrors) {
- const warnings: Diagnostic[] = diagnostics.filter(
- (diag: Diagnostic) => diag.severity === DiagnosticSeverity.Warning
- );
- const errors: Diagnostic[] = diagnostics.filter((diag: Diagnostic) => diag.severity === DiagnosticSeverity.Error);
-
- warningContent = warnings.map((diag) => diag.message).join(',');
-
- errorContent = errors.map((diag) => diag.message).join(',');
- }
-
- return (
-
-
- {item.icon != null && (
-
- )}
- {item.displayName}
- {showErrors && (
-
- )}
-
-
- );
-};
-
-const onRenderOverflowButton = (
- isActive: boolean,
- menuOpenCallback: (cb: boolean) => void,
- setThisItemSelected: (sel: boolean) => void
-) => {
- const moreLabel = formatMessage('Actions');
- return (overflowItems: IContextualMenuItem[] | undefined) => {
- if (overflowItems == null) return null;
- return (
-
- {
- setThisItemSelected(true);
- menuOpenCallback(true);
- },
- onMenuDismissed: () => {
- setThisItemSelected(false);
- menuOpenCallback(false);
- },
- }}
- role="cell"
- styles={moreButton(isActive)}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- e.stopPropagation();
- }
- }}
- />
-
- );
- };
-};
-
export const TreeItem: React.FC = ({
link,
isActive = false,
+ isChildSelected = false,
icon,
dialogName,
onSelect,
@@ -448,12 +367,12 @@ export const TreeItem: React.FC = ({
menu = [],
extraSpace = 0,
padLeft = 0,
+ marginLeft = 0,
menuOpenCallback = () => {},
isMenuOpen = false,
showErrors = true,
}) => {
const [thisItemSelected, setThisItemSelected] = useState(false);
-
const a11yLabel = `${dialogName ?? '$Root'}_${link.displayName}`;
const overflowMenu = menu.map(renderTreeMenuItem(link));
@@ -462,42 +381,172 @@ export const TreeItem: React.FC = ({
const isBroken = !!link.botError;
const spacerWidth = hasChildren ? 0 : SUMMARY_ARROW_SPACE + extraSpace;
+ const overflowIconWidthOnHover = overflowMenu.length > 0 ? THREE_DOTS_ICON_WIDTH : 0;
+
+ const overflowIconWidthActiveOrChildSelected =
+ (isActive || isChildSelected) && overflowMenu.length > 0 ? THREE_DOTS_ICON_WIDTH : 0;
+
+ const onRenderItem = useCallback(
+ (maxTextWidth: number, showErrors: boolean) => (item: IOverflowSetItemProps) => {
+ const { diagnostics = [], projectId, skillId, onErrorClick } = item;
+
+ let warningContent = '';
+ let errorContent = '';
+ if (showErrors) {
+ const warnings: Diagnostic[] = diagnostics.filter(
+ (diag: Diagnostic) => diag.severity === DiagnosticSeverity.Warning
+ );
+ const errors: Diagnostic[] = diagnostics.filter(
+ (diag: Diagnostic) => diag.severity === DiagnosticSeverity.Error
+ );
+
+ warningContent = warnings.map((diag) => diag.message).join(',');
+
+ errorContent = errors.map((diag) => diag.message).join(',');
+ }
+
+ return (
+
+
+ {item.icon != null && (
+
+ )}
+
+ {item.displayName}
+
+ {showErrors && (
+
+ )}
+
+
+ );
+ },
+ [textWidth, spacerWidth, extraSpace, overflowIconWidthActiveOrChildSelected, showErrors]
+ );
+
+ const onRenderOverflowButton = useCallback(
+ (
+ isActive: boolean,
+ isChildSelected: boolean,
+ menuOpenCallback: (cb: boolean) => void,
+ setThisItemSelected: (sel: boolean) => void
+ ) => {
+ const moreLabel = formatMessage('More options');
+ return (overflowItems: IContextualMenuItem[] | undefined) => {
+ if (overflowItems == null) return null;
+ return (
+
+ {
+ setThisItemSelected(true);
+ menuOpenCallback(true);
+ },
+ onMenuDismissed: () => {
+ setThisItemSelected(false);
+ menuOpenCallback(false);
+ },
+ }}
+ role="cell"
+ styles={moreButton(isActive || isChildSelected)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.stopPropagation();
+ }
+ }}
+ />
+
+ );
+ };
+ },
+ [isActive, isChildSelected, menuOpenCallback, setThisItemSelected]
+ );
+
return (
{
- onSelect?.(link);
- }}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- onSelect?.(link);
- }
- }}
+ css={navContainer(
+ isMenuOpen,
+ isActive,
+ thisItemSelected,
+ textWidth - spacerWidth + extraSpace - overflowIconWidthOnHover
+ )}
>
-
-
+
{
+ onSelect?.(link);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ onSelect?.(link);
+ }
+ }}
+ >
+
+
+
);
};