diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx index 49c3d1fa67..4f66b81d50 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx @@ -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'; @@ -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', @@ -137,6 +143,8 @@ export const ProjectTree: React.FC = ({ const currentProjectId = useRecoilValue(currentProjectIdState); const botProjectSpace = useRecoilValue(botProjectSpaceSelector); + const jsonSchemaFilesByProjectId = useRecoilValue(jsonSchemaFilesByProjectIdSelector); + const notificationMap: { [projectId: string]: { [dialogId: string]: Diagnostic[] } } = {}; for (const bot of projectCollection) { @@ -155,6 +163,10 @@ export const ProjectTree: React.FC = ({ 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); }; @@ -244,7 +256,7 @@ export const ProjectTree: React.FC = ({ = ({ ); }; - 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, @@ -271,7 +283,7 @@ export const ProjectTree: React.FC = ({ trigger: item.index, dialogName: dialog.id, isRoot: false, - projectId: currentProjectId, + projectId, skillId: null, }; @@ -307,6 +319,80 @@ export const ProjectTree: React.FC = ({ 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 ( + + + + ); + }; + + // 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 ( + +
{renderTriggerList(triggers, dialog, projectId)}
+
+ ); + }; + + // 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); @@ -321,19 +407,6 @@ export const ProjectTree: React.FC = ({ 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 ( = ({ detailsRef={dialog.isRoot ? addMainDialogRef : undefined} summary={renderDialogHeader(projectId, dialog)} > -
{triggerList}
+
{renderDialogTriggers(dialog, projectId, startDepth + 1)}
); }); diff --git a/Composer/packages/client/src/recoilModel/selectors/project.ts b/Composer/packages/client/src/recoilModel/selectors/project.ts index cebb4de1a9..43468508c0 100644 --- a/Composer/packages/client/src/recoilModel/selectors/project.ts +++ b/Composer/packages/client/src/recoilModel/selectors/project.ts @@ -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, @@ -15,6 +15,7 @@ import { botNameIdentifierState, formDialogSchemaIdsState, formDialogSchemaState, + jsonSchemaFilesState, } from '../atoms'; // Actions @@ -76,3 +77,15 @@ export const formDialogSchemaDialogExistsSelector = selectorFamily d.id === schemaId); }, }); + +export const jsonSchemaFilesByProjectIdSelector = selector({ + key: 'jsonSchemaFilesByProjectIdSelector', + get: ({ get }) => { + const projectIds = get(botProjectIdsState); + const result: Record = {}; + projectIds.forEach((projectId) => { + result[projectId] = get(jsonSchemaFilesState(projectId)); + }); + return result; + }, +});