diff --git a/Composer/cypress/integration/LuisDeploy.spec.ts b/Composer/cypress/integration/LuisDeploy.spec.ts index ecd8aaf902..7b143e4ea2 100644 --- a/Composer/cypress/integration/LuisDeploy.spec.ts +++ b/Composer/cypress/integration/LuisDeploy.spec.ts @@ -14,6 +14,7 @@ context('Luis Deploy', () => { it('can deploy luis success', () => { cy.visitPage('Project Settings'); + cy.findByText('LUIS and QnA').click(); cy.findAllByTestId('rootLUISAuthoringKey').type('12345678', { delay: 200 }); cy.findAllByTestId('rootLUISRegion').click(); cy.findByText('westus').click(); diff --git a/Composer/packages/client/__tests__/components/header.test.tsx b/Composer/packages/client/__tests__/components/header.test.tsx index c0ad5cb93e..0c67d14844 100644 --- a/Composer/packages/client/__tests__/components/header.test.tsx +++ b/Composer/packages/client/__tests__/components/header.test.tsx @@ -39,6 +39,6 @@ describe('
', () => { return { location: { pathname: 'http://server/bot/1234/settings' } }; }); const result = renderWithRecoil(
); - expect(result.findAllByDisplayValue('Start all bots')).not.toBeNull(); + expect(result.findAllByDisplayValue('Start bot')).not.toBeNull(); }); }); diff --git a/Composer/packages/client/src/pages/botProject/BotProjectInfo.tsx b/Composer/packages/client/src/pages/botProject/BotProjectInfo.tsx new file mode 100644 index 0000000000..91e8b433d9 --- /dev/null +++ b/Composer/packages/client/src/pages/botProject/BotProjectInfo.tsx @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import { useRecoilValue } from 'recoil'; +import React, { useState, Fragment } from 'react'; +import { RouteComponentProps } from '@reach/router'; +import { DisplayMarkdownDialog } from '@bfc/ui-shared'; +import formatMessage from 'format-message'; +import { Stack, StackItem } from 'office-ui-fabric-react/lib/Stack'; +import { Link } from 'office-ui-fabric-react/lib/Link'; + +import { projectReadmeState, locationState } from '../../recoilModel/atoms'; +import { localBotsDataSelector } from '../../recoilModel/selectors/project'; + +const labelStyle = css` + font-size: 12px; + color: #828282; +`; + +const valueStyle = css` + font-size: 14px; +`; + +const headerStyle = css` + font-size: 16px; + font-weight: 600; +`; + +export const BotProjectInfo: React.FC> = (props) => { + const { projectId = '' } = props; + const botProjects = useRecoilValue(localBotsDataSelector); + const botProject = botProjects.find((b) => b.projectId === projectId); + const readme = useRecoilValue(projectReadmeState(projectId)); + const location = useRecoilValue(locationState(projectId)); + const [readmeHidden, setReadmeHidden] = useState(true); + + return ( +
+

{formatMessage('Bot Details')}

+ + +
{formatMessage('Bot Name')}
+
{botProject?.name}
+
+ +
{formatMessage('File Location')}
+
{location}
+
+ +
{formatMessage('Read Me')}
+ {readme && ( + + { + setReadmeHidden(false); + }} + > + {formatMessage('View project readme')} + + + )} +
+
+
+ ); +}; + +export default BotProjectInfo; diff --git a/Composer/packages/client/src/pages/botProject/BotProjectSettings.tsx b/Composer/packages/client/src/pages/botProject/BotProjectSettings.tsx index fadf18e421..51db8431ed 100644 --- a/Composer/packages/client/src/pages/botProject/BotProjectSettings.tsx +++ b/Composer/packages/client/src/pages/botProject/BotProjectSettings.tsx @@ -10,8 +10,6 @@ import { RouteComponentProps } from '@reach/router'; import { JsonEditor } from '@bfc/code-editor'; import { Toggle } from 'office-ui-fabric-react/lib/Toggle'; import { DialogSetting } from '@bfc/shared'; -import { FontSizes, FontWeights } from 'office-ui-fabric-react/lib/Styling'; -import { NeutralColors } from '@uifabric/fluent-theme'; import { defaultToolbarButtonStyles } from '@bfc/ui-shared'; import TelemetryClient from '../../telemetry/TelemetryClient'; @@ -25,7 +23,7 @@ import { createBotSettingUrl, navigateTo } from '../../utils/navigation'; import { mergePropertiesManagedByRootBot } from '../../recoilModel/dispatchers/utils/project'; import { openDeleteBotModal } from './DeleteBotButton'; -import BotProjectSettingsTableView from './BotProjectSettingsTableView'; +import { BotProjectSettingsTabView } from './BotProjectsSettingsTabView'; // -------------------- Styles -------------------- // @@ -45,18 +43,6 @@ const container = css` height: 100%; `; -const botNameStyle = css` - font-size: ${FontSizes.xLarge}; - font-weight: ${FontWeights.semibold}; - color: ${NeutralColors.black}; -`; - -const mainContentHeader = css` - display: flex; - justify-content: space-between; - margin-bottom: 15px; -`; - // -------------------- BotProjectSettings -------------------- // const BotProjectSettings: React.FC> = (props) => { @@ -68,8 +54,6 @@ const BotProjectSettings: React.FC b.projectId === currentProjectId); const { deleteBot } = useRecoilValue(dispatcherState); - const isRootBot = !!botProject?.isRootBot; - const botName = botProject?.name; const settings = useRecoilValue(settingsState(currentProjectId)); const mergedSettings = mergePropertiesManagedByRootBot(currentProjectId, rootBotProjectId, settings); @@ -205,19 +189,6 @@ const BotProjectSettings: React.FC }>
-
-
- {`${botName} (${isRootBot ? formatMessage('Root Bot') : formatMessage('Skill')})`} -
- setAdvancedSettingsEnabled(!isAdvancedSettingsEnabled)} - /> -
{isAdvancedSettingsEnabled ? ( ) : ( - + )} + { + setAdvancedSettingsEnabled(!isAdvancedSettingsEnabled); + }} + />
diff --git a/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx b/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx index 6ce719bb9c..80d0a687e0 100644 --- a/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx +++ b/Composer/packages/client/src/pages/botProject/BotProjectSettingsTableView.tsx @@ -10,6 +10,7 @@ import { RouteComponentProps } from '@reach/router'; import { localBotsDataSelector } from '../../recoilModel/selectors/project'; import { useFeatureFlag } from '../../utils/hooks'; +import { BotProjectInfo } from './BotProjectInfo'; import { SkillHostEndPoint } from './SkillHostEndPoint'; import { AppIdAndPassword } from './AppIdAndPassword'; import { ExternalService } from './ExternalService'; @@ -46,6 +47,7 @@ export const BotProjectSettingsTableView: React.FC + {isRootBot && } diff --git a/Composer/packages/client/src/pages/botProject/BotProjectsSettingsTabView.tsx b/Composer/packages/client/src/pages/botProject/BotProjectsSettingsTabView.tsx new file mode 100644 index 0000000000..451d11cc28 --- /dev/null +++ b/Composer/packages/client/src/pages/botProject/BotProjectsSettingsTabView.tsx @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import { useRecoilValue } from 'recoil'; +import React, { useEffect, useState } from 'react'; +import { RouteComponentProps } from '@reach/router'; +import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/components/Pivot'; +import formatMessage from 'format-message'; + +import { localBotsDataSelector } from '../../recoilModel/selectors/project'; +import { useFeatureFlag } from '../../utils/hooks'; + +import { SkillHostEndPoint } from './SkillHostEndPoint'; +import { BotProjectInfo } from './BotProjectInfo'; +import { AppIdAndPassword } from './AppIdAndPassword'; +import { ExternalService } from './ExternalService'; +import { BotLanguage } from './BotLanguage'; +import { RuntimeSettings } from './RuntimeSettings'; +import { PublishTargets } from './PublishTargets'; +import AdapterSection from './adapters/AdapterSection'; + +// -------------------- Styles -------------------- // + +const container = css` + display: flex; + flex-direction: column; + max-width: 1000px; + height: 100%; +`; + +const publishTargetsWrap = (isLastComponent) => css` + margin-bottom: ${isLastComponent ? '120px' : 0}; +`; + +const idsInTab: Record = { + Basics: ['runtimeSettings'], + LuisQna: [], + Connections: ['connections', 'addNewPublishProfile'], + SkillConfig: [], + Language: [], +}; + +enum PivotItemKey { + Basics = 'Basics', + LuisQna = 'LuisQna', + Connections = 'Connections', + SkillConfig = 'SkillConfig', + Language = 'Language', +} + +// -------------------- BotProjectSettingsTableView -------------------- // + +export const BotProjectSettingsTabView: React.FC> = (props) => { + const { projectId = '', scrollToSectionId = '' } = props; + const botProjects = useRecoilValue(localBotsDataSelector); + const botProject = botProjects.find((b) => b.projectId === projectId); + const isRootBot = !!botProject?.isRootBot; + const useAdapters = useFeatureFlag('NEW_CREATION_FLOW'); + const [selectedKey, setSelectedKey] = useState(PivotItemKey.Basics); + + useEffect(() => { + if (scrollToSectionId) { + const htmlIdTagName = scrollToSectionId.replace('#', ''); + for (const key in PivotItemKey) { + if (idsInTab[key].includes(htmlIdTagName)) { + setSelectedKey(key as PivotItemKey); + } + } + } + }, [scrollToSectionId]); + + return ( +
+ { + item?.props.itemKey && setSelectedKey(item.props.itemKey as PivotItemKey); + }} + > + + + + + + + + + +
+ + {isRootBot && useAdapters && } +
+
+ + {isRootBot && } + + + + +
+
+ ); +}; + +export default BotProjectSettingsTabView; diff --git a/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx b/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx index c65c8d57d4..fe19c9c432 100644 --- a/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx +++ b/Composer/packages/client/src/pages/botProject/RootBotExternalService.tsx @@ -16,6 +16,7 @@ import get from 'lodash/get'; import { css } from '@emotion/core'; import { FontSizes } from 'office-ui-fabric-react/lib/Styling'; import { NeutralColors, SharedColors } from '@uifabric/fluent-theme'; +import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; import { dispatcherState, @@ -30,6 +31,8 @@ import { rootBotProjectIdSelector } from '../../recoilModel/selectors/project'; import { CollapsableWrapper } from '../../components/CollapsableWrapper'; import { mergePropertiesManagedByRootBot } from '../../recoilModel/dispatchers/utils/project'; import { LUIS_REGIONS } from '../../constants'; +import { ManageLuis } from '../../components/ManageLuis/ManageLuis'; +import { ManageQNA } from '../../components/ManageQNA/ManageQNA'; import { title } from './styles'; @@ -184,6 +187,8 @@ export const RootBotExternalService: React.FC = (pr const [localRootQnAKey, setLocalRootQnAKey] = useState(rootqnaKey ?? ''); const [localRootLuisRegion, setLocalRootLuisRegion] = useState(rootLuisRegion ?? ''); const [localRootLuisName, setLocalRootLuisName] = useState(rootLuisName ?? ''); + const [displayManageLuis, setDisplayManageLuis] = useState(false); + const [displayManageQNA, setDisplayManageQNA] = useState(false); const luisKeyFieldRef = useRef(null); const luisEndpointKeyFieldRef = useRef(null); @@ -327,6 +332,20 @@ export const RootBotExternalService: React.FC = (pr } }; + const updateLuisSettings = (newLuisSettings) => { + setSettings(projectId, { + ...mergedSettings, + luis: { ...mergedSettings.luis, ...newLuisSettings }, + }); + }; + + const updateQNASettings = (newQNASettings) => { + setSettings(projectId, { + ...mergedSettings, + qna: { ...mergedSettings.qna, ...newQNASettings }, + }); + }; + return (
@@ -393,6 +412,13 @@ export const RootBotExternalService: React.FC = (pr
)} + { + setDisplayManageLuis(true); + }} + />
= (pr onRenderLabel={onRenderLabel} />
+ { + setDisplayManageQNA(true); + }} + /> +
); diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 2b1c27e37b..0d1418a00c 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -178,6 +178,13 @@ export const locationState = atomFamily({ }, }); +export const projectReadmeState = atomFamily({ + key: getFullyQualifiedKey('readme'), + default: (id) => { + return ''; + }, +}); + export const botEnvironmentState = atomFamily({ key: getFullyQualifiedKey('botEnvironment'), default: (id) => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts index 398f2058bf..fa4174ba6b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/utils/project.ts @@ -53,6 +53,7 @@ import { botProjectIdsState, botProjectSpaceLoadedState, botStatusState, + projectReadmeState, currentProjectIdState, dialogSchemasState, dialogState, @@ -436,7 +437,7 @@ export const initQnaFilesStatus = (projectId: string, qnaFiles: QnAFile[], dialo export const initBotState = async (callbackHelpers: CallbackInterface, data: any, botFiles: any) => { const { set } = callbackHelpers; - const { botName, botEnvironment, location, schemas, settings, id: projectId, diagnostics } = data; + const { botName, botEnvironment, location, readme, schemas, settings, id: projectId, diagnostics } = data; const { dialogs, dialogSchemas, @@ -503,6 +504,7 @@ export const initBotState = async (callbackHelpers: CallbackInterface, data: any set(botDiagnosticsState(projectId), diagnostics); refreshLocalStorage(projectId, settings); set(settingsState(projectId), mergedSettings); + set(projectReadmeState(projectId), readme); set(filePersistenceState(projectId), new FilePersistence(projectId)); set(undoHistoryState(projectId), new UndoHistory(projectId)); diff --git a/Composer/packages/lib/code-editor/src/components/toolbar/__tests__/ToolbarButtonMenu.test.tsx b/Composer/packages/lib/code-editor/src/components/toolbar/__tests__/ToolbarButtonMenu.test.tsx index 725874cd0b..b14382af79 100644 --- a/Composer/packages/lib/code-editor/src/components/toolbar/__tests__/ToolbarButtonMenu.test.tsx +++ b/Composer/packages/lib/code-editor/src/components/toolbar/__tests__/ToolbarButtonMenu.test.tsx @@ -8,7 +8,7 @@ import { act, fireEvent, render, screen } from '@botframework-composer/test-util import React from 'react'; import { ToolbarButtonMenu } from '../ToolbarButtonMenu'; -import { FunctionRefPayload, PropertyRefPayload, TemplateRefPayload } from '../../../lg/types'; +import { FunctionRefPayload, PropertyRefPayload, TemplateRefPayload } from '../../../types'; (global as any).crypto = { getRandomValues: (arr: any[]) => crypto.randomBytes(arr.length), diff --git a/Composer/packages/lib/ui-shared/package.json b/Composer/packages/lib/ui-shared/package.json index ac25cd2fb6..8c7b1f6cad 100644 --- a/Composer/packages/lib/ui-shared/package.json +++ b/Composer/packages/lib/ui-shared/package.json @@ -31,6 +31,7 @@ "dependencies": { "@emotion/core": "^10.0.27", "@emotion/styled": "^10.0.27", + "react-markdown": "^5.0.3", "office-ui-fabric-react": "7.71.0" }, "devDependencies": { diff --git a/Composer/packages/lib/ui-shared/src/components/DisplayMarkdownDialog/DisplayMarkdownDialog.tsx b/Composer/packages/lib/ui-shared/src/components/DisplayMarkdownDialog/DisplayMarkdownDialog.tsx new file mode 100644 index 0000000000..e2edf7d291 --- /dev/null +++ b/Composer/packages/lib/ui-shared/src/components/DisplayMarkdownDialog/DisplayMarkdownDialog.tsx @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; +import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; +import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; +import formatMessage from 'format-message'; +import ReactMarkdown from 'react-markdown'; + +import { DialogTypes, DialogWrapper } from '../DialogWrapper'; + +type DisplayMarkdownDialogProps = { + title: string; + content: string; + hidden: boolean; + onDismiss: () => void; +}; + +export const DisplayMarkdownDialog = (props: DisplayMarkdownDialogProps) => { + return ( + +
+ + + {props.content} + + +
+ + + +
+ ); +}; diff --git a/Composer/packages/lib/ui-shared/src/components/index.ts b/Composer/packages/lib/ui-shared/src/components/index.ts index ac104b4f8c..6d36592590 100644 --- a/Composer/packages/lib/ui-shared/src/components/index.ts +++ b/Composer/packages/lib/ui-shared/src/components/index.ts @@ -8,3 +8,4 @@ export * from './ConfirmDialog'; export * from './PropertyAssignment'; export * from './Toolbar'; export * from './ProvisionHandoff/ProvisionHandoff'; +export * from './DisplayMarkdownDialog/DisplayMarkdownDialog'; diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index cd9e8f95a1..535bbea45c 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -413,6 +413,9 @@ "back_2900f52a": { "message": "Back" }, + "basics_75016d2a": { + "message": "Basics" + }, "been_used_5daccdb2": { "message": "Been used" }, @@ -473,6 +476,9 @@ "bot_controller_319b408d": { "message": "Bot Controller" }, + "bot_details_1995d4fe": { + "message": "Bot Details" + }, "bot_endpoint_not_available_in_the_request_43c381f8": { "message": "Bot endpoint not available in the request" }, @@ -509,6 +515,9 @@ "bot_management_and_configurations_b7dadd69": { "message": "Bot management and configurations" }, + "bot_name_bbd0779d": { + "message": "Bot Name" + }, "bot_name_cannot_not_start_with_a_number_d70239": { "message": "Bot name cannot not start with a number" }, @@ -770,6 +779,9 @@ "connecting_to_b_targetname_b_to_import_bot_content_65d8db95": { "message": "Connecting to { targetName } to import bot content..." }, + "connections_917ef4e4": { + "message": "Connections" + }, "continue_ac067716": { "message": "Continue" }, @@ -1568,6 +1580,9 @@ "fields_must_be_either_all_strings_or_all_fieldset__d3df28c": { "message": "fields must be either all strings or all fieldset objects" }, + "file_location_9258afd3": { + "message": "File Location" + }, "file_name_8fd421ff": { "message": "File name" }, @@ -1685,6 +1700,12 @@ "get_conversation_members_71602275": { "message": "Get conversation members" }, + "get_luis_keys_4cbb975c": { + "message": "Get LUIS keys" + }, + "get_qna_key_583b2548": { + "message": "Get QnA key" + }, "get_started_76ed4cb9": { "message": "Get started" }, @@ -1964,6 +1985,9 @@ "l_startline_startcharacter_l_endline_endcharacter_72bc2e5d": { "message": "L{ startLine }:{ startCharacter } - L{ endLine }:{ endCharacter } " }, + "language_6b3e2c7c": { + "message": "Language" + }, "language_generation_1876f6d6": { "message": "Language Generation" }, @@ -2144,6 +2168,9 @@ "luis_add4bbe3": { "message": "LUIS" }, + "luis_and_qna_7df3ee36": { + "message": "LUIS and QnA" + }, "luis_application_name_1530d3aa": { "message": "LUIS application name" }, @@ -2891,6 +2918,9 @@ "re_prompt_for_input_reprompt_dialog_event_ba028f7": { "message": "Re-prompt for input (Reprompt dialog event)" }, + "read_me_4734ca1": { + "message": "Read Me" + }, "recent_bots_53585911": { "message": "Recent Bots" }, @@ -3035,9 +3065,6 @@ "root_bot_7bb35314": { "message": "Root bot." }, - "root_bot_da9de71c": { - "message": "Root Bot" - }, "root_bot_luis_authoring_key_is_empty_aec2634e": { "message": "Root Bot LUIS authoring key is empty" }, @@ -3281,6 +3308,9 @@ "skill_9b084d2e": { "message": "Skill" }, + "skill_configuration_ed35b038": { + "message": "Skill Configuration" + }, "skill_dialog_name_1bbf0eff": { "message": "Skill Dialog Name" }, @@ -3890,6 +3920,9 @@ "view_on_npm_2051324d": { "message": "View on npm" }, + "view_project_readme_ff079d2e": { + "message": "View project readme" + }, "vishwac_sena_45910bf0": { "message": "Vishwac Sena" }, diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 46b9ddd4ed..c368c49115 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -54,6 +54,7 @@ export class BotProject implements IBotProject { public id: string | undefined; public name: string; public dir: string; + public readme: string; public dataDir: string; public eTag?: string; public fileStorage: IFileStorage; @@ -83,6 +84,7 @@ export class BotProject implements IBotProject { this.settingManager = new DefaultSettingManager(this.dir); this.fileStorage = StorageService.getStorageClient(this.ref.storageId, user); this.builder = new Builder(this.dir, this.fileStorage, defaultLanguage); + this.readme = ''; } public get dialogFiles() { @@ -181,12 +183,14 @@ export class BotProject implements IBotProject { this.diagnostics = []; this.settings = await this.getEnvSettings(false); this.files = await this._getFiles(); + this.readme = await this._getReadme(); }; public getProject = () => { return { botName: this.name, files: Array.from(this.files.values()), + readme: this.readme, location: this.dir, schemas: this.getSchemas(), diagnostics: this.diagnostics, @@ -773,6 +777,19 @@ export class BotProject implements IBotProject { } }; + private _getReadme = async (): Promise => { + const variants = ['readme.md', 'README.md', 'README.MD']; + + for (const v in variants) { + const readmePath = Path.join(this.dir, variants[v]); + if (await this.fileStorage.exists(readmePath)) { + return await this.fileStorage.readFile(readmePath); + } + } + + return ''; + }; + private _getFiles = async () => { if (!(await this.exists())) { throw new Error(`${this.dir} is not a valid path`); diff --git a/extensions/packageManager/src/components/LibraryList.tsx b/extensions/packageManager/src/components/LibraryList.tsx index 6bd2be933f..e53e2aa912 100644 --- a/extensions/packageManager/src/components/LibraryList.tsx +++ b/extensions/packageManager/src/components/LibraryList.tsx @@ -26,13 +26,14 @@ export interface LibraryRef { license?: string; repository?: string; copyright?: string; - icon?: string; + iconUrl?: string; description: string; type?: string; category?: string; language: string; source?: string; isCompatible?: boolean; + readme?: string; } export interface ILibraryListProps { @@ -84,7 +85,7 @@ export const LibraryList: React.FC = (props) => { isResizable: false, data: 'string', onRender: (item: LibraryRef) => { - if (item.icon) return icon; + if (item.iconUrl) return icon; return ; }, }, diff --git a/extensions/packageManager/src/node/index.ts b/extensions/packageManager/src/node/index.ts index 791d9395a3..d92f7559dc 100644 --- a/extensions/packageManager/src/node/index.ts +++ b/extensions/packageManager/src/node/index.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import * as path from 'path'; +import * as fs from 'fs'; import axios from 'axios'; import formatMessage from 'format-message'; @@ -27,6 +28,45 @@ const isAdaptiveComponent = (c) => { return hasSchema(c) || c.includesExports; }; +const readFileAsync = async (path, encoding) => { + return new Promise((resolve, reject) => { + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.readFile(path, { encoding }, (err, data) => { + if (err) { + return reject(err); + } + resolve(data); + }); + }); +}; + +const loadPackageAssets = async (components) => { + const variants = ['readme.md', 'README.md', 'README.MD']; + for (const c in components) { + if (components[c].path) { + const rootFolder = path.dirname(components[c].path); + for (const v in variants) { + const readmePath = path.join(rootFolder, variants[v]); + // eslint-disable-next-line security/detect-non-literal-fs-filename + if (fs.existsSync(readmePath)) { + components[c].readme = await readFileAsync(readmePath, 'utf-8'); + continue; + } + } + + if (components[c].icon) { + const iconPath = path.resolve(rootFolder, components[c].icon); + // eslint-disable-next-line security/detect-non-literal-fs-filename + if (fs.existsSync(iconPath)) { + components[c].iconUrl = 'data:image/png;base64,' + (await readFileAsync(iconPath, 'base64')); + } + } + } + } + + return components; +}; + export default async (composer: IExtensionRegistration): Promise => { const updateRecentlyUsed = (componentList, runtimeLanguage) => { const recentlyUsed = (composer.store.read('recentlyUsed') as any[]) || []; @@ -220,7 +260,7 @@ export default async (composer: IExtensionRegistration): Promise => { if (dryRunMergeResults) { res.json({ projectId, - components: dryRunMergeResults.components.filter(isAdaptiveComponent), + components: await loadPackageAssets(dryRunMergeResults.components.filter(isAdaptiveComponent)), }); } else { res.status(500).json({ @@ -308,7 +348,7 @@ export default async (composer: IExtensionRegistration): Promise => { ); const mergeResults = await realMerge.merge(); - const installedComponents = mergeResults.components.filter(isAdaptiveComponent); + const installedComponents = await loadPackageAssets(mergeResults.components.filter(isAdaptiveComponent)); if (mergeResults) { res.json({ success: true, @@ -410,7 +450,7 @@ export default async (composer: IExtensionRegistration): Promise => { res.json({ success: true, - components: mergeResults.components.filter(isAdaptiveComponent), + components: await loadPackageAssets(mergeResults.components.filter(isAdaptiveComponent)), }); // update the settings.components array diff --git a/extensions/packageManager/src/pages/Library.tsx b/extensions/packageManager/src/pages/Library.tsx index 2af856372b..8a46c587cd 100644 --- a/extensions/packageManager/src/pages/Library.tsx +++ b/extensions/packageManager/src/pages/Library.tsx @@ -32,7 +32,7 @@ import { useTelemetryClient, TelemetryClient, } from '@bfc/extension-client'; -import { Toolbar, IToolbarItem, LoadingSpinner } from '@bfc/ui-shared'; +import { Toolbar, IToolbarItem, LoadingSpinner, DisplayMarkdownDialog } from '@bfc/ui-shared'; import ReactMarkdown from 'react-markdown'; import { ContentHeaderStyle, HeaderText } from '../components/styles'; @@ -78,6 +78,7 @@ const Library: React.FC = () => { const [readmeContent, setReadmeContent] = useState(''); const [versionOptions, setVersionOptions] = useState(undefined); const [isUpdate, setIsUpdate] = useState(false); + const [readmeHidden, setReadmeHidden] = useState(true); const httpClient = useHttpClient(); const API_ROOT = ''; const TABS = { @@ -399,6 +400,14 @@ const Library: React.FC = () => { setWorking(''); updateInstalledComponents(results.data.components); + // find newly installed item + // and pop up the readme if one exists. + const newItem = results.data.components.find((i) => i.name === packageName); + if (newItem?.readme) { + setSelectedItem(newItem); + setReadmeHidden(false); + } + // reload modified content await reloadProject(); } @@ -571,6 +580,16 @@ const Library: React.FC = () => { hidden={!isModalVisible} onUpdateFeed={updateFeed} /> + {selectedItem && ( +