diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx index 6a17eb65ad..2f79907ee4 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectHeader.tsx @@ -113,7 +113,7 @@ export const ProjectHeader = (props: ProjectHeaderProps) => { onClick: () => {}, }, { - label: formatMessage('Create/edit skill manifest'), + label: formatMessage('Share as a skill'), onClick: () => { onBotEditManifest(projectId); }, diff --git a/Composer/packages/client/src/pages/botProject/CreatePublishProfileDialog.tsx b/Composer/packages/client/src/pages/botProject/CreatePublishProfileDialog.tsx new file mode 100644 index 0000000000..402d8e3918 --- /dev/null +++ b/Composer/packages/client/src/pages/botProject/CreatePublishProfileDialog.tsx @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import React, { Fragment, useState, useEffect } from 'react'; +import { jsx } from '@emotion/core'; +import { useRecoilValue } from 'recoil'; +import { PublishTarget } from '@bfc/shared'; +import formatMessage from 'format-message'; +import { ActionButton, DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { useBoolean } from '@uifabric/react-hooks'; +import Dialog, { DialogFooter } from 'office-ui-fabric-react/lib/Dialog'; + +import { dispatcherState, settingsState, publishTypesState } from '../../recoilModel'; +import { AuthDialog } from '../../components/Auth/AuthDialog'; +import { isShowAuthDialog } from '../../utils/auth'; + +import { PublishProfileDialog } from './create-publish-profile/PublishProfileDialog'; +import { actionButton } from './styles'; + +// -------------------- CreatePublishProfileDialog -------------------- // + +type CreatePublishProfileDialogProps = { + projectId: string; +}; + +export const CreatePublishProfileDialog: React.FC = (props) => { + const { projectId } = props; + const { publishTargets } = useRecoilValue(settingsState(projectId)); + const { getPublishTargetTypes, setPublishTargets } = useRecoilValue(dispatcherState); + const publishTypes = useRecoilValue(publishTypesState(projectId)); + + const [dialogHidden, setDialogHidden] = useState(true); + const [showAuthDialog, setShowAuthDialog] = useState(false); + const [hideDialog, { toggle: toggleHideDialog }] = useBoolean(false); + + const dialogTitle = { + title: formatMessage('Create a publish profile to continue'), + subText: formatMessage( + 'To make your bot available as a remote skill you will need to provision Azure resources . This process may take a few minutes depending on the resources you select.' + ), + }; + const [currentPublishProfile, setCurrentPublishProfile] = useState<{ index: number; item: PublishTarget } | null>( + null + ); + + useEffect(() => { + if (projectId) { + getPublishTargetTypes(projectId); + } + }, [projectId]); + + return ( + + + {showAuthDialog && ( + { + setDialogHidden(false); + }} + onDismiss={() => { + setShowAuthDialog(false); + }} + /> + )} + {!dialogHidden ? ( + { + setDialogHidden(true); + setCurrentPublishProfile(null); + }} + current={currentPublishProfile} + projectId={projectId} + setPublishTargets={setPublishTargets} + targets={publishTargets || []} + types={publishTypes} + /> + ) : null} + + ); +}; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/__tests__/generateSkillManifest.test.ts b/Composer/packages/client/src/pages/design/exportSkillModal/__tests__/generateSkillManifest.test.ts index 47a857c5e7..0106f803e0 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/__tests__/generateSkillManifest.test.ts +++ b/Composer/packages/client/src/pages/design/exportSkillModal/__tests__/generateSkillManifest.test.ts @@ -11,6 +11,14 @@ import { generateDispatchModels, } from '../generateSkillManifest'; +const projectId = '42345.23432'; +const currentTarget = { + configuration: + '{\n "name": "test",\n "environment": "composer",\n "tenantId": "aaa",\n "subscriptionId": "aaa",\n "resourceGroup": "testGroup",\n "botName": "test",\n "hostname": "test",\n "luisResource": "test",\n "runtimeIdentifier": "win-x64",\n "region": "westus",\n "settings": {\n "applicationInsights": {},\n "luis": {"authoringKey":"aaa", "endpointKey": "aaa",\n "endpoint": "https://westus.api.cognitive.microsoft.com/"\n },\n "qna": {},\n "MicrosoftAppId": "aaa",\n "MicrosoftAppPassword": "aaa"\n }\n}', + name: 'test', + type: 'azurePublish', + lastPublished: new Date('2021-04-08T08:38:17.566Z'), +}; const dialogSchema = { id: 'test', content: { @@ -188,7 +196,15 @@ describe('generateDispatchModels', () => { const selectedTriggers = []; const luFiles = []; const qnaFiles = []; - const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles); + const result = generateDispatchModels( + schema, + dialogs, + selectedTriggers, + luFiles, + qnaFiles, + currentTarget, + projectId + ); expect(result).toEqual({}); }); @@ -201,7 +217,15 @@ describe('generateDispatchModels', () => { { id: 'test.fr-FR', empty: false }, ]; const qnaFiles = []; - const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles); + const result = generateDispatchModels( + schema, + dialogs, + selectedTriggers, + luFiles, + qnaFiles, + currentTarget, + projectId + ); expect(result).toEqual({}); }); @@ -214,7 +238,15 @@ describe('generateDispatchModels', () => { { id: 'test.fr-FR', empty: false }, ]; const qnaFiles = []; - const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles); + const result = generateDispatchModels( + schema, + dialogs, + selectedTriggers, + luFiles, + qnaFiles, + currentTarget, + projectId + ); expect(result).toEqual({}); }); @@ -227,37 +259,45 @@ describe('generateDispatchModels', () => { { id: 'test.fr-FR', empty: false }, ]; const qnaFiles: any = [{ id: 'test.es-es', empty: false }]; - const result = generateDispatchModels(schema, dialogs, selectedTriggers, luFiles, qnaFiles); + const result = generateDispatchModels( + schema, + dialogs, + selectedTriggers, + luFiles, + qnaFiles, + currentTarget, + projectId + ); expect(result).toEqual( expect.objectContaining({ dispatchModels: { + intents: ['testIntent'], languages: { 'en-us': [ { - name: 'test', contentType: 'application/lu', - url: ``, description: '', + name: 'test', + url: 'https://test.azurewebsites.net/manifests/skill-test.en-us.lu', }, ], - 'fr-FR': [ + 'es-es': [ { - name: 'test', - contentType: 'application/lu', - url: ``, + contentType: 'application/qna', description: '', + name: 'test', + url: 'https://test.azurewebsites.net/manifests/skill-test.es-es.qna', }, ], - 'es-es': [ + 'fr-FR': [ { - name: 'test', - contentType: 'application/qna', - url: ``, + contentType: 'application/lu', description: '', + name: 'test', + url: 'https://test.azurewebsites.net/manifests/skill-test.fr-FR.lu', }, ], }, - intents: ['testIntent'], }, }) ); diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx index 611914dcbc..18f7c5c883 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx @@ -3,23 +3,15 @@ import formatMessage from 'format-message'; import { JSONSchema7 } from '@bfc/extension-client'; -import { resolveRef } from '@bfc/adaptive-form'; import { SkillManifestFile } from '@bfc/shared'; import startCase from 'lodash/startCase'; import { SDKKinds } from '@bfc/shared'; import { nameRegex } from '../../../constants'; -import { - Description, - Endpoints, - FetchManifestSchema, - ReviewManifest, - SaveManifest, - SelectDialogs, - SelectManifest, - SelectTriggers, -} from './content'; +import { Description, ReviewManifest, SaveManifest, SelectDialogs, SelectTriggers } from './content'; +import { SelectProfile } from './content/SelectProfile'; +import { AddCallers } from './content/AddCallers'; export const VERSION_REGEX = /\d\.\d+\.(\d+|preview-\d+)|\d\.\d+/i; @@ -95,10 +87,14 @@ export interface ContentProps { setSelectedTriggers: (selectedTriggers: any[]) => void; setSkillManifest: (_: Partial) => void; schema: JSONSchema7; + selectedDialogs: any[]; + selectedTriggers: any[]; skillManifests: SkillManifestFile[]; value: { [key: string]: any }; onChange: (_: any) => void; projectId: string; + callers: string[]; + onUpdateCallers: (callers: string[]) => void; } interface Button { @@ -127,25 +123,21 @@ interface EditorStep { } export enum ManifestEditorSteps { - ENDPOINTS = 'ENDPOINTS', - FETCH_MANIFEST_SCHEMA = 'FETCH_MANIFEST_SCHEMA', MANIFEST_DESCRIPTION = 'MANIFEST_DESCRIPTION', MANIFEST_REVIEW = 'MANIFEST_REVIEW', SAVE_MANIFEST = 'SAVE_MANIFEST', - SELECT_MANIFEST = 'SELECT_MANIFEST', SELECT_DIALOGS = 'SELECT_DIALOGS', SELECT_TRIGGERS = 'SELECT_TRIGGERS', + SELECT_PROFILE = 'SELECT_PROFILE', + ADD_CALLERS = 'ADD_CALLERS', } export const order: ManifestEditorSteps[] = [ - ManifestEditorSteps.SELECT_MANIFEST, - ManifestEditorSteps.FETCH_MANIFEST_SCHEMA, ManifestEditorSteps.MANIFEST_DESCRIPTION, - ManifestEditorSteps.ENDPOINTS, ManifestEditorSteps.SELECT_DIALOGS, ManifestEditorSteps.SELECT_TRIGGERS, - ManifestEditorSteps.MANIFEST_REVIEW, - ManifestEditorSteps.SAVE_MANIFEST, + ManifestEditorSteps.ADD_CALLERS, + ManifestEditorSteps.SELECT_PROFILE, ]; const cancelButton: Button = { @@ -159,6 +151,12 @@ const nextButton: Button = { onClick: ({ onNext }) => onNext, }; +const backButton: Button = { + primary: true, + text: () => formatMessage('Back'), + onClick: ({ onBack }) => onBack, +}; + const validate = ({ content, schema }) => { const required = schema?.required || []; @@ -177,61 +175,79 @@ const validate = ({ content, schema }) => { }; export const editorSteps: { [key in ManifestEditorSteps]: EditorStep } = { - [ManifestEditorSteps.SELECT_MANIFEST]: { + [ManifestEditorSteps.MANIFEST_DESCRIPTION]: { + buttons: [cancelButton, nextButton], + content: Description, + editJson: false, + title: () => formatMessage('Describe your skill'), + subText: () => formatMessage('To make your bot available for others as a skill, we need to generate a manifest.'), + validate, + }, + [ManifestEditorSteps.SELECT_PROFILE]: { buttons: [ cancelButton, + backButton, { - disabled: ({ manifest }) => !manifest?.id, + disabled: ({ publishTargets }) => { + try { + return ( + publishTargets.findIndex((item) => { + const config = JSON.parse(item.configuration); + return ( + config.settings && + config.settings.MicrosoftAppId && + config.hostname && + config.settings.MicrosoftAppId.length > 0 && + config.hostname.length > 0 + ); + }) < 0 + ); + } catch (err) { + console.log(err.message); + return true; + } + }, + primary: true, - text: () => formatMessage('Edit'), - onClick: ({ onNext, manifest }) => () => { - onNext({ id: manifest?.id }); + text: () => formatMessage('Generate and Publish'), + onClick: ({ generateManifest, onNext, onPublish }) => () => { + generateManifest(); + onNext({ dismiss: true, save: true }); + onPublish(); }, }, ], - content: SelectManifest, editJson: false, - subText: () => formatMessage('Create a new skill manifest or select which one you want to edit'), - title: () => formatMessage('Create or edit skill manifest'), + content: SelectProfile, + subText: () => + formatMessage('We need to define the endpoints for the skill to allow other bots to interact with it.'), + title: () => formatMessage('Confirm skill endpoints'), }, - [ManifestEditorSteps.FETCH_MANIFEST_SCHEMA]: { - content: FetchManifestSchema, + [ManifestEditorSteps.ADD_CALLERS]: { + buttons: [ + cancelButton, + backButton, + { + primary: true, + text: () => formatMessage('Next'), + onClick: ({ onNext, onSaveSkill }) => () => { + onSaveSkill(); + onNext(); + }, + }, + ], editJson: false, - title: () => formatMessage('Select manifest version'), - }, - [ManifestEditorSteps.MANIFEST_DESCRIPTION]: { - buttons: [cancelButton, nextButton], - content: Description, - editJson: true, - title: () => formatMessage('Describe your skill'), - subText: () => formatMessage('To make your bot available for others as a skill, we need to generate a manifest.'), - validate, - }, - [ManifestEditorSteps.ENDPOINTS]: { - buttons: [cancelButton, nextButton], - content: Endpoints, - editJson: true, + content: AddCallers, subText: () => - formatMessage('We need to define the endpoints for the skill to allow other bots to interact with it.'), - title: () => formatMessage('Skill endpoints'), - validate: ({ content, schema }) => { - const { items, minItems } = schema.properties?.endpoints; - - if (!content.endpoints || content.endpoints.length < minItems) { - return { endpoints: formatMessage('Please add at least {minItems} endpoint', { minItems }) }; - } - - const endpointSchema = resolveRef(items, schema.definitions); - const endpoints = (content.endpoints || []).map((endpoint) => - validate({ content: endpoint, schema: endpointSchema }) - ); - - return endpoints.some((endpoint) => Object.keys(endpoint).length) ? { endpoints } : {}; - }, + formatMessage( + 'Add Microsoft App Ids of bots that can access this skill. You can skip this step and add this information later from the project settings tab.' + ), + title: () => formatMessage('Which bots are allowed to use this skill?'), }, [ManifestEditorSteps.MANIFEST_REVIEW]: { buttons: [ cancelButton, + backButton, { primary: true, text: () => formatMessage('Next'), @@ -245,6 +261,7 @@ export const editorSteps: { [key in ManifestEditorSteps]: EditorStep } = { [ManifestEditorSteps.SELECT_DIALOGS]: { buttons: [ cancelButton, + backButton, { primary: true, text: () => formatMessage('Next'), @@ -252,7 +269,7 @@ export const editorSteps: { [key in ManifestEditorSteps]: EditorStep } = { }, ], content: SelectDialogs, - editJson: true, + editJson: false, subText: () => formatMessage( 'These tasks will be used to generate the manifest and describe the capabilities of this skill to those who may want to use it.' @@ -262,17 +279,18 @@ export const editorSteps: { [key in ManifestEditorSteps]: EditorStep } = { [ManifestEditorSteps.SELECT_TRIGGERS]: { buttons: [ cancelButton, + backButton, { primary: true, - text: () => formatMessage('Generate'), - onClick: ({ generateManifest, onNext }) => () => { - generateManifest(); + text: () => formatMessage('Next'), + onClick: ({ onNext, generateManifest }) => () => { + // generateManifest(); onNext(); }, }, ], content: SelectTriggers, - editJson: true, + editJson: false, subText: () => formatMessage( 'These tasks will be used to generate the manifest and describe the capabilities of this skill to those who may want to use it.' @@ -282,6 +300,7 @@ export const editorSteps: { [key in ManifestEditorSteps]: EditorStep } = { [ManifestEditorSteps.SAVE_MANIFEST]: { buttons: [ cancelButton, + backButton, { primary: true, text: () => formatMessage('Save'), @@ -291,7 +310,7 @@ export const editorSteps: { [key in ManifestEditorSteps]: EditorStep } = { }, ], content: SaveManifest, - editJson: true, + editJson: false, subText: () => formatMessage('Name and save your skill manifest.'), title: () => formatMessage('Save your skill manifest'), validate: ({ editingId, id, skillManifests }) => { diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/AddCallers.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/AddCallers.tsx new file mode 100644 index 0000000000..4951b2ebae --- /dev/null +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/AddCallers.tsx @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import { SharedColors } from '@uifabric/fluent-theme'; +import formatMessage from 'format-message'; +import { ActionButton } from 'office-ui-fabric-react/lib/Button'; +import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import React from 'react'; + +import { tableColumnHeader, tableRow, tableRowItem } from '../../../botProject/styles'; +import { ContentProps } from '../constants'; + +const header = css` + display: flex; + flex-direction: row; + height: 42px; +`; + +const addNewAllowCallers = { + root: { + fontSize: 12, + fontWeight: FontWeights.regular, + color: SharedColors.cyanBlue10, + paddingLeft: 0, + marginLeft: 5, + }, +}; + +const removeCaller = { + root: { + fontSize: 12, + fontWeight: FontWeights.regular, + color: SharedColors.cyanBlue10, + paddingLeft: 0, + paddingBottom: 5, + }, +}; + +export const AddCallers: React.FC = ({ projectId, callers, onUpdateCallers }) => { + const handleRemove = (index) => { + onUpdateCallers(callers.filter((_, i) => i !== index)); + }; + const handleAddNewAllowedCallerClick = () => { + const currentCallers = callers.slice(); + currentCallers?.push('0000-11111-00000-11111'); + onUpdateCallers(currentCallers); + }; + + return ( +
+
+
{formatMessage('Allowed Callers')}
+
+ {callers?.map((caller, index) => { + return ( +
+
+ { + const currentCallers = callers.slice(); + currentCallers[index] = newValue ?? ''; + onUpdateCallers(currentCallers); + }} + /> +
+
+ handleRemove(index)}> + {formatMessage('Remove')} + +
+
+ ); + })} + + {formatMessage('Add allowed callers')} + +
+ ); +}; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx index 5ff2dc68d4..ac6092a281 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/Description.tsx @@ -3,15 +3,19 @@ /** @jsx jsx */ import { css, jsx } from '@emotion/core'; -import React, { useMemo, useEffect } from 'react'; +import React, { useMemo, useEffect, Fragment, useState } from 'react'; import AdaptiveForm, { FieldLabel } from '@bfc/adaptive-form'; import { FieldProps, JSONSchema7, UIOptions } from '@bfc/extension-client'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { useRecoilValue } from 'recoil'; import { v4 as uuid } from 'uuid'; +import { Label } from 'office-ui-fabric-react/lib/Label'; +import formatMessage from 'format-message'; +import { Dropdown, IDropdownOption, ResponsiveMode } from 'office-ui-fabric-react/lib/Dropdown'; +import { LoadingSpinner } from '@bfc/ui-shared/lib/components/LoadingSpinner'; -import { ContentProps } from '../constants'; import { botDisplayNameState } from '../../../../recoilModel'; +import { ContentProps, SCHEMA_URIS, VERSION_REGEX } from '../constants'; const styles = { row: css` @@ -25,6 +29,12 @@ const styles = { `, }; +const chooseVersion = css` + display: flex; + width: 72%; + margin: 10px 18px; + justify-content: space-between; +`; const InlineLabelField: React.FC = (props) => { const { id, placeholder, rawErrors, value = '', onChange } = props; @@ -50,42 +60,94 @@ const InlineLabelField: React.FC = (props) => { ); }; -export const Description: React.FC = ({ errors, value, schema, onChange, projectId }) => { +export const Description: React.FC = ({ + errors, + value, + schema, + skillManifests, + onChange, + projectId, + setSchema, + setSkillManifest, + editJson, +}) => { const botName = useRecoilValue(botDisplayNameState(projectId)); - const { $schema, ...rest } = value; + const [isFetchCompleted, setIsFetchCompleted] = useState(false); + const { $id, $schema, ...rest } = value; - const { hidden, properties } = useMemo( + const { hidden, properties } = useMemo(() => { + if (!schema.properties) return { hidden: [], properties: {} } as any; + return Object.entries(schema.properties as JSONSchema7).reduce( + ({ hidden, properties }, [key, property]) => { + if (property.type === 'object' || (property.type === 'array' && property?.items?.type !== 'string')) { + return { hidden: [...hidden, key], properties }; + } + + const itemSchema = property?.items as JSONSchema7; + const serializer = + itemSchema?.type === 'string' + ? { + get: (value) => (Array.isArray(value) ? value.join(',') : value), + set: (value) => (typeof value === 'string' ? value.split(/\s*,\s*/) : value), + } + : null; + + return { + hidden, + properties: { ...properties, [key]: { field: InlineLabelField, hideError: true, serializer } }, + }; + }, + { hidden: [], properties: {} } as any + ); + }, [schema]); + + const options: IDropdownOption[] = useMemo( () => - Object.entries(schema?.properties as JSONSchema7).reduce( - ({ hidden, properties }, [key, property]) => { - if (property.type === 'object' || (property.type === 'array' && property?.items?.type !== 'string')) { - return { hidden: [...hidden, key], properties }; - } - - const itemSchema = property?.items as JSONSchema7; - const serializer = - itemSchema?.type === 'string' - ? { - get: (value) => (Array.isArray(value) ? value.join(',') : value), - set: (value) => (typeof value === 'string' ? value.split(/\s*,\s*/) : value), - } - : null; - - return { - hidden, - properties: { ...properties, [key]: { field: InlineLabelField, hideError: true, serializer } }, - }; - }, - { hidden: [], properties: {} } as any - ), - [] + SCHEMA_URIS.map((key, index) => { + const [version] = VERSION_REGEX.exec(key) || []; + let selected = false; + if ($schema) { + selected = $schema && key === $schema; + } else { + selected = !index; + } + return { + text: formatMessage('Version {version}', { version }), + key, + selected, + }; + }), + [$schema] ); useEffect(() => { - if (!value.$id) { - onChange({ $schema, $id: `${botName}-${uuid()}`, endpoints: [{}], name: botName, ...rest }); - } - }, []); + const skillManifest = skillManifests.find( + (manifest) => manifest.content.$schema === ($schema || SCHEMA_URIS[0]) + ) || { + content: { + $schema: $schema || SCHEMA_URIS[0], + $id: `${botName}-${uuid()}`, + endpoints: [{}], + name: botName, + ...rest, + }, + }; + setSkillManifest(skillManifest); + (async function () { + try { + if ($schema) { + const res = await fetch($schema); + const schema = await res.json(); + setSchema(schema); + setIsFetchCompleted(true); + } else { + editJson(); + } + } catch (error) { + editJson(); + } + })(); + }, [$schema]); const required = schema?.required || []; @@ -96,5 +158,45 @@ export const Description: React.FC = ({ errors, value, schema, onC properties, }; - return ; + const handleChange = (_e: React.FormEvent, option?: IDropdownOption) => { + if (option) { + const skillManifest = skillManifests.find((manifest) => manifest.content.$schema === option.key) || { + content: { $schema: option.key as string }, + }; + setIsFetchCompleted(false); + setSkillManifest(skillManifest); + } + }; + + return ( + +
+ + +
+ {isFetchCompleted ? ( + + ) : ( + + )} +
+ ); }; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/FetchManifestSchema.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/FetchManifestSchema.tsx deleted file mode 100644 index ff2d1fc9d6..0000000000 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/FetchManifestSchema.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React, { useEffect } from 'react'; - -import { LoadingSpinner } from '../../../../components/LoadingSpinner'; -import { ContentProps } from '../constants'; - -export const FetchManifestSchema: React.FC = ({ completeStep, editJson, value, setSchema }) => { - useEffect(() => { - (async function () { - try { - if (value?.$schema) { - const res = await fetch(value.$schema); - const schema = await res.json(); - setSchema(schema); - completeStep(); - } else { - editJson(); - } - } catch (error) { - editJson(); - } - })(); - }, [value]); - - return ; -}; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx index ee90f5fd68..882ed6b526 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectDialogs.tsx @@ -93,7 +93,7 @@ const DescriptionColumn: React.FC = (props) => { ); }; -export const SelectDialogs: React.FC = ({ setSelectedDialogs, projectId }) => { +export const SelectDialogs: React.FC = ({ selectedDialogs, setSelectedDialogs, projectId }) => { const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const items = useMemo(() => dialogs.map(({ id, content, displayName }) => ({ id, content, displayName })), [ projectId, @@ -138,16 +138,20 @@ export const SelectDialogs: React.FC = ({ setSelectedDialogs, proj [projectId] ); - const selection = useMemo( - () => - new Selection({ - onSelectionChanged: () => { - const selectedItems = selection.getSelection(); - setSelectedDialogs(selectedItems); - }, - }), - [] + const selectionRef = useRef( + new Selection({ + getKey: (item) => item.id, + onSelectionChanged: () => { + const selectedItems = selectionRef.current.getSelection(); + setSelectedDialogs(selectedItems); + }, + }) ); - return ; + useEffect(() => { + for (const item of selectedDialogs) { + selectionRef.current.setKeySelected(selectionRef.current.getKey(item), true, false); + } + }, []); + return ; }; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectItems.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectItems.tsx index 1e69515a6c..6584e61922 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectItems.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectItems.tsx @@ -77,7 +77,7 @@ export const SelectItems: React.FC = ({ items, selection, tabl diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectManifest.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectManifest.tsx deleted file mode 100644 index 3e4faca419..0000000000 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectManifest.tsx +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { css, jsx } from '@emotion/core'; -import React, { useMemo, useState } from 'react'; -import { Dropdown, IDropdownOption, ResponsiveMode } from 'office-ui-fabric-react/lib/Dropdown'; -import { Label } from 'office-ui-fabric-react/lib/Label'; -import { PrimaryButton } from 'office-ui-fabric-react/lib/Button'; -import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; -import { ScrollablePane, ScrollbarVisibility } from 'office-ui-fabric-react/lib/ScrollablePane'; -import { - CheckboxVisibility, - DetailsList, - DetailsListLayoutMode, - SelectionMode, -} from 'office-ui-fabric-react/lib/DetailsList'; -import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; -import formatMessage from 'format-message'; - -import { calculateTimeDiff } from '../../../../utils/fileUtil'; -import { ContentProps, SCHEMA_URIS, VERSION_REGEX } from '../constants'; - -const styles = { - detailListContainer: css` - position: relative; - max-height: 40vh; - padding-top: 10px; - overflow: hidden; - flex-grow: 1; - min-height: 250px; - `, - create: css` - display: flex; - `, -}; - -export const SelectManifest: React.FC = ({ completeStep, skillManifests, setSkillManifest }) => { - const [manifestVersion, setManifestVersion] = useState(SCHEMA_URIS[0]); - const [errors, setErrors] = useState<{ version?: string }>({}); - const [version] = useMemo(() => VERSION_REGEX.exec(manifestVersion) || [''], []); - - const options: IDropdownOption[] = useMemo( - () => - SCHEMA_URIS.map((key, index) => { - const [version] = VERSION_REGEX.exec(key) || []; - - return { - text: formatMessage('Version {version}', { version }), - key, - selected: !index, - }; - }), - [] - ); - - const handleChange = (_e: React.FormEvent, option?: IDropdownOption) => { - if (option) { - setManifestVersion(option.key as string); - } - }; - - const handleCreate = () => { - if (!version) { - setErrors({ version: formatMessage('Please select a version of the manifest schema') }); - return; - } - setSkillManifest({ content: { $schema: manifestVersion } }); - completeStep(); - }; - - // for detail file list in open panel - const tableColumns = [ - { - key: 'column1', - name: formatMessage('Name'), - fieldName: 'id', - minWidth: 300, - maxWidth: 350, - isRowHeader: true, - isResizable: true, - isSorted: true, - isSortedDescending: false, - sortAscendingAriaLabel: formatMessage('Sorted A to Z'), - sortDescendingAriaLabel: formatMessage('Sorted Z to A'), - data: 'string', - onRender: (item) => { - return {item.id}; - }, - isPadded: true, - }, - { - key: 'column2', - name: formatMessage('Date modified'), - fieldName: 'lastModified', - minWidth: 60, - maxWidth: 70, - isResizable: true, - data: 'number', - onRender: (item) => { - return {calculateTimeDiff(item.lastModified)}; - }, - isPadded: true, - }, - ]; - - function onRenderDetailsHeader(props, defaultRender) { - return ( - - {defaultRender({ - ...props, - onRenderColumnHeaderTooltip: (tooltipHostProps) => , - })} - - ); - } - - return ( -
-
- -
- - - {formatMessage('Create')} - -
-
-
- - item.name} - items={skillManifests} - layoutMode={DetailsListLayoutMode.justified} - selectionMode={SelectionMode.single} - onActiveItemChanged={setSkillManifest} - onRenderDetailsHeader={onRenderDetailsHeader} - /> - -
-
- ); -}; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectProfile.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectProfile.tsx new file mode 100644 index 0000000000..51bf07dc99 --- /dev/null +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectProfile.tsx @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import { PublishTarget, SkillManifestFile } from '@bfc/shared'; +import formatMessage from 'format-message'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; + +import { botDisplayNameState, dispatcherState, settingsState, skillManifestsState } from '../../../../recoilModel'; +import { CreatePublishProfileDialog } from '../../../botProject/CreatePublishProfileDialog'; +import { iconStyle } from '../../../botProject/runtime-settings/style'; +import { ContentProps, VERSION_REGEX } from '../constants'; + +const styles = { + container: css` + height: 350px; + overflow: auto; + `, +}; + +const onRenderLabel = (props) => { + return ( +
+
+
+ {props.label} +
+
+ + + +
+ ); +}; + +export const getManifestId = ( + botName: string, + skillManifests: SkillManifestFile[], + { content: { $schema } = {} }: Partial +): string => { + const [version] = VERSION_REGEX.exec($schema) || ['']; + + let fileId = version ? `${botName}-${version.replace(/\./g, '-')}-manifest` : `${botName}-manifest`; + let i = -1; + + while (skillManifests.some(({ id }) => id === fileId) && i < skillManifests.length) { + if (i < 0) { + fileId = fileId.concat(`-${++i}`); + } else { + fileId = fileId.substr(0, fileId.lastIndexOf('-')).concat(`-${++i}`); + } + } + + return fileId; +}; + +export const SelectProfile: React.FC = ({ manifest, setSkillManifest, value, onChange, projectId }) => { + const [publishingTargets, setPublishingTargets] = useState([]); + const [currentTarget, setCurrentTarget] = useState(); + const { updateCurrentTarget } = useRecoilValue(dispatcherState); + const settings = useRecoilValue(settingsState(projectId)); + const [endpointUrl, setEndpointUrl] = useState(); + const [appId, setAppId] = useState(); + const { id, content } = manifest; + const botName = useRecoilValue(botDisplayNameState(projectId)); + const skillManifests = useRecoilValue(skillManifestsState(projectId)); + + const handleCurrentProfileChange = useMemo( + () => (_e, option?: IDropdownOption) => { + const target = publishingTargets.find((t) => { + return t.name === option?.key; + }); + setCurrentTarget(target); + }, + [publishingTargets] + ); + + useEffect(() => { + try { + if (currentTarget) { + updateCurrentTarget(projectId, currentTarget); + const config = JSON.parse(currentTarget.configuration); + setEndpointUrl(`https://${config.hostname}.azurewebsites.net/api/messages`); + setAppId(config.settings.MicrosoftAppId); + + setSkillManifest({ + content: { + ...content, + endpoints: [ + { + protocol: 'BotFrameworkV3', + name: currentTarget.name, + endpointUrl: `https://${config.hostname}.azurewebsites.net/api/messages`, + description: '', + msAppId: config.settings.MicrosoftAppId, + }, + ], + }, + id: id, + }); + } + } catch (err) { + console.log(err.message); + } + }, [currentTarget]); + const isProfileValid = useMemo(() => { + try { + if (!publishingTargets) { + return false; + } + const filteredProfile = publishingTargets.filter((item) => { + const config = JSON.parse(item.configuration); + return ( + config.settings && + config.settings.MicrosoftAppId && + config.hostname && + config.settings.MicrosoftAppId.length > 0 && + config.hostname.length > 0 + ); + }); + return filteredProfile.length > 0; + } catch (err) { + console.log(err.message); + return false; + } + }, [publishingTargets]); + + const publishingOptions = useMemo(() => { + return publishingTargets.map((t) => ({ + key: t.name, + text: t.name, + })); + }, [publishingTargets]); + + useEffect(() => { + setPublishingTargets(settings.publishTargets || []); + setCurrentTarget((settings.publishTargets || [])[0]); + }, [settings]); + + useEffect(() => { + if (!id) { + const fileId = getManifestId(botName, skillManifests, manifest); + setSkillManifest({ ...manifest, id: fileId }); + } + }, [id]); + + return isProfileValid ? ( +
+ + + +
+ ) : ( +
+ +
+ ); +}; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx index f1a5127be7..ccf13b1ccc 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectTriggers.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { DialogInfo, ITrigger, SDKKinds, getFriendlyName } from '@bfc/shared'; import { Selection } from 'office-ui-fabric-react/lib/DetailsList'; import { useRecoilValue } from 'recoil'; @@ -20,7 +20,7 @@ const getLabel = (kind: SDKKinds, uiSchema) => { return label || kind.replace('Microsoft.', ''); }; -export const SelectTriggers: React.FC = ({ setSelectedTriggers, projectId }) => { +export const SelectTriggers: React.FC = ({ selectedTriggers, setSelectedTriggers, projectId }) => { const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const schemas = useRecoilValue(schemasState(projectId)); @@ -80,16 +80,21 @@ export const SelectTriggers: React.FC = ({ setSelectedTriggers, pr }, ]; - const selection = useMemo( - () => - new Selection({ - onSelectionChanged: () => { - const selectedItems = selection.getSelection(); - setSelectedTriggers(selectedItems); - }, - }), - [] + const selectionRef = useRef( + new Selection({ + getKey: (item) => item.id, + onSelectionChanged: () => { + const selectedItems = selectionRef.current.getSelection(); + setSelectedTriggers(selectedItems); + }, + }) ); - return ; + useEffect(() => { + for (const item of selectedTriggers) { + selectionRef.current.setKeySelected(selectionRef.current.getKey(item), true, false); + } + }, []); + + return ; }; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/index.ts b/Composer/packages/client/src/pages/design/exportSkillModal/content/index.ts index 94ea08db69..d0ea0a1c97 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/index.ts +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/index.ts @@ -3,9 +3,7 @@ export * from './Description'; export * from './Endpoints'; -export * from './FetchManifestSchema'; export * from './ReviewManifest'; export * from './SaveManifest'; -export * from './SelectManifest'; export * from './SelectDialogs'; export * from './SelectTriggers'; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts b/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts index 79a160e931..6984908364 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts +++ b/Composer/packages/client/src/pages/design/exportSkillModal/generateSkillManifest.ts @@ -2,8 +2,20 @@ // Licensed under the MIT License. import get from 'lodash/get'; -import { DialogInfo, DialogSchemaFile, ITrigger, SDKKinds, SkillManifestFile, LuFile, QnAFile } from '@bfc/shared'; +import { + DialogInfo, + DialogSchemaFile, + ITrigger, + SDKKinds, + SkillManifestFile, + LuFile, + QnAFile, + PublishTarget, +} from '@bfc/shared'; import { JSONSchema7 } from '@bfc/extension-client'; +import { luIndexer } from '@bfc/indexers'; + +import { createManifestFile } from '../../../utils/manifestFileUtil'; import { Activities, Activity, activityHandlerMap, ActivityTypes, DispatchModels } from './constants'; @@ -19,7 +31,9 @@ export const generateSkillManifest = ( luFiles: LuFile[], qnaFiles: QnAFile[], selectedTriggers: ITrigger[], - selectedDialogs: Partial[] + selectedDialogs: Partial[], + currentTarget: PublishTarget, + projectId: string ) => { const { activities: previousActivities, @@ -40,7 +54,7 @@ export const generateSkillManifest = ( const triggers = selectedTriggers.map((tr) => get(content, tr.id) as ITrigger).filter(Boolean); const activities = generateActivities(dialogSchemas, triggers, resolvedDialogs); - const dispatchModels = generateDispatchModels(schema, dialogs, triggers, luFiles, qnaFiles); + const dispatchModels = generateDispatchModels(schema, dialogs, triggers, luFiles, qnaFiles, currentTarget, projectId); const definitions = getDefinitions(dialogSchemas, resolvedDialogs); return { @@ -104,7 +118,9 @@ export const generateDispatchModels = ( dialogs: DialogInfo[], selectedTriggers: any[], luFiles: LuFile[], - qnaFiles: QnAFile[] + qnaFiles: QnAFile[], + target: PublishTarget, + projectId: string ): { dispatchModels?: DispatchModels } => { const intents = selectedTriggers.filter(({ $kind }) => $kind === SDKKinds.OnIntent).map(({ intent }) => intent); const { id: rootId } = dialogs.find((dialog) => dialog?.isRoot) || {}; @@ -123,6 +139,21 @@ export const generateDispatchModels = ( return {}; } + const config = JSON.parse(target.configuration); + const baseEndpointUrl = `https://${config.hostname}.azurewebsites.net/manifests`; + + for (const rootLuFile of rootLuFiles) { + const currentFileName = `skill-${rootLuFile.id}`; + const parsedLuFile = luIndexer.parse(rootLuFile.content, rootLuFile.id, {}); + const contents = parsedLuFile.intents.map((x) => { + if (intents.findIndex((intent) => intent == x.Name) !== -1) { + return [`# ${x.Name}`, x.Body].join('\n'); + } + }); + const mergedContents = contents.join('\n'); + createManifestFile(projectId, currentFileName, mergedContents); + } + const luLanguages = intents.length ? rootLuFiles.reduce((acc, { empty, id }) => { const [name, locale] = id.split('.'); @@ -140,7 +171,7 @@ export const generateDispatchModels = ( { name, contentType: 'application/lu', - url: `<${id}.lu url>`, + url: `${baseEndpointUrl}/skill-${id}.lu`, description: '', }, ], @@ -164,7 +195,7 @@ export const generateDispatchModels = ( { name, contentType: 'application/qna', - url: `<${id}.qna url>`, + url: `${baseEndpointUrl}/skill-${id}.qna`, description: '', }, ], diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx index 55fcbc39a5..5efb3939d9 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx @@ -11,6 +11,9 @@ import { JSONSchema7 } from '@bfc/extension-client'; import { Link } from 'office-ui-fabric-react/lib/components/Link'; import { useRecoilValue } from 'recoil'; import { SkillManifestFile } from '@bfc/shared'; +import { navigate } from '@reach/router'; +import { isUsingAdaptiveRuntime } from '@bfc/shared'; +import cloneDeep from 'lodash/cloneDeep'; import { dispatcherState, @@ -18,12 +21,16 @@ import { qnaFilesSelectorFamily, dialogsSelectorFamily, dialogSchemasState, + currentPublishTargetState, luFilesSelectorFamily, + settingsState, + rootBotProjectIdSelector, } from '../../../recoilModel'; +import { mergePropertiesManagedByRootBot } from '../../../recoilModel/dispatchers/utils/project'; -import { editorSteps, ManifestEditorSteps, order } from './constants'; -import { generateSkillManifest } from './generateSkillManifest'; import { styles } from './styles'; +import { generateSkillManifest } from './generateSkillManifest'; +import { editorSteps, ManifestEditorSteps, order } from './constants'; interface ExportSkillModalProps { isOpen: boolean; @@ -35,18 +42,17 @@ interface ExportSkillModalProps { const ExportSkillModal: React.FC = ({ onSubmit, onDismiss: handleDismiss, projectId }) => { const dialogs = useRecoilValue(dialogsSelectorFamily(projectId)); const dialogSchemas = useRecoilValue(dialogSchemasState(projectId)); + const currentPublishTarget = useRecoilValue(currentPublishTargetState(projectId)); const luFiles = useRecoilValue(luFilesSelectorFamily(projectId)); const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId)); const skillManifests = useRecoilValue(skillManifestsState(projectId)); const { updateSkillManifest } = useRecoilValue(dispatcherState); - const [editingId, setEditingId] = useState(); const [currentStep, setCurrentStep] = useState(0); const [errors, setErrors] = useState({}); const [schema, setSchema] = useState({}); const [skillManifest, setSkillManifest] = useState>({}); - const { content = {}, id } = skillManifest; const [selectedDialogs, setSelectedDialogs] = useState([]); @@ -55,6 +61,32 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss const editorStep = order[currentStep]; const { buttons = [], content: Content, editJson, helpLink, subText, title, validate } = editorSteps[editorStep]; + const settings = useRecoilValue(settingsState(projectId)); + const rootBotProjectId = useRecoilValue(rootBotProjectIdSelector) || ''; + const mergedSettings = mergePropertiesManagedByRootBot(projectId, rootBotProjectId, settings); + const { skillConfiguration, runtime, runtimeSettings, publishTargets } = mergedSettings; + const { setSettings } = useRecoilValue(dispatcherState); + const isAdaptive = isUsingAdaptiveRuntime(runtime); + const [callers, setCallers] = useState( + !isAdaptive ? skillConfiguration?.allowedCallers : runtimeSettings?.skills?.allowedCallers ?? [] + ); + + const updateAllowedCallers = React.useCallback( + (allowedCallers: string[] = []) => { + const updatedSetting = isAdaptive + ? { + ...cloneDeep(mergedSettings), + runtimeSettings: { ...runtimeSettings, skills: { ...runtimeSettings?.skills, allowedCallers } }, + } + : { + ...cloneDeep(mergedSettings), + skillConfiguration: { ...skillConfiguration, allowedCallers }, + }; + setSettings(projectId, updatedSetting); + }, + [mergedSettings, projectId, isAdaptive, skillConfiguration, runtimeSettings] + ); + const handleGenerateManifest = () => { const manifest = generateSkillManifest( schema, @@ -64,9 +96,14 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss luFiles, qnaFiles, selectedTriggers, - selectedDialogs + selectedDialogs, + currentPublishTarget, + projectId ); setSkillManifest(manifest); + if (manifest.content && manifest.id) { + updateSkillManifest(manifest as SkillManifestFile, projectId); + } }; const handleEditJson = () => { @@ -77,20 +114,41 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss } }; + const handleTriggerPublish = () => { + const filePath = `https://${JSON.parse(currentPublishTarget.configuration).hostname}.azurewebsites.net/manifests/${ + skillManifest.id + }.json`; + navigate(`/bot/${projectId}/publish/all?publishTargetName=${currentPublishTarget.name}&url=${filePath}`); + }; + const handleSave = () => { - if (skillManifest.content && skillManifest.id) { - updateSkillManifest(skillManifest as SkillManifestFile, projectId); + const manifest = generateSkillManifest( + schema, + skillManifest, + dialogs, + dialogSchemas, + luFiles, + qnaFiles, + selectedTriggers, + selectedDialogs, + currentPublishTarget, + projectId + ); + if (manifest.content && manifest.id) { + updateSkillManifest(manifest as SkillManifestFile, projectId); } }; + const onSaveSkill = () => { + updateAllowedCallers(callers); + }; + const handleNext = (options?: { dismiss?: boolean; id?: string; save?: boolean }) => { - const validated = - typeof validate === 'function' ? validate({ content, editingId, id, schema, skillManifests }) : errors; + const validated = typeof validate === 'function' ? validate({ content, id, schema, skillManifests }) : errors; if (!Object.keys(validated).length) { setCurrentStep((current) => (current + 1 < order.length ? current + 1 : current)); options?.save && handleSave(); - options?.id && setEditingId(options.id); options?.dismiss && handleDismiss(); setErrors({}); } else { @@ -98,6 +156,10 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss } }; + const handleBack = () => { + setCurrentStep((current) => (current > 0 ? current - 1 : current)); + }; + return ( = ({ onSubmit, onDismiss )}

-
+
= ({ onSubmit, onDismiss skillManifests={skillManifests} value={content} onChange={(manifestContent) => setSkillManifest({ ...skillManifest, content: manifestContent })} + onUpdateCallers={setCallers} />
@@ -147,7 +218,8 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss
{buttons.map(({ disabled, primary, text, onClick }, index) => { const Button = primary ? PrimaryButton : DefaultButton; - const isDisabled = typeof disabled === 'function' ? disabled({ manifest: skillManifest }) : !!disabled; + + const isDisabled = typeof disabled === 'function' ? disabled({ publishTargets }) : !!disabled; return (