diff --git a/Composer/packages/client/config/extensions.config.js b/Composer/packages/client/config/extensions.config.js index c1fe1357a8..da8f3122ee 100644 --- a/Composer/packages/client/config/extensions.config.js +++ b/Composer/packages/client/config/extensions.config.js @@ -3,101 +3,53 @@ const path = require('path'); const PnpWebpackPlugin = require('pnp-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); -const paths = require('./paths'); - module.exports = (webpackEnv) => { - const isEnvDevelopment = webpackEnv === 'development'; const isEnvProduction = webpackEnv === 'production'; - return [ - { - entry: { - 'react-bundle': 'react', - }, - mode: isEnvProduction ? 'production' : 'development', - // export react globally under a variable named React - output: { - path: path.resolve(__dirname, '../public'), - library: 'React', - libraryTarget: 'var', - }, - resolve: { - extensions: ['.js'], - }, - optimization: { - minimize: isEnvProduction, - minimizer: [ - new TerserPlugin({ - extractComments: false, - }), - ], - }, + return { + mode: isEnvProduction ? 'production' : 'development', + entry: { + 'plugin-host-preload': path.resolve(__dirname, '../extension-container/plugin-host-preload.tsx'), + }, + output: { + path: path.resolve(__dirname, '../public'), + filename: '[name].js', }, - { - entry: { - 'react-dom-bundle': 'react-dom', - }, - mode: isEnvProduction ? 'production' : 'development', - // export react-dom globally under a variable named ReactDOM - output: { - path: path.resolve(__dirname, '../public'), - library: 'ReactDOM', - libraryTarget: 'var', - }, - externals: { - // ReactDOM depends on React, but we need this to resolve to the globally-exposed React variable in react-bundle.js (created by extensions.config.js). - // If we don't do this, ReactDom will bundle its own copy of React and we will have 2 copies which breaks hooks. - react: 'React', - }, - resolve: { - extensions: ['.js'], - }, - optimization: { - minimize: isEnvProduction, - minimizer: [ - new TerserPlugin({ - extractComments: false, + resolve: { + extensions: ['.js'], + plugins: [PnpWebpackPlugin], + }, + resolveLoader: { + plugins: [ + // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders + // from the current package. + PnpWebpackPlugin.moduleLoader(module), + ], + }, + module: { + rules: [ + { + test: /\.tsx?$/, + loader: require.resolve('ts-loader'), + include: [path.resolve(__dirname, '../extension-container')], + options: PnpWebpackPlugin.tsLoaderOptions({ + transpileOnly: true, + configFile: path.resolve(__dirname, '../tsconfig.build.json'), }), - ], - }, + }, + ], }, - { - mode: isEnvProduction ? 'production' : 'development', - entry: { - 'plugin-host-preload': path.resolve(__dirname, '../extension-container/plugin-host-preload.tsx'), - }, - output: { - path: path.resolve(__dirname, '../public'), - }, - externals: { - // expect react & react-dom to be available in the extension host iframe globally under "React" and "ReactDOM" variables - react: 'React', - 'react-dom': 'ReactDOM', - }, - resolve: { - extensions: ['.js'], - plugins: [PnpWebpackPlugin], - }, - resolveLoader: { - plugins: [ - // Also related to Plug'n'Play, but this time it tells Webpack to load its loaders - // from the current package. - PnpWebpackPlugin.moduleLoader(module), - ], - }, - module: { - rules: [ - { - test: /\.tsx?$/, - loader: require.resolve('ts-loader'), - include: [path.resolve(__dirname, '../extension-container')], - options: PnpWebpackPlugin.tsLoaderOptions({ - transpileOnly: true, - configFile: path.resolve(__dirname, '../tsconfig.build.json'), - }), + optimization: { + minimize: isEnvProduction, + + minimizer: [ + new TerserPlugin({ + extractComments: false, + terserOptions: { + sourceMap: true, }, - ], - }, + }), + ], }, - ]; + }; }; diff --git a/Composer/packages/client/extension-container/plugin-host-preload.tsx b/Composer/packages/client/extension-container/plugin-host-preload.tsx index 13117fd1ac..6f67c236b4 100644 --- a/Composer/packages/client/extension-container/plugin-host-preload.tsx +++ b/Composer/packages/client/extension-container/plugin-host-preload.tsx @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import React from 'react'; import ReactDOM from 'react-dom'; +import * as ExtensionClient from '@bfc/extension-client'; +import { syncStore, Shell } from '@bfc/extension-client'; if (!document.head.title) { const title = document.createElement('title'); @@ -33,9 +36,27 @@ if (!document.getElementById('plugin-root')) { document.body.appendChild(root); } // initialize the API object +window.React = React; +window.ReactDOM = ReactDOM; +window.ExtensionClient = ExtensionClient; +// eslint-disable-next-line @typescript-eslint/ban-ts-ignore +// @ts-ignore window.Composer = {}; // init the render function -window.Composer.render = function (component) { +window.Composer.render = function (type: string, shell: Shell, component: React.ReactElement) { + // eslint-disable-next-line no-underscore-dangle + window.Composer.__pluginType = type; + + if (shell) { + syncStore(shell); + } + ReactDOM.render(component, document.getElementById('plugin-root')); }; + +window.Composer.sync = function (shell: Shell) { + syncStore(shell); +}; + +window.parent?.postMessage('host-preload-complete', '*'); diff --git a/Composer/packages/client/package.json b/Composer/packages/client/package.json index 85798a9ce4..5bbfbb3ac3 100644 --- a/Composer/packages/client/package.json +++ b/Composer/packages/client/package.json @@ -10,7 +10,7 @@ "scripts": { "start": "yarn build:extension-bundles && node scripts/start.js", "build": "node --max_old_space_size=4096 scripts/build.js", - "build:extension-bundles": "webpack --config ./config/extensions.config.js", + "build:extension-bundles": "webpack --config ./config/extensions.config.js --env production", "clean": "rimraf build", "test": "jest", "lint": "eslint --quiet --ext .js,.jsx,.ts,.tsx ./src ./__tests__", diff --git a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx index 07957cd8df..4b61efc9f7 100644 --- a/Composer/packages/client/src/components/PluginHost/PluginHost.tsx +++ b/Composer/packages/client/src/components/PluginHost/PluginHost.tsx @@ -3,11 +3,11 @@ /** @jsx jsx */ import { jsx, SerializedStyles } from '@emotion/core'; -import * as React from 'react'; -import { useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; +import { Shell } from '@bfc/types'; +import { PluginType } from '@bfc/extension-client'; import { PluginAPI } from '../../plugins/api'; -import { PluginType } from '../../plugins/types'; import { iframeStyle } from './styles'; @@ -16,14 +16,20 @@ interface PluginHostProps { pluginName: string; pluginType: PluginType; bundleId: string; + shell?: Shell; } /** Binds closures around Composer client code to plugin iframe's window object */ -function attachPluginAPI(win: Window, type: PluginType) { +function attachPluginAPI(win: Window, type: PluginType, shell?: object) { const api = { ...PluginAPI[type], ...PluginAPI.auth }; + for (const method in api) { win.Composer[method] = (...args) => api[method](...args); } + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + win.Composer.render = win.Composer.render.bind(null, type, shell); } function injectScript(doc: Document, id: string, src: string, async: boolean, onload?: () => any) { @@ -39,38 +45,52 @@ function injectScript(doc: Document, id: string, src: string, async: boolean, on */ export const PluginHost: React.FC = (props) => { const targetRef = useRef(null); - const { extraIframeStyles = [] } = props; + const [isLoaded, setIsLoaded] = useState(false); + const { extraIframeStyles = [], pluginType, pluginName, bundleId, shell } = props; useEffect(() => { - const { pluginName, pluginType, bundleId } = props; // renders the plugin's UI inside of the iframe - const renderPluginView = async () => { - if (pluginName && pluginType) { - const iframeWindow = targetRef.current?.contentWindow as Window; - const iframeDocument = targetRef.current?.contentDocument as Document; - - // inject the react / react-dom bundles - injectScript(iframeDocument, 'react-bundle', '/react-bundle.js', false); - injectScript(iframeDocument, 'react-dom-bundle', '/react-dom-bundle.js', false); - // // load the preload script to setup the plugin API - injectScript(iframeDocument, 'preload-bundle', '/plugin-host-preload.js', false, () => { - attachPluginAPI(iframeWindow, pluginType); - }); - - //load the bundle for the specified plugin - const pluginScriptId = `plugin-${pluginType}-${pluginName}`; - await new Promise((resolve) => { - const cb = () => { - resolve(); - }; - const bundleUri = `/api/extensions/${pluginName}/${bundleId}`; - // If plugin bundles end up being too large and block the client thread due to the load, enable the async flag on this call - injectScript(iframeDocument, pluginScriptId, bundleUri, false, cb); - }); - } - }; - renderPluginView(); - }, [props.pluginName, props.pluginType, props.bundleId, targetRef]); - - return ; + if (pluginName && pluginType) { + const iframeDocument = targetRef.current?.contentDocument as Document; + + // // load the preload script to setup the plugin API + injectScript(iframeDocument, 'preload-bundle', '/plugin-host-preload.js', false); + + const onPreloaded = (ev) => { + if (ev.data === 'host-preload-complete') { + setIsLoaded(true); + } + }; + + window.addEventListener('message', onPreloaded); + + return () => { + window.removeEventListener('message', onPreloaded); + }; + } + }, [pluginName, pluginType, bundleId, targetRef]); + + useEffect(() => { + if (isLoaded && pluginType && pluginName && bundleId) { + const iframeWindow = targetRef.current?.contentWindow as Window; + const iframeDocument = targetRef.current?.contentDocument as Document; + + attachPluginAPI(iframeWindow, pluginType, shell); + + //load the bundle for the specified plugin + const pluginScriptId = `plugin-${pluginType}-${pluginName}`; + const bundleUri = `/api/extensions/${pluginName}/${bundleId}`; + // If plugin bundles end up being too large and block the client thread due to the load, enable the async flag on this call + injectScript(iframeDocument, pluginScriptId, bundleUri, false); + } + }, [isLoaded]); + + // sync the shell to the iframe store when shell changes + useEffect(() => { + if (isLoaded && targetRef.current) { + targetRef.current.contentWindow?.Composer.sync(shell); + } + }, [isLoaded, shell]); + + return ; }; diff --git a/Composer/packages/client/src/pages/plugin/pluginPageContainer.tsx b/Composer/packages/client/src/pages/plugin/PluginPageContainer.tsx similarity index 57% rename from Composer/packages/client/src/pages/plugin/pluginPageContainer.tsx rename to Composer/packages/client/src/pages/plugin/PluginPageContainer.tsx index c589300d45..fd101bb17d 100644 --- a/Composer/packages/client/src/pages/plugin/pluginPageContainer.tsx +++ b/Composer/packages/client/src/pages/plugin/PluginPageContainer.tsx @@ -5,15 +5,19 @@ import React from 'react'; import { RouteComponentProps } from '@reach/router'; import { PluginHost } from '../../components/PluginHost/PluginHost'; +import { useShell } from '../../shell'; -const PluginPageContainer: React.FC> = (props) => { - const { pluginId, bundleId } = props; +const PluginPageContainer: React.FC> = ( + props +) => { + const { pluginId, bundleId, projectId } = props; + const shell = useShell('DesignPage', projectId as string); - if (!pluginId || !bundleId) { + if (!pluginId || !bundleId || !projectId) { return null; } - return ; + return ; }; export { PluginPageContainer }; diff --git a/Composer/packages/client/src/recoilModel/selectors/extensions.ts b/Composer/packages/client/src/recoilModel/selectors/extensions.ts index 6839f16bf8..32ab01aadb 100644 --- a/Composer/packages/client/src/recoilModel/selectors/extensions.ts +++ b/Composer/packages/client/src/recoilModel/selectors/extensions.ts @@ -4,6 +4,7 @@ import { selector } from 'recoil'; import { extensionsState } from '../atoms/appState'; +import { ExtensionPageConfig } from '../../utils/pageLinks'; export const enabledExtensionsSelector = selector({ key: 'enabledExtensionsSelector', @@ -13,3 +14,18 @@ export const enabledExtensionsSelector = selector({ return extensions.filter((e) => e.enabled); }, }); + +export const pluginPagesSelector = selector({ + key: 'pluginPagesSelector', + get: ({ get }) => { + const extensions = get(enabledExtensionsSelector); + + return extensions.reduce((pages, p) => { + const pagesConfig = p.contributes?.views?.pages; + if (Array.isArray(pagesConfig) && pagesConfig.length > 0) { + pages.push(...pagesConfig.map((page) => ({ ...page, id: p.id }))); + } + return pages; + }, [] as ExtensionPageConfig[]); + }, +}); diff --git a/Composer/packages/client/src/router.tsx b/Composer/packages/client/src/router.tsx index c915c7b28f..037bc9eff8 100644 --- a/Composer/packages/client/src/router.tsx +++ b/Composer/packages/client/src/router.tsx @@ -12,11 +12,11 @@ import { resolveToBasePath } from './utils/fileUtil'; import { data } from './styles'; import { NotFound } from './components/NotFound'; import { BASEPATH } from './constants'; -import { dispatcherState, schemasState, botProjectIdsState, botOpeningState } from './recoilModel'; +import { dispatcherState, schemasState, botProjectIdsState, botOpeningState, pluginPagesSelector } from './recoilModel'; import { openAlertModal } from './components/Modal/AlertDialog'; import { dialogStyle } from './components/Modal/dialogStyle'; import { LoadingSpinner } from './components/LoadingSpinner'; -import { PluginPageContainer } from './pages/plugin/pluginPageContainer'; +import { PluginPageContainer } from './pages/plugin/PluginPageContainer'; const DesignPage = React.lazy(() => import('./pages/design/DesignPage')); const LUPage = React.lazy(() => import('./pages/language-understanding/LUPage')); @@ -31,6 +31,8 @@ const FormDialogPage = React.lazy(() => import('./pages/form-dialog/FormDialogPa const Routes = (props) => { const botOpening = useRecoilValue(botOpeningState); + const pluginPages = useRecoilValue(pluginPagesSelector); + return (
}> @@ -59,11 +61,18 @@ const Routes = (props) => { + {pluginPages.map((page) => ( + + ))} - diff --git a/Composer/packages/client/src/shell/lgApi.ts b/Composer/packages/client/src/shell/lgApi.ts index ab7f47e70b..007c81970e 100644 --- a/Composer/packages/client/src/shell/lgApi.ts +++ b/Composer/packages/client/src/shell/lgApi.ts @@ -29,6 +29,13 @@ function createLgApi( return file.templates; }; + const updateLgFile = async (id: string, content: string) => { + const file = lgFileResolver(id); + if (!file) throw new Error(fileNotFound(id)); + + await actions.updateLgFile({ id, content, projectId: state.projectId }); + }; + const updateLgTemplate = async (id: string, templateName: string, templateBody: string) => { const file = lgFileResolver(id); if (!file) throw new Error(fileNotFound(id)); @@ -81,6 +88,7 @@ function createLgApi( }; return { + updateLgFile, addLgTemplate: updateLgTemplate, getLgTemplates, updateLgTemplate, diff --git a/Composer/packages/client/src/shell/luApi.ts b/Composer/packages/client/src/shell/luApi.ts index 5c7ac05231..cbc43eae28 100644 --- a/Composer/packages/client/src/shell/luApi.ts +++ b/Composer/packages/client/src/shell/luApi.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { useEffect, useState } from 'react'; -import { LuFile, LuIntentSection } from '@bfc/shared'; +import { LuFile, LuIntentSection, LuContextApi } from '@bfc/types'; import { useRecoilValue } from 'recoil'; import formatMessage from 'format-message'; import debounce from 'lodash/debounce'; @@ -20,7 +20,14 @@ function createLuApi( state: { focusPath: string; projectId: string }, dispatchers: Dispatcher, luFileResolver: (id: string) => LuFile | undefined -) { +): LuContextApi { + const updateLuFile = async (id: string, content: string) => { + const file = luFileResolver(id); + if (!file) throw new Error(fileNotFound(id)); + + await dispatchers.updateLuFile({ id, content, projectId: state.projectId }); + }; + const addLuIntent = async (id: string, intentName: string, intent: LuIntentSection) => { const file = luFileResolver(id); if (!file) throw new Error(fileNotFound(id)); @@ -73,6 +80,7 @@ function createLuApi( }; return { + updateLuFile, addLuIntent, getLuIntents, getLuIntent, diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index e536a10baa..40931185e5 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { useMemo, useRef } from 'react'; -import { ShellApi, ShellData, Shell, DialogSchemaFile } from '@bfc/shared'; +import { ShellApi, ShellData, Shell, DialogSchemaFile, DialogInfo } from '@bfc/types'; import { useRecoilValue } from 'recoil'; import formatMessage from 'format-message'; @@ -214,44 +214,40 @@ export function useShell(source: EventSource, projectId: string): Shell { updateFlowZoomRate, }; - const currentDialog = useMemo(() => dialogs.find((d) => d.id === dialogId), [dialogs, dialogId]); + const currentDialog = useMemo(() => dialogs.find((d) => d.id === dialogId), [dialogs, dialogId]) as DialogInfo; const editorData = useMemo(() => { return source === 'PropertyEditor' ? getDialogData(dialogsMap, dialogId, focused || selected || '') : getDialogData(dialogsMap, dialogId); }, [source, dialogsMap, dialogId, focused, selected]); - const data: ShellData = currentDialog - ? { - data: editorData, - locale, - botName, - projectId, - dialogs, - dialogSchemas, - dialogId, - focusPath, - schemas, - lgFiles, - luFiles, - qnaFiles, - currentDialog, - userSettings, - designerId: editorData?.$designer?.id, - focusedEvent: selected, - focusedActions: focused ? [focused] : [], - focusedSteps: focused ? [focused] : selected ? [selected] : [], - focusedTab: promptTab, - clipboardActions, - hosted: !!isAbsHosted(), - luFeatures: settings.luFeatures, - skills, - skillsSettings: settings.skill || {}, - flowZoomRate, - } - : ({ - projectId, - } as ShellData); + const data: ShellData = { + data: editorData, + locale, + botName, + projectId, + dialogs, + dialogSchemas, + dialogId, + focusPath, + schemas, + lgFiles, + luFiles, + qnaFiles, + currentDialog, + userSettings, + designerId: editorData?.$designer?.id, + focusedEvent: selected, + focusedActions: focused ? [focused] : [], + focusedSteps: focused ? [focused] : selected ? [selected] : [], + focusedTab: promptTab, + clipboardActions, + hosted: !!isAbsHosted(), + luFeatures: settings.luFeatures, + skills, + skillsSettings: settings.skill || {}, + flowZoomRate, + }; return { api, diff --git a/Composer/packages/client/src/types/window.d.ts b/Composer/packages/client/src/types/window.d.ts index 75e2cc6501..18e8c289d8 100644 --- a/Composer/packages/client/src/types/window.d.ts +++ b/Composer/packages/client/src/types/window.d.ts @@ -1,19 +1,30 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -interface Window { - /** - * Electron mechanism used for communication from renderer to main process. - */ - ipcRenderer: IPCRenderer; +import * as ExtensionClient from '@bfc/extension-client'; - /** - * Flag that is set on the window object when the client is embedded within Electron. - */ - __IS_ELECTRON__?: boolean; +declare global { + interface Window { + /** + * Electron mechanism used for communication from renderer to main process. + */ + ipcRenderer: IPCRenderer; - /** - * Composer UI Extension API - */ - Composer: any; + /** + * Flag that is set on the window object when the client is embedded within Electron. + */ + __IS_ELECTRON__?: boolean; + + /** + * Composer UI Extension API + */ + Composer: { + __pluginType: string; + render: (type: string, shell: Shell, component: React.ReactElement) => void; + sync: (shell: Shell) => void; + [key: string]: any; + }; + + ExtensionClient: typeof ExtensionClient; + } } diff --git a/Composer/packages/client/src/utils/hooks.ts b/Composer/packages/client/src/utils/hooks.ts index c24bc5e8b7..2eaac47ea4 100644 --- a/Composer/packages/client/src/utils/hooks.ts +++ b/Composer/packages/client/src/utils/hooks.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; import { globalHistory } from '@reach/router'; import replace from 'lodash/replace'; import find from 'lodash/find'; import { useRecoilValue } from 'recoil'; -import { designPageLocationState, enabledExtensionsSelector, currentProjectIdState } from '../recoilModel'; +import { designPageLocationState, currentProjectIdState, pluginPagesSelector } from '../recoilModel'; -import { bottomLinks, topLinks, ExtensionPageConfig } from './pageLinks'; +import { bottomLinks, topLinks } from './pageLinks'; import routerCache from './routerCache'; import { projectIdCache } from './projectCache'; @@ -25,19 +25,14 @@ export const useLocation = () => { export const useLinks = () => { const projectId = useRecoilValue(currentProjectIdState); const designPageLocation = useRecoilValue(designPageLocationState(projectId)); - const extensions = useRecoilValue(enabledExtensionsSelector); + const pluginPages = useRecoilValue(pluginPagesSelector); const openedDialogId = designPageLocation.dialogId || 'Main'; - // add page-contributing extensions - const pluginPages = extensions.reduce((pages, p) => { - const pagesConfig = p.contributes?.views?.pages; - if (Array.isArray(pagesConfig) && pagesConfig.length > 0) { - pages.push(...pagesConfig.map((page) => ({ ...page, id: p.id }))); - } - return pages; - }, [] as ExtensionPageConfig[]); + const pageLinks = useMemo(() => { + return topLinks(projectId, openedDialogId, pluginPages); + }, [projectId, openedDialogId, pluginPages]); - return { topLinks: topLinks(projectId, openedDialogId, pluginPages), bottomLinks }; + return { topLinks: pageLinks, bottomLinks }; }; export const useRouterCache = (to: string) => { diff --git a/Composer/packages/client/src/utils/pageLinks.ts b/Composer/packages/client/src/utils/pageLinks.ts index 8f761c137d..7e9deb52e6 100644 --- a/Composer/packages/client/src/utils/pageLinks.ts +++ b/Composer/packages/client/src/utils/pageLinks.ts @@ -84,11 +84,11 @@ export const topLinks = (projectId: string, openedDialogId: string, pluginPages: if (pluginPages.length > 0) { pluginPages.forEach((p) => { links.push({ - to: `plugin/${p.id}/${p.bundleId}`, + to: `/bot/${projectId}/plugin/${p.id}/${p.bundleId}`, iconName: p.icon ?? 'StatusCircleQuestionMark', labelName: p.label, exact: true, - disabled: false, + disabled: !projectId, }); }); } diff --git a/Composer/packages/extension-client/src/hooks/index.ts b/Composer/packages/extension-client/src/hooks/index.ts index 5bc77489c7..0b5a724b56 100644 --- a/Composer/packages/extension-client/src/hooks/index.ts +++ b/Composer/packages/extension-client/src/hooks/index.ts @@ -9,5 +9,6 @@ export * from './useLgApi'; export * from './useLuApi'; export * from './useMenuConfig'; export * from './useRecognizerConfig'; +export * from './useProjectApi'; export * from './useShellApi'; export * from './useTriggerApi'; diff --git a/Composer/packages/extension-client/src/hooks/useProjectApi.ts b/Composer/packages/extension-client/src/hooks/useProjectApi.ts new file mode 100644 index 0000000000..b6ee4116eb --- /dev/null +++ b/Composer/packages/extension-client/src/hooks/useProjectApi.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useMemo } from 'react'; +import pick from 'lodash/pick'; +import { ProjectContext, ProjectContextApi } from '@bfc/types'; + +import { validateHookContext } from '../utils/validateHookContext'; + +import { useStore } from './useStore'; + +const PROJECT_KEYS = [ + 'data.botName', + 'data.projectId', + 'data.dialogs', + 'data.dialogSchemas', + 'data.lgFiles', + 'data.luFiles', + 'data.qnaFiles', + 'data.skills', + 'data.skillsSettings', + 'data.schemas', + + 'api.getDialog', + 'api.saveDialog', + 'api.updateQnaContent', + 'api.updateRegExIntent', + 'api.renameRegExIntent', + 'api.updateIntentTrigger', + 'api.createDialog', + 'api.commitChanges', + 'api.addSkillDialog', + 'api.displayManifestModal', + 'api.updateDialogSchema', + 'api.createTrigger', + 'api.updateSkillSetting', +]; + +export function useProjectApi(): ProjectContext & ProjectContextApi { + const shell = useStore(); + + const projectContext = useMemo(() => { + const ctx = pick(shell, PROJECT_KEYS); + return { + ...ctx.api, + ...ctx.data, + } as ProjectContext & ProjectContextApi; + }, [pick(shell, PROJECT_KEYS)]); + + validateHookContext('project'); + + return projectContext; +} diff --git a/Composer/packages/extension-client/src/hooks/useStore.ts b/Composer/packages/extension-client/src/hooks/useStore.ts new file mode 100644 index 0000000000..31f90fd8b5 --- /dev/null +++ b/Composer/packages/extension-client/src/hooks/useStore.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useState, useEffect } from 'react'; + +import { State, __store__, Store } from '../store'; + +export function useStore() { + const [, dispatch] = useState>(__store__.getState()); + + useEffect(() => { + __store__.addListener(dispatch); + + return () => { + __store__.removeListener(dispatch); + }; + }, []); + + const store: Store = { + getState: __store__.getState.bind(__store__), + setState: __store__.setState.bind(__store__), + }; + + return store.getState(); +} diff --git a/Composer/packages/extension-client/src/index.ts b/Composer/packages/extension-client/src/index.ts index ec87761835..6bebb5884d 100644 --- a/Composer/packages/extension-client/src/index.ts +++ b/Composer/packages/extension-client/src/index.ts @@ -11,3 +11,5 @@ export * from './hooks'; export * from './publish'; export * from './types'; export * from './utils'; + +export { syncStore } from './store'; diff --git a/Composer/packages/extension-client/src/store.ts b/Composer/packages/extension-client/src/store.ts new file mode 100644 index 0000000000..245164f7a7 --- /dev/null +++ b/Composer/packages/extension-client/src/store.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dispatch } from 'react'; +import { Shell } from '@bfc/types'; + +export type State = Shell; + +export type Store = { + getState: () => State; + setState: (newState: Partial) => void; +}; + +class ExtensionStore implements Store { + private state: State = {} as Shell; + private listeners: Dispatch>[] = []; + + constructor(initialState = {}) { + this.setState(initialState); + } + + getState() { + return this.state; + } + + setState(newState: Partial) { + this.state = { ...this.state, ...newState }; + + this.listeners.forEach((dispatch) => { + dispatch(this.state); + }); + } + + addListener(listener: Dispatch>) { + this.listeners.push(listener); + } + + removeListener(listener: Dispatch>) { + this.listeners = this.listeners.filter((l) => l !== listener); + } +} + +// eslint-disable-next-line no-underscore-dangle +const __store__ = new ExtensionStore(); + +export function syncStore(data = {}) { + __store__.setState(data); +} + +export { __store__ }; diff --git a/Composer/packages/extension-client/src/types/index.ts b/Composer/packages/extension-client/src/types/index.ts index a44ce84f99..bc071a2148 100644 --- a/Composer/packages/extension-client/src/types/index.ts +++ b/Composer/packages/extension-client/src/types/index.ts @@ -7,3 +7,4 @@ export * from './formSchema'; export * from './flowSchema'; export * from './menuSchema'; export * from './recognizerSchema'; +export * from './pluginType'; diff --git a/Composer/packages/client/src/plugins/types.ts b/Composer/packages/extension-client/src/types/pluginType.ts similarity index 55% rename from Composer/packages/client/src/plugins/types.ts rename to Composer/packages/extension-client/src/types/pluginType.ts index 1dfa876955..6ec1bfaf9b 100644 --- a/Composer/packages/client/src/plugins/types.ts +++ b/Composer/packages/extension-client/src/types/pluginType.ts @@ -1,4 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export type PluginType = 'publish' | 'page' | 'storage' | 'create'; +export type PluginType = 'page' | 'publish' | 'storage' | 'create'; diff --git a/Composer/packages/extension-client/src/types/window.d.ts b/Composer/packages/extension-client/src/types/window.d.ts new file mode 100644 index 0000000000..ac40226c4e --- /dev/null +++ b/Composer/packages/extension-client/src/types/window.d.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Shell } from '@bfc/types'; + +import * as ExtensionClient from '../index'; + +declare global { + interface Window { + /** + * Electron mechanism used for communication from renderer to main process. + */ + ipcRenderer: IPCRenderer; + + /** + * Flag that is set on the window object when the client is embedded within Electron. + */ + __IS_ELECTRON__?: boolean; + + /** + * Composer UI Extension API + */ + Composer: { + __pluginType: string; + render: (component: React.ReactElement) => void; + sync: (shell: Shell) => void; + [key: string]: any; + }; + + ExtensionClient: typeof ExtensionClient; + } +} diff --git a/Composer/packages/extension-client/src/utils/validateHookContext.ts b/Composer/packages/extension-client/src/utils/validateHookContext.ts new file mode 100644 index 0000000000..7ca3e51185 --- /dev/null +++ b/Composer/packages/extension-client/src/utils/validateHookContext.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { PluginType } from '../types/pluginType'; + +type HookContext = 'project'; + +const pluginTypeToContextMap: { [key in PluginType]: HookContext[] } = { + page: ['project'], + publish: [], + storage: ['project'], + create: [], +}; + +export function validateHookContext(targetContext: HookContext) { + // eslint-disable-next-line no-underscore-dangle + const validContexts = pluginTypeToContextMap[window.Composer.__pluginType] ?? []; + + if (!validContexts.includes(targetContext)) { + const msg = Object.entries(pluginTypeToContextMap) + .reduce((types, [type, ctxs]) => { + if (ctxs.includes(targetContext)) { + types.push(`- ${type}`); + } + + return types; + }, [] as string[]) + .join('\n'); + throw Error(`Invalid use of ${targetContext} api. Only available in these contexts:\n${msg}`); + } +} diff --git a/Composer/packages/extension/src/manager/__tests__/manager.test.ts b/Composer/packages/extension/src/manager/__tests__/manager.test.ts index 27ea3be5b2..b64c3b9216 100644 --- a/Composer/packages/extension/src/manager/__tests__/manager.test.ts +++ b/Composer/packages/extension/src/manager/__tests__/manager.test.ts @@ -103,6 +103,8 @@ describe('#loadAll', () => { let loadSpy: jest.SpyInstance; beforeEach(() => { + (manifest.getExtensions as jest.Mock).mockReturnValue({}); + manager = new ExtensionManagerImp(manifest); loadSpy = jest.spyOn(manager, 'loadFromDir'); diff --git a/Composer/packages/extension/src/manager/manager.ts b/Composer/packages/extension/src/manager/manager.ts index 198d04dd4c..f081ee330d 100644 --- a/Composer/packages/extension/src/manager/manager.ts +++ b/Composer/packages/extension/src/manager/manager.ts @@ -4,7 +4,7 @@ import path from 'path'; import glob from 'globby'; -import { readJson, ensureDir, remove } from 'fs-extra'; +import { readJson, ensureDir, remove, pathExists } from 'fs-extra'; import { ExtensionBundle, PackageJSON, ExtensionMetadata } from '@bfc/types'; import { ExtensionContext } from '../extensionContext'; @@ -62,6 +62,8 @@ export class ExtensionManagerImp { await this.loadFromDir(this.builtinDir, true); await this.loadFromDir(this.remoteDir); + + await this.cleanManifest(); } /** @@ -219,6 +221,15 @@ export class ExtensionManagerImp { return bundle.path; } + private async cleanManifest() { + for (const ext of this.getAll()) { + if (!(await pathExists(ext.path))) { + log('Removing %s. It is in the manifest but could not be located.', ext.id); + this.remove(ext.id); + } + } + } + private async getPackageJson(id: string, dir: string): Promise { try { const extensionPackagePath = path.resolve(dir, id, 'package.json'); diff --git a/Composer/packages/types/src/shell.ts b/Composer/packages/types/src/shell.ts index c395cd0228..915987455a 100644 --- a/Composer/packages/types/src/shell.ts +++ b/Composer/packages/types/src/shell.ts @@ -40,73 +40,53 @@ export type BotSchemas = { diagnostics?: any[]; }; -export type ShellData = { +export type ApplicationContextApi = { + navTo: (path: string, rest?: any) => void; + updateUserSettings: (settings: AllPartial) => void; + announce: (message: string) => void; + addCoachMarkRef: (ref: { [key: string]: any }) => void; +}; + +export type ApplicationContext = { locale: string; - botName: string; - currentDialog: DialogInfo; - projectId: string; - data: { - $kind: string; - [key: string]: any; - }; - designerId: string; - dialogId: string; - dialogs: DialogInfo[]; - dialogSchemas: DialogSchemaFile[]; - focusedEvent: string; - focusedActions: string[]; - focusedSteps: string[]; - focusedTab?: string; - focusPath: string; - clipboardActions: any[]; hosted: boolean; - lgFiles: LgFile[]; - luFiles: LuFile[]; - luFeatures: ILUFeaturesConfig; - qnaFiles: QnAFile[]; userSettings: UserSettings; - skills: any[]; - skillsSettings: Record; - // TODO: remove - schemas: BotSchemas; flowZoomRate: ZoomInfo; }; -export type ShellApi = { - getDialog: (dialogId: string) => any; - saveDialog: (dialogId: string, newDialogData: any) => any; - saveData: (newData: T, updatePath?: string) => void; - navTo: (path: string, rest?: any) => void; - onFocusSteps: (stepIds: string[], focusedTab?: string) => void; - onFocusEvent: (eventId: string) => void; - onSelect: (ids: string[]) => void; - getLgTemplates: (id: string) => LgTemplate[]; - copyLgTemplate: (id: string, fromTemplateName: string, toTemplateName?: string) => Promise; - addLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; - updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; - debouncedUpdateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; - removeLgTemplate: (id: string, templateName: string) => Promise; - removeLgTemplates: (id: string, templateNames: string[]) => Promise; +export type LuContextApi = { getLuIntent: (id: string, intentName: string) => LuIntentSection | undefined; getLuIntents: (id: string) => LuIntentSection[]; addLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise; + updateLuFile: (id: string, content: string) => Promise; updateLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise; debouncedUpdateLuIntent: (id: string, intentName: string, intent: LuIntentSection) => Promise; renameLuIntent: (id: string, intentName: string, newIntentName: string) => Promise; removeLuIntent: (id: string, intentName: string) => void; +}; + +export type LgContextApi = { + getLgTemplates: (id: string) => LgTemplate[]; + copyLgTemplate: (id: string, fromTemplateName: string, toTemplateName?: string) => Promise; + addLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; + updateLgFile: (id: string, content: string) => Promise; + updateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; + debouncedUpdateLgTemplate: (id: string, templateName: string, templateStr: string) => Promise; + removeLgTemplate: (id: string, templateName: string) => Promise; + removeLgTemplates: (id: string, templateNames: string[]) => Promise; +}; + +export type ProjectContextApi = { + getDialog: (dialogId: string) => any; + saveDialog: (dialogId: string, newDialogData: any) => any; + updateQnaContent: (id: string, content: string) => void; updateRegExIntent: (id: string, intentName: string, pattern: string) => void; renameRegExIntent: (id: string, intentName: string, newIntentName: string) => void; updateIntentTrigger: (id: string, intentName: string, newIntentName: string) => void; createDialog: (actions: any) => Promise; - addCoachMarkRef: (ref: { [key: string]: any }) => void; - onCopy: (clipboardActions: any[]) => void; - undo: () => void; - redo: () => void; commitChanges: () => void; - updateUserSettings: (settings: AllPartial) => void; addSkillDialog: () => Promise<{ manifestUrl: string; name: string } | null>; - announce: (message: string) => void; displayManifestModal: (manifestId: string) => void; updateDialogSchema: (_: DialogSchemaFile) => Promise; createTrigger: (id: string, formData, url?: string) => void; @@ -114,6 +94,54 @@ export type ShellApi = { updateFlowZoomRate: (currentRate: number) => void; }; +export type ProjectContext = { + botName: string; + projectId: string; + dialogs: DialogInfo[]; + dialogSchemas: DialogSchemaFile[]; + lgFiles: LgFile[]; + luFiles: LuFile[]; + luFeatures: ILUFeaturesConfig; + qnaFiles: QnAFile[]; + skills: any[]; + skillsSettings: Record; + schemas: BotSchemas; +}; + +export type DialogEditingContextApi = { + saveData: (newData: T, updatePath?: string) => void; + onFocusSteps: (stepIds: string[], focusedTab?: string) => void; + onFocusEvent: (eventId: string) => void; + onSelect: (ids: string[]) => void; + onCopy: (clipboardActions: any[]) => void; + undo: () => void; + redo: () => void; +}; + +export type DialogEditingContext = { + currentDialog: DialogInfo; + data: { + $kind: string; + [key: string]: any; + }; + designerId: string; + dialogId: string; + clipboardActions: any[]; + focusedEvent: string; + focusedActions: string[]; + focusedSteps: string[]; + focusedTab?: string; + focusPath: string; +}; + +export type ShellData = ApplicationContext & ProjectContext & DialogEditingContext; + +export type ShellApi = ApplicationContextApi & + ProjectContextApi & + DialogEditingContextApi & + LgContextApi & + LuContextApi; + export type Shell = { api: ShellApi; data: ShellData; diff --git a/Composer/plugins/sample-ui-plugin/package.json b/Composer/plugins/sample-ui-plugin/package.json index d05f805e34..aeddae0f15 100644 --- a/Composer/plugins/sample-ui-plugin/package.json +++ b/Composer/plugins/sample-ui-plugin/package.json @@ -22,13 +22,16 @@ ], "contributes": { "views": { - "publish": [{ - "bundleId": "publish" - }], - "pages-DISABLED": [ + "publish": [ + { + "bundleId": "publish" + } + ], + "pages": [ { "bundleId": "page", - "label": "Sample UI Plugin" + "label": "Sample UI Plugin", + "icon": "FolderList" } ] } @@ -37,6 +40,7 @@ "main": "dist/index.js", "dependencies": { "@bfc/extension-client": "file:../../packages/extension-client", + "@fluentui/react": "^7.145.0", "emotion": "^10.0.27", "react": "^16.13.0", "react-dom": "^16.13.0" diff --git a/Composer/plugins/sample-ui-plugin/src/client/page/index.tsx b/Composer/plugins/sample-ui-plugin/src/client/page/index.tsx index 6f5ac4606f..183b7fd42e 100644 --- a/Composer/plugins/sample-ui-plugin/src/client/page/index.tsx +++ b/Composer/plugins/sample-ui-plugin/src/client/page/index.tsx @@ -1,34 +1,123 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import * as React from 'react'; -import { useCallback, useState } from 'react'; -import { render } from '@bfc/extension-client'; -import { cx } from 'emotion'; +import React, { useMemo } from 'react'; +import { render, useProjectApi } from '@bfc/extension-client'; +import { + DetailsList, + ScrollablePane, + ScrollbarVisibility, + CheckboxVisibility, + DetailsListLayoutMode, + IColumn, + TextField, +} from '@fluentui/react'; -import { label, output, pageRoot, shortTextField, textField } from '../styles'; +const Main: React.FC = () => { + const { dialogs, lgFiles, luFiles, qnaFiles, saveDialog } = useProjectApi(); -const Main: React.FC<{}> = (props) => { - const [text, setText] = useState(''); + const items = useMemo(() => { + const all: any[] = []; - const onInputChange = useCallback((ev) => { - setText(ev.target.value); - }, []); + dialogs.forEach((d) => { + all.push({ + name: d.id, + type: 'dialog', + content: d.content, + }); + }); + + luFiles.forEach((d) => { + all.push({ + name: d.id, + type: 'lu', + content: d.content, + }); + }); + + lgFiles.forEach((d) => { + all.push({ + name: d.id, + type: 'lg', + content: d.content, + }); + }); + + qnaFiles.forEach((d) => { + all.push({ + name: d.id, + type: 'qna', + content: d.content, + }); + }); + + return all; + }, [dialogs, luFiles, lgFiles, qnaFiles]); + + const handleFileChange = (type: string, fileId: string, data?: string) => { + try { + switch (type) { + case 'dialog': + return saveDialog(fileId, JSON.parse(data)); + } + } catch (err) { + console.error(err); + } + }; + + const tableColumns: IColumn[] = [ + { + key: 'name', + name: 'Name', + fieldName: 'name', + minWidth: 150, + maxWidth: 200, + isResizable: true, + data: 'string', + }, + { + key: 'type', + name: 'Type', + fieldName: 'type', + minWidth: 200, + maxWidth: 450, + isResizable: true, + data: 'string', + }, + { + key: 'content', + name: 'Content', + minWidth: 200, + maxWidth: 800, + isResizable: true, + onRender: (item) => { + if (item) { + const content = typeof item.content === 'string' ? item.content : JSON.stringify(item.content, null, 2); + + return ( + handleFileChange(item.type, item.name, newVal)} + /> + ); + } + }, + }, + ]; return ( -
- - -

{text}

-
+ + + Dialog data from project api +
{JSON.stringify(dialogs[0].content, null, 2)}
+
); }; diff --git a/Composer/plugins/sample-ui-plugin/tsconfig.json b/Composer/plugins/sample-ui-plugin/tsconfig.json index 43030ee21b..63c65b0bfd 100644 --- a/Composer/plugins/sample-ui-plugin/tsconfig.json +++ b/Composer/plugins/sample-ui-plugin/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "alwaysStrict": true, - "jsx": "react" + "jsx": "react", + "esModuleInterop": true }, "include": [ "src/client/**/*", diff --git a/Composer/plugins/sample-ui-plugin/webpack.config.js b/Composer/plugins/sample-ui-plugin/webpack.config.js index 0fdfc56a9a..1eb8d3f021 100644 --- a/Composer/plugins/sample-ui-plugin/webpack.config.js +++ b/Composer/plugins/sample-ui-plugin/webpack.config.js @@ -18,6 +18,7 @@ module.exports = { // expect react & react-dom to be available in the extension host iframe globally under "React" and "ReactDOM" variables react: 'React', 'react-dom': 'ReactDOM', + '@bfc/extension-client': 'ExtensionClient', }, module: { rules: [{ test: /\.tsx?$/, use: 'ts-loader' }], diff --git a/Composer/plugins/sample-ui-plugin/yarn.lock b/Composer/plugins/sample-ui-plugin/yarn.lock index 6cdfd82ac0..a5f563bf50 100644 --- a/Composer/plugins/sample-ui-plugin/yarn.lock +++ b/Composer/plugins/sample-ui-plugin/yarn.lock @@ -49,7 +49,17 @@ "@bfc/extension-client@file:../../packages/extension-client": version "1.0.0" dependencies: + "@bfc/shared" "file:../../../../../Library/Caches/Yarn/v6/npm-@bfc-extension-client-1.0.0-c66e4324-c38c-41dc-a538-e4cab00a6748-1602528055919/node_modules/@bfc/lib/shared" debug "^4.1.1" + lodash "^4.17.19" + +"@bfc/shared@file:../../packages/lib/shared": + version "0.0.0" + dependencies: + format-message "6.2.3" + json-schema "^0.2.5" + nanoid "^3.1.3" + nanoid-dictionary "^3.0.0" "@emotion/cache@^10.0.27": version "10.0.29" @@ -107,6 +117,73 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@fluentui/date-time-utilities@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@fluentui/date-time-utilities/-/date-time-utilities-7.9.0.tgz#4c8570b3af8bc654963ecb034d0fd100010e7d6d" + integrity sha512-D8p5WWeonqRO1EgIvo7WSlX1rcm87r2VQd62zTJPQImx8rpwc77CRI+iAvfxyVHRZMdt4Qk6Jq99dUaudPWaZw== + dependencies: + "@uifabric/set-version" "^7.0.23" + tslib "^1.10.0" + +"@fluentui/dom-utilities@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@fluentui/dom-utilities/-/dom-utilities-1.1.1.tgz#b0bbab665fe726f245800bb9e7883b1ceb54248b" + integrity sha512-w40gi8fzCpwa7U8cONiuu8rszPStkVOL/weDf5pCbYEb1gdaV7MDPSNkgM6IV0Kz+k017noDgK9Fv4ru1Dwz1g== + dependencies: + "@uifabric/set-version" "^7.0.23" + tslib "^1.10.0" + +"@fluentui/keyboard-key@^0.2.12": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@fluentui/keyboard-key/-/keyboard-key-0.2.12.tgz#74eddf4657c164193b6c8855746e691af466441a" + integrity sha512-t3yIbbPKJubb22vQ/FIWwS9vFAzaPYzFxKWPHVWLtxs/P+5yL+LD3B16DRtYreWAdl9CZvEbos58ChLZ0KHwSQ== + dependencies: + tslib "^1.10.0" + +"@fluentui/react-focus@^7.16.10": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@fluentui/react-focus/-/react-focus-7.16.10.tgz#e82e0719f48877bfc7220d359db4859c4d10b99d" + integrity sha512-+4aP36KjD2RrijRBr6aPYNiBm9M9+33DOHpAdcE0K93TToLIlQ/WIwZGDNbM/6dPFD6vgUj+ya5rvfy6sbibjw== + dependencies: + "@fluentui/keyboard-key" "^0.2.12" + "@uifabric/merge-styles" "^7.19.1" + "@uifabric/set-version" "^7.0.23" + "@uifabric/styling" "^7.16.10" + "@uifabric/utilities" "^7.32.4" + tslib "^1.10.0" + +"@fluentui/react-window-provider@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@fluentui/react-window-provider/-/react-window-provider-0.3.3.tgz#2950c4e19b28f24079d0e9d6a7a7e4d5b79ad115" + integrity sha512-MVPf2hqOQ17LAZsuvGcr3oOHksAskUm+fCYdXFhbVoAgsCDVTIuH6i8XgHFd6YjBtzjZmI4+k/3NTQfDqBX8EQ== + dependencies: + "@uifabric/set-version" "^7.0.23" + tslib "^1.10.0" + +"@fluentui/react@^7.145.0": + version "7.145.0" + resolved "https://registry.yarnpkg.com/@fluentui/react/-/react-7.145.0.tgz#d9ff288cbaf13a61c5d0d36d19eaa693519e4e4c" + integrity sha512-RNYTWwRQhIFKoMHmDkMLF48KcH91brHV/WXCEboIxsmtHm4HyYVG4Zpjib6UG2XOS1zk9sBefHao/zTx3pdG8Q== + dependencies: + "@uifabric/set-version" "^7.0.23" + office-ui-fabric-react "^7.145.0" + tslib "^1.10.0" + +"@fluentui/theme@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@fluentui/theme/-/theme-1.3.0.tgz#15a65c6ff20c11a1a7550919b1a71b9c922e9255" + integrity sha512-OVN3yPdJShOGhTakalI5DC9+8myOhLPKzY/ZlICnNoKhh7cbPVrvdCnv2fLcDyZYMQg0CnB3oAaghBHiIW3jmA== + dependencies: + "@uifabric/merge-styles" "^7.19.1" + "@uifabric/set-version" "^7.0.23" + "@uifabric/utilities" "^7.32.4" + tslib "^1.10.0" + +"@microsoft/load-themed-styles@^1.10.26": + version "1.10.109" + resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.109.tgz#75baf9732fbc9311bf8c238b4b122186d5133899" + integrity sha512-SmMg0H0HiYfGRahbMeNpvZo6+eQ2+Er1oXzDdHGiNqNY/9lwnKRVL98GtN0kguQfSs2uRjxv3UB7B0+kAT4wIg== + "@types/node@^14.6.2": version "14.6.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f" @@ -117,6 +194,74 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@uifabric/foundation@^7.9.10": + version "7.9.10" + resolved "https://registry.yarnpkg.com/@uifabric/foundation/-/foundation-7.9.10.tgz#3855fe1490e20eb2ac5c8bfb428d7488dd6989bf" + integrity sha512-DeKN+beoqn6HsMW+Ezici1umj7j8aaNykOhLjqg11XRevJh7ifKQOrpePXM+m13B2VAdm2nPyfO3d7eYFvu7Zw== + dependencies: + "@uifabric/merge-styles" "^7.19.1" + "@uifabric/set-version" "^7.0.23" + "@uifabric/styling" "^7.16.10" + "@uifabric/utilities" "^7.32.4" + tslib "^1.10.0" + +"@uifabric/icons@^7.5.9": + version "7.5.9" + resolved "https://registry.yarnpkg.com/@uifabric/icons/-/icons-7.5.9.tgz#08f5772a0eb7e224fda3434dd3ab29143f21517a" + integrity sha512-kiFw2hm2++OwcVT8ZkHm/UIsyA+4CXBgttmtMaEXMB6/VSt6mfAc+3gs0ehnfXbrBTLUIM9omxXUrrtULSWLTw== + dependencies: + "@uifabric/set-version" "^7.0.23" + "@uifabric/styling" "^7.16.10" + tslib "^1.10.0" + +"@uifabric/merge-styles@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@uifabric/merge-styles/-/merge-styles-7.19.1.tgz#446b3da48ce9925d1649edd0fc232221974b00c9" + integrity sha512-yqUwmk62Kgu216QNPE9vOfS3h0kiSbTvoqM5QcZi+IzpqsBOlzZx3A9Er9UiDaqHRd5lsYF5pO/jeUULmBWF/A== + dependencies: + "@uifabric/set-version" "^7.0.23" + tslib "^1.10.0" + +"@uifabric/react-hooks@^7.13.6": + version "7.13.6" + resolved "https://registry.yarnpkg.com/@uifabric/react-hooks/-/react-hooks-7.13.6.tgz#0626791fe283b235b8ddfedc51f693b50b2792b6" + integrity sha512-DvfphxrTsjo3oYioRZ0D8mXdpxWQhhIHeWk1cfdq0MVGqRyKM+cm++9pJpvItFvnTNba38jzKdggqRSUWi+clg== + dependencies: + "@fluentui/react-window-provider" "^0.3.3" + "@uifabric/set-version" "^7.0.23" + "@uifabric/utilities" "^7.32.4" + tslib "^1.10.0" + +"@uifabric/set-version@^7.0.23": + version "7.0.23" + resolved "https://registry.yarnpkg.com/@uifabric/set-version/-/set-version-7.0.23.tgz#bfe10b6ba17a2518704cca856bdba8adbc11ffb0" + integrity sha512-9E+YKtnH2kyMKnK9XZZsqyM8OCxEJIIfxtaThTlQpYOzrWAGJxQADFbZ7+Usi0U2xHnWNPFROjq+B9ocEzhqMA== + dependencies: + tslib "^1.10.0" + +"@uifabric/styling@^7.16.10": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@uifabric/styling/-/styling-7.16.10.tgz#c9f25c4a269b2a75214928376088dbbd20820e65" + integrity sha512-qhTJME41VM63paw690xp9SxD3NJP7a4YGUQTifLU0q2GM0e7AMQVDPQ+Cd8Fv9YBS1lJKHi069hVRwSfBejlwg== + dependencies: + "@fluentui/theme" "^1.3.0" + "@microsoft/load-themed-styles" "^1.10.26" + "@uifabric/merge-styles" "^7.19.1" + "@uifabric/set-version" "^7.0.23" + "@uifabric/utilities" "^7.32.4" + tslib "^1.10.0" + +"@uifabric/utilities@^7.32.4": + version "7.32.4" + resolved "https://registry.yarnpkg.com/@uifabric/utilities/-/utilities-7.32.4.tgz#6e784c29101742196da69d0b60e63cf193c10c71" + integrity sha512-IKZc03uyfTrgcCGSPAUWfFl9CyxzpSrxrYwAuO4NgDEySdOlkYBRwsRUh6UwWfM+sJdhg7Y3nupzNU3VSC/arg== + dependencies: + "@fluentui/dom-utilities" "^1.1.1" + "@uifabric/merge-styles" "^7.19.1" + "@uifabric/set-version" "^7.0.23" + prop-types "^15.7.2" + tslib "^1.10.0" + "@webassemblyjs/ast@1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" @@ -1212,6 +1357,34 @@ for-in@^1.0.2: resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= +format-message-formats@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/format-message-formats/-/format-message-formats-6.2.0.tgz#231c2a3d2806e3200331c7aa06430cb403f58792" + integrity sha512-QP0dl1O9P3fDCG5klj101nfizgiWiv0T+DMNBqwi25FvB1LIenQQW4PpULk5zO5PiOSvmMu5pW4HS542kJRAww== + +format-message-interpret@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/format-message-interpret/-/format-message-interpret-6.2.3.tgz#ee72fe924102c7d3f0c2d12797f53d8d93795810" + integrity sha512-OoOdB5yHLzW89RwDQW4fj+8p2Eay9Dtmx4B7Tz8C/QQl/j+aVjy65A5xasQhQD+4JumYU/OxMBdjYzBSR8+ivA== + dependencies: + format-message-formats "^6.2.0" + lookup-closest-locale "^6.2.0" + +format-message-parse@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/format-message-parse/-/format-message-parse-6.2.3.tgz#1393d9e2bc598603a36be992379781a78d1b6c08" + integrity sha512-ZSL3nZ0zaDktDAGwkoGJ439rS9ZfTcMOrWAVav9BOAoKs0qE2Ozal2H6vVJOuIdDhwaJuFqvJ7fdRSE+qR5oNg== + +format-message@6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/format-message/-/format-message-6.2.3.tgz#5cb50a664180299c562f5a75083c8476b4157cb4" + integrity sha512-vtPM3hSLXtmPxRLWo4/FX2ylBMOTpV5wGNIq3OYv3ZyK7F+AWkftrAVsJ36gvY8Gw4Yp33xP2Ak+22Pm7sOV/A== + dependencies: + format-message-formats "^6.2.0" + format-message-interpret "^6.2.3" + format-message-parse "^6.2.3" + lookup-closest-locale "^6.2.0" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -1653,6 +1826,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b" + integrity sha512-gWJOWYFrhQ8j7pVm0EM8Slr+EPVq1Phf6lvzvD/WCeqkrx/f2xBI0xOsRRS9xCn3I4vKtP519dvs3TP09r24wQ== + json5@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" @@ -1716,6 +1894,11 @@ lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lookup-closest-locale@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz#57f665e604fd26f77142d48152015402b607bcf3" + integrity sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ== + loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -1890,6 +2073,16 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== +nanoid-dictionary@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/nanoid-dictionary/-/nanoid-dictionary-3.0.0.tgz#e4ad77e528792095662a7be1855ce31448d0687c" + integrity sha512-dYCOXltfavrN7LGYt3DEAGl6Ya3UcnypXPsYR7HZ5k1eIesakVm+zlfv7V75uSe+Zhyxvyhg9yEbPl8qMx1dwA== + +nanoid@^3.1.3: + version "3.1.12" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.12.tgz#6f7736c62e8d39421601e4a0c77623a97ea69654" + integrity sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -1986,6 +2179,25 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" +office-ui-fabric-react@^7.145.0: + version "7.145.0" + resolved "https://registry.yarnpkg.com/office-ui-fabric-react/-/office-ui-fabric-react-7.145.0.tgz#e86a5fa08a2a3528f2a692bec88ea55c76b238b3" + integrity sha512-+eAo/bZpnTXmirwsPZxIbPhjQk+C34f0W/8tAHYWYASrRio9DWWDB0PRsSGUNrn1QubWxjudW294fovkewPeAQ== + dependencies: + "@fluentui/date-time-utilities" "^7.9.0" + "@fluentui/react-focus" "^7.16.10" + "@fluentui/react-window-provider" "^0.3.3" + "@microsoft/load-themed-styles" "^1.10.26" + "@uifabric/foundation" "^7.9.10" + "@uifabric/icons" "^7.5.9" + "@uifabric/merge-styles" "^7.19.1" + "@uifabric/react-hooks" "^7.13.6" + "@uifabric/set-version" "^7.0.23" + "@uifabric/styling" "^7.16.10" + "@uifabric/utilities" "^7.32.4" + prop-types "^15.7.2" + tslib "^1.10.0" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2153,7 +2365,7 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -prop-types@^15.6.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -2770,6 +2982,11 @@ ts-loader@^8.0.0: micromatch "^4.0.0" semver "^6.0.0" +tslib@^1.10.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6" + integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw== + tslib@^1.9.0: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"