Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Composer/packages/client/src/shell/useShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { useMemo, useRef } from 'react';
import { ShellApi, ShellData, Shell, fetchFromSettings, DialogSchemaFile, SkillSetting } from '@bfc/shared';
import { ShellApi, ShellData, Shell, DialogSchemaFile } from '@bfc/shared';
import { useRecoilValue } from 'recoil';
import formatMessage from 'format-message';

Expand Down Expand Up @@ -203,10 +203,7 @@ export function useShell(source: EventSource, projectId: string): Shell {
updateDialogSchema: async (dialogSchema: DialogSchemaFile) => {
updateDialogSchema(dialogSchema, projectId);
},
skillsSettings: {
get: (path: string) => fetchFromSettings(path, settings),
set: (id: string, skill: SkillSetting) => updateSkill(projectId, id, skill),
},
updateSkillSetting: (...params) => updateSkill(projectId, ...params),
};

const currentDialog = useMemo(() => dialogs.find((d) => d.id === dialogId), [dialogs, dialogId]);
Expand Down Expand Up @@ -240,6 +237,7 @@ export function useShell(source: EventSource, projectId: string): Shell {
clipboardActions,
hosted: !!isAbsHosted(),
skills,
skillsSettings: settings.skill || {},
}
: ({} as ShellData);

Expand Down
6 changes: 2 additions & 4 deletions Composer/packages/lib/shared/src/types/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface ShellData {
qnaFiles: QnAFile[];
userSettings: UserSettings;
skills: any[];
skillsSettings: { [name: string]: SkillSetting };
// TODO: remove
schemas: BotSchemas;
}
Expand Down Expand Up @@ -100,10 +101,7 @@ export interface ShellApi {
displayManifestModal: (manifestId: string) => void;
updateDialogSchema: (_: DialogSchemaFile) => Promise<void>;
createTrigger: (id: string, formData, url?: string) => void;
skillsSettings: {
get: (path: string) => any;
set: (skillId: string, skillsData: SkillSetting) => Promise<void>;
};
updateSkillSetting: (skillId: string, skillsData: SkillSetting) => Promise<void>;
}

export interface Shell {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,147 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React, { useMemo, useState, useEffect } from 'react';
import { FieldProps, JSONSchema7, useShellApi } from '@bfc/extension-client';
import { Link } from 'office-ui-fabric-react/lib/Link';
import React from 'react';
import { FieldProps } from '@bfc/extension-client';
import { ObjectField } from '@bfc/adaptive-form';
import formatMessage from 'format-message';
import { Skill, getSkillNameFromSetting } from '@bfc/shared';
import { IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox';

import { SelectSkillDialog } from './SelectSkillDialogField';
import { SkillEndpointField } from './SkillEndpointField';

const referBySettings = (skillName: string, property: string) => {
return `=settings.skill['${skillName}'].${property}`;
};

const settingReferences = (skillName: string) => ({
skillEndpoint: referBySettings(skillName, 'endpointUrl'),
skillAppId: referBySettings(skillName, 'msAppId'),
});

const handleBackwardCompatibility = (skills: Skill[], value): { name: string; endpointName: string } | undefined => {
const { skillEndpoint } = value;
const foundSkill = skills.find(({ manifestUrl }) => manifestUrl === value.id);
if (foundSkill) {
const matchedEndpoint: any = foundSkill.endpoints.find(({ endpointUrl }) => endpointUrl === skillEndpoint);
return {
name: foundSkill?.name,
endpointName: matchedEndpoint ? matchedEndpoint.name : '',
};
}
};

export const BeginSkillDialogField: React.FC<FieldProps> = (props) => {
const { depth, id, schema, uiOptions, value, onChange, definitions } = props;
const { projectId, shellApi, skills = [] } = useShellApi();
const { displayManifestModal, skillsSettings } = shellApi;
const [selectedSkill, setSelectedSkill] = useState<string>('');
const [oldEndpoint, loadEndpointForOldBots] = useState<string>('');

useEffect(() => {
const { skillEndpoint } = value;
const skill = skills.find(({ name }) => name === getSkillNameFromSetting(skillEndpoint));

if (skill) {
setSelectedSkill(skill.name);
} else {
const result = handleBackwardCompatibility(skills, value);
if (result) {
setSelectedSkill(result.name);
if (result.endpointName) {
loadEndpointForOldBots(result.endpointName);
}
}
}
}, []);

const matchedSkill = useMemo(() => {
return skills.find(({ id }) => id === selectedSkill) || ({} as Skill);
}, [skills, selectedSkill]);

const endpointOptions = useMemo(() => {
return (matchedSkill.endpoints || []).map(({ name }) => name);
}, [matchedSkill]);

const handleEndpointChange = async (skillEndpoint) => {
if (matchedSkill.id) {
const { msAppId, endpointUrl } =
(matchedSkill.endpoints || []).find(({ name }) => name === skillEndpoint) || ({} as any);
const schemaUpdate: any = {};
const settingsUpdate: any = { ...matchedSkill };
if (endpointUrl) {
schemaUpdate.skillEndpoint = referBySettings(matchedSkill.name, 'endpointUrl');
settingsUpdate.endpointUrl = endpointUrl;
}
if (msAppId) {
schemaUpdate.skillAppId = referBySettings(matchedSkill.name, 'msAppId');
settingsUpdate.msAppId = msAppId;
}
skillsSettings.set(matchedSkill.id, { ...settingsUpdate });
onChange({
...value,
...schemaUpdate,
});
}
};

useEffect(() => {
if (oldEndpoint) {
handleEndpointChange(oldEndpoint);
}
}, [oldEndpoint]);

const handleShowManifestClick = () => {
matchedSkill && displayManifestModal(matchedSkill.manifestUrl);
};

const skillEndpointUiSchema = uiOptions.properties?.skillEndpoint || {};
skillEndpointUiSchema.serializer = {
get: (value) => {
const url: any = skillsSettings.get(value);
const endpoint = (matchedSkill?.endpoints || []).find(({ endpointUrl }) => endpointUrl === url);
return endpoint?.name;
},
set: (value) => {
const endpoint = (matchedSkill?.endpoints || []).find(({ name }) => name === value);
return endpoint?.endpointUrl;
},
};

const onSkillSelectionChange = (option: IComboBoxOption | null) => {
if (option?.text) {
setSelectedSkill(option.text);
onChange({ ...value, ...settingReferences(option.text) });
}
};
const { value, onChange } = props;

return (
<React.Fragment>
<SelectSkillDialog value={selectedSkill} onChange={onSkillSelectionChange} />
<Link
disabled={!matchedSkill || !matchedSkill.content || !matchedSkill.name}
styles={{ root: { fontSize: '12px', padding: '0 16px' } }}
onClick={handleShowManifestClick}
>
{formatMessage('Show skill manifest')}
</Link>
<SkillEndpointField
definitions={definitions}
depth={depth + 1}
enumOptions={endpointOptions}
id={`${id}.skillEndpoint`}
name="skillEndpoint"
rawErrors={{}}
schema={(schema?.properties?.skillEndpoint as JSONSchema7) || {}}
uiOptions={skillEndpointUiSchema}
value={value?.skillEndpoint}
onChange={handleEndpointChange}
/>
<Link href={`/bot/${projectId}/skills`} styles={{ root: { fontSize: '12px', padding: '0 16px' } }}>
{formatMessage('Open Skills page for configuration details')}
</Link>
<SelectSkillDialog value={value} onChange={onChange} />
<ObjectField {...props} />
</React.Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,39 @@ import { IComboBoxOption, SelectableOptionMenuItemType } from 'office-ui-fabric-
import { useShellApi } from '@bfc/extension-client';
import formatMessage from 'format-message';
import { schemaField } from '@bfc/adaptive-form';
import { getSkillNameFromSetting, Skill } from '@bfc/shared';
import { Link } from 'office-ui-fabric-react/lib/components/Link/Link';

import { ComboBoxField } from './ComboBoxField';

const ADD_DIALOG = 'ADD_DIALOG';

const referBySettings = (skillName: string, property: string) => {
return `=settings.skill['${skillName}'].${property}`;
};

export const settingReferences = (skillName: string) => ({
skillEndpoint: referBySettings(skillName, 'endpointUrl'),
skillAppId: referBySettings(skillName, 'msAppId'),
});

export const SelectSkillDialog: React.FC<{
value: string;
onChange: (option: IComboBoxOption | null) => void;
value: any;
onChange: (value: any) => void;
}> = (props) => {
const { value, onChange } = props;
const { shellApi, skills = [] } = useShellApi();
const { addSkillDialog } = shellApi;
const { addSkillDialog, displayManifestModal } = shellApi;
const [comboboxTitle, setComboboxTitle] = useState<string | null>(null);

const options: IComboBoxOption[] = skills.map(({ name }) => ({
const skillId = getSkillNameFromSetting(value.skillEndpoint);
const { content, manifestUrl, name } = skills.find(({ id }) => id === skillId) || ({} as Skill);

const options: IComboBoxOption[] = skills.map(({ id, name }) => ({
key: name,
text: name,
isSelected: value === name,
data: settingReferences(id),
isSelected: id === skillId,
}));

options.push(
Expand All @@ -41,21 +56,19 @@ export const SelectSkillDialog: React.FC<{
options.push({ key: 'customTitle', text: comboboxTitle });
}

const handleChange = (_, option) => {
const handleChange = (_, option: IComboBoxOption) => {
if (option) {
if (option.key === ADD_DIALOG) {
setComboboxTitle(formatMessage('Add a new Skill Dialog'));
addSkillDialog().then((skill) => {
if (skill?.manifestUrl && skill?.name) {
onChange({ key: skill?.manifestUrl, text: skill?.name });
onChange({ ...value, ...settingReferences(skill.name) });
}
setComboboxTitle(null);
});
} else {
onChange(option);
onChange({ ...value, ...option?.data });
}
} else {
onChange(null);
}
};

Expand All @@ -67,9 +80,16 @@ export const SelectSkillDialog: React.FC<{
id={'SkillDialogName'}
label={formatMessage('Skill Dialog Name')}
options={options}
value={value}
value={skillId}
onChange={handleChange}
/>
<Link
disabled={!content || !name}
styles={{ root: { fontSize: '12px', paddingTop: '4px' } }}
onClick={() => manifestUrl && displayManifestModal(manifestUrl)}
>
{formatMessage('Show skill manifest')}
</Link>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,48 @@

/** @jsx jsx */
import { jsx } from '@emotion/core';
import React from 'react';
import { FieldProps, useFormConfig } from '@bfc/extension-client';
import {
getUiLabel,
getUIOptions,
getUiPlaceholder,
getUiDescription,
schemaField,
SelectField,
} from '@bfc/adaptive-form';
import React, { useMemo } from 'react';
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
import { FieldProps, useShellApi } from '@bfc/extension-client';
import { FieldLabel } from '@bfc/adaptive-form';
import { getSkillNameFromSetting, Skill } from '@bfc/shared';

export const SkillEndpointField: React.FC<FieldProps> = (props) => {
const { depth, schema, uiOptions: baseUIOptions, value, onChange } = props;
const formUIOptions = useFormConfig();
const { description, label, required, uiOptions, value } = props;
const { shellApi, skillsSettings, skills = [] } = useShellApi();
const { updateSkillSetting } = shellApi;

const uiOptions = {
...getUIOptions(schema, formUIOptions),
...baseUIOptions,
};
const id = getSkillNameFromSetting(value);
const skill = skills.find(({ id: skillId }) => skillId === id) || ({} as Skill);
const { endpointUrl, msAppId } = skillsSettings[id] || {};

const { endpoints = [] } = skill;

const deserializedValue = typeof uiOptions?.serializer?.get === 'function' ? uiOptions.serializer.get(value) : value;
const options = useMemo(
() =>
endpoints.map(({ name, endpointUrl, msAppId }, key) => ({
key,
text: name,
data: {
endpointUrl,
msAppId,
},
})),
[endpoints]
);

const handleChange = (newValue: any) => {
const serializedValue = newValue;
if (typeof uiOptions?.serializer?.set === 'function') {
uiOptions.serializer.set(newValue);
const { key } = options.find(({ data }) => data.endpointUrl === endpointUrl && data.msAppId === msAppId) || {};

const handleChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => {
if (option) {
updateSkillSetting(skill.id, { ...skill, ...option.data });
}
onChange(serializedValue);
};

const label = getUiLabel({ ...props, uiOptions });
const placeholder = getUiPlaceholder({ ...props, uiOptions });
const description = getUiDescription({ ...props, uiOptions });

return (
<div css={schemaField.container(depth)}>
<SelectField
{...props}
description={description}
label={label}
placeholder={placeholder}
value={deserializedValue}
onChange={handleChange}
/>
</div>
<React.Fragment>
<FieldLabel description={description} helpLink={uiOptions?.helpLink} id={id} label={label} required={required} />
<Dropdown options={options} selectedKey={key} onChange={handleChange} />
</React.Fragment>
);
};
Loading