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 all 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: Record<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