Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added trigger grouping for form dialogs #4475

Merged
merged 8 commits into from
Oct 22, 2020
113 changes: 93 additions & 20 deletions Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ import debounce from 'lodash/debounce';
import { useRecoilValue } from 'recoil';
import { ISearchBoxStyles } from 'office-ui-fabric-react/lib/SearchBox';
import isEqual from 'lodash/isEqual';

import { dispatcherState, currentProjectIdState, botProjectSpaceSelector } from '../../recoilModel';
import { extractSchemaProperties, groupTriggersByPropertyReference } from '@bfc/indexers';

import {
dispatcherState,
currentProjectIdState,
botProjectSpaceSelector,
jsonSchemaFilesByProjectIdSelector,
} from '../../recoilModel';
import { getFriendlyName } from '../../utils/dialogUtil';
import { triggerNotSupported } from '../../utils/dialogValidator';

Expand Down Expand Up @@ -47,7 +53,7 @@ const icons = {
DIALOG: 'Org',
BOT: 'CubeShape',
EXTERNAL_SKILL: 'Globe',
FORM_DIALOG: '',
FORM_DIALOG: 'Table',
FORM_FIELD: 'Variable2', // x in parentheses
FORM_TRIGGER: 'TriggerAuto', // lightning bolt with gear
FILTER: 'Filter',
Expand Down Expand Up @@ -137,6 +143,8 @@ export const ProjectTree: React.FC<Props> = ({
const currentProjectId = useRecoilValue(currentProjectIdState);
const botProjectSpace = useRecoilValue(botProjectSpaceSelector);

const jsonSchemaFilesByProjectId = useRecoilValue(jsonSchemaFilesByProjectIdSelector);

const notificationMap: { [projectId: string]: { [dialogId: string]: Diagnostic[] } } = {};

for (const bot of projectCollection) {
Expand All @@ -155,6 +163,10 @@ export const ProjectTree: React.FC<Props> = ({
notificationMap[currentProjectId][dialog.id]?.some((diag) => diag.severity === DiagnosticSeverity.Warning);
};

const dialogIsFormDialog = (dialog: DialogInfo) => {
return process.env.COMPOSER_ENABLE_FORMS && dialog.content?.schema !== undefined;
};

const botHasWarnings = (bot: BotInProject) => {
return bot.dialogs.some(dialogHasWarnings);
};
Expand Down Expand Up @@ -244,7 +256,7 @@ export const ProjectTree: React.FC<Props> = ({
<TreeItem
showProps
forceIndent={showTriggers ? 0 : SUMMARY_ARROW_SPACE}
icon={icons.DIALOG}
icon={dialogIsFormDialog(dialog) ? icons.FORM_DIALOG : icons.DIALOG}
isSubItemActive={isEqual(link, selectedLink)}
link={link}
menu={[
Expand All @@ -262,7 +274,7 @@ export const ProjectTree: React.FC<Props> = ({
);
};

const renderTrigger = (projectId: string, item: any, dialog: DialogInfo): React.ReactNode => {
const renderTrigger = (item: any, dialog: DialogInfo, projectId: string): React.ReactNode => {
// NOTE: put the form-dialog detection here when it's ready
const link: TreeLink = {
displayName: item.displayName,
Expand All @@ -271,7 +283,7 @@ export const ProjectTree: React.FC<Props> = ({
trigger: item.index,
dialogName: dialog.id,
isRoot: false,
projectId: currentProjectId,
GeoffCoxMSFT marked this conversation as resolved.
Show resolved Hide resolved
projectId,
skillId: null,
};

Expand Down Expand Up @@ -307,6 +319,80 @@ export const ProjectTree: React.FC<Props> = ({
return scope.toLowerCase().includes(filter.toLowerCase());
};

const renderTriggerList = (triggers: ITrigger[], dialog: DialogInfo, projectId: string) => {
return triggers
.filter((tr) => filterMatch(dialog.displayName) || filterMatch(getTriggerName(tr)))
.map((tr, index) => {
const warningContent = triggerNotSupported(dialog, tr);
const errorContent = notificationMap[projectId][dialog.id].some(
(diag) => diag.severity === DiagnosticSeverity.Error && diag.path?.match(RegExp(`triggers\\[${index}\\]`))
);
return renderTrigger(
{ ...tr, index, displayName: getTriggerName(tr), warningContent, errorContent },
dialog,
projectId
);
});
};

const renderTriggerGroupHeader = (groupName: string, dialog: DialogInfo, projectId: string) => {
const link: TreeLink = {
dialogName: dialog.id,
displayName: groupName,
isRoot: false,
projectId: projectId,
skillId: null,
};
return (
<span
css={css`
margin-top: -6px;
width: 100%;
label: trigger-group-header;
`}
role="grid"
GeoffCoxMSFT marked this conversation as resolved.
Show resolved Hide resolved
>
<TreeItem showProps forceIndent={0} isSubItemActive={false} link={link} />
</span>
);
};

// renders a named expandible node with the triggers as items underneath
const renderTriggerGroup = (
projectId: string,
dialog: DialogInfo,
groupName: string,
triggers: ITrigger[],
startDepth: number
) => {
const key = `${projectId}.${dialog.id}.group-{groupName}`;

return (
<ExpandableNode key={key} depth={startDepth} summary={renderTriggerGroupHeader(groupName, dialog, projectId)}>
<div>{renderTriggerList(triggers, dialog, projectId)}</div>
</ExpandableNode>
);
};

// renders triggers grouped by the schema property they are associated with.
const renderDialogTriggersByProperty = (dialog: DialogInfo, projectId: string, startDepth: number) => {
const jsonSchemaFiles = jsonSchemaFilesByProjectId[projectId];
const dialogSchemaProperties = extractSchemaProperties(dialog, jsonSchemaFiles);
const groupedTriggers = groupTriggersByPropertyReference(dialog, { validProperties: dialogSchemaProperties });

const triggerGroups = Object.keys(groupedTriggers);

return triggerGroups.map((triggerGroup) => {
return renderTriggerGroup(projectId, dialog, triggerGroup, groupedTriggers[triggerGroup], startDepth);
});
};

const renderDialogTriggers = (dialog: DialogInfo, projectId: string, startDepth: number) => {
return dialogIsFormDialog(dialog)
? renderDialogTriggersByProperty(dialog, projectId, startDepth)
: renderTriggerList(dialog.triggers, dialog, projectId);
};

const createDetailsTree = (bot: BotInProject, startDepth: number) => {
const { projectId } = bot;
const dialogs = sortDialog(bot.dialogs);
Expand All @@ -321,27 +407,14 @@ export const ProjectTree: React.FC<Props> = ({

if (showTriggers) {
return filteredDialogs.map((dialog: DialogInfo) => {
const triggerList = dialog.triggers
.filter((tr) => filterMatch(dialog.displayName) || filterMatch(getTriggerName(tr)))
.map((tr, index) => {
const warningContent = triggerNotSupported(dialog, tr);
const errorContent = notificationMap[projectId][dialog.id].some(
(diag) => diag.severity === DiagnosticSeverity.Error && diag.path?.match(RegExp(`triggers\\[${index}\\]`))
);
return renderTrigger(
projectId,
{ ...tr, index, displayName: getTriggerName(tr), warningContent, errorContent },
dialog
);
});
return (
<ExpandableNode
key={dialog.id}
depth={startDepth}
detailsRef={dialog.isRoot ? addMainDialogRef : undefined}
summary={renderDialogHeader(projectId, dialog)}
>
<div>{triggerList}</div>
<div>{renderDialogTriggers(dialog, projectId, startDepth + 1)}</div>
</ExpandableNode>
);
});
Expand Down
15 changes: 14 additions & 1 deletion Composer/packages/client/src/recoilModel/selectors/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { selector, selectorFamily } from 'recoil';
import isEmpty from 'lodash/isEmpty';
import { FormDialogSchema } from '@bfc/shared';
import { FormDialogSchema, JsonSchemaFile } from '@bfc/shared';

import {
botErrorState,
Expand All @@ -15,6 +15,7 @@ import {
botNameIdentifierState,
formDialogSchemaIdsState,
formDialogSchemaState,
jsonSchemaFilesState,
} from '../atoms';

// Actions
Expand Down Expand Up @@ -76,3 +77,15 @@ export const formDialogSchemaDialogExistsSelector = selectorFamily<boolean, { pr
return !!dialogs.find((d) => d.id === schemaId);
},
});

export const jsonSchemaFilesByProjectIdSelector = selector({
key: 'jsonSchemaFilesByProjectIdSelector',
get: ({ get }) => {
const projectIds = get(botProjectIdsState);
const result: Record<string, JsonSchemaFile[]> = {};
projectIds.forEach((projectId) => {
result[projectId] = get(jsonSchemaFilesState(projectId));
});
return result;
},
});