diff --git a/Composer/.prettierignore b/Composer/.prettierignore index 76add878f8..f06235c460 100644 --- a/Composer/.prettierignore +++ b/Composer/.prettierignore @@ -1,2 +1,2 @@ node_modules -dist \ No newline at end of file +dist diff --git a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx index 9311de2920..258d3392f7 100644 --- a/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx +++ b/Composer/packages/client/src/components/ProjectTree/ProjectTree.tsx @@ -461,7 +461,10 @@ export const ProjectTree: React.FC = ({ const renderDialogTriggersByProperty = (dialog: DialogInfo, projectId: string, startDepth: number) => { const jsonSchemaFiles = jsonSchemaFilesByProjectId[projectId]; const dialogSchemaProperties = extractSchemaProperties(dialog, jsonSchemaFiles); - const groupedTriggers = groupTriggersByPropertyReference(dialog, { validProperties: dialogSchemaProperties }); + const groupedTriggers = groupTriggersByPropertyReference(dialog, { + validProperties: dialogSchemaProperties, + allowMultiParent: true, + }); const triggerGroups = Object.keys(groupedTriggers); diff --git a/Composer/packages/client/src/pages/form-dialog/FormDialogPage.tsx b/Composer/packages/client/src/pages/form-dialog/FormDialogPage.tsx index e0ad4b6392..058cb9aad6 100644 --- a/Composer/packages/client/src/pages/form-dialog/FormDialogPage.tsx +++ b/Composer/packages/client/src/pages/form-dialog/FormDialogPage.tsx @@ -63,10 +63,9 @@ const FormDialogPage: React.FC = React.memo((props: Props) => { loadFormDialogSchemaTemplates(); }, []); - const availableTemplates = React.useMemo( - () => formDialogLibraryTemplates.filter((t) => !t.isGlobal).map((t) => t.name), - [formDialogLibraryTemplates] - ); + const availableTemplates = React.useMemo(() => formDialogLibraryTemplates.filter((t) => !t.$global), [ + formDialogLibraryTemplates, + ]); const validSchemaId = React.useMemo(() => formDialogSchemaIds.includes(schemaId), [formDialogSchemaIds, schemaId]); diff --git a/Composer/packages/client/src/pages/form-dialog/VisualFormDialogSchemaEditor.tsx b/Composer/packages/client/src/pages/form-dialog/VisualFormDialogSchemaEditor.tsx index 0186e66680..2d7d1455cb 100644 --- a/Composer/packages/client/src/pages/form-dialog/VisualFormDialogSchemaEditor.tsx +++ b/Composer/packages/client/src/pages/form-dialog/VisualFormDialogSchemaEditor.tsx @@ -3,7 +3,7 @@ import { JsonEditor } from '@bfc/code-editor'; import { FormDialogSchemaEditor } from '@bfc/form-dialogs'; -import { FileExtensions } from '@bfc/shared'; +import { FileExtensions, FormDialogSchemaTemplate } from '@bfc/shared'; import styled from '@emotion/styled'; import { NeutralColors } from '@uifabric/fluent-theme'; import formatMessage from 'format-message'; @@ -13,7 +13,7 @@ import { classNamesFunction } from 'office-ui-fabric-react/lib/Utilities'; import * as React from 'react'; import { useRecoilValue } from 'recoil'; -import { formDialogSchemaState } from '../../recoilModel'; +import { formDialogSchemaState, localeState } from '../../recoilModel'; import TelemetryClient from '../../telemetry/TelemetryClient'; const Root = styled(Stack)<{ @@ -51,7 +51,7 @@ type Props = { projectId: string; schemaId: string; generationInProgress?: boolean; - templates: string[]; + templates: FormDialogSchemaTemplate[]; onChange: (id: string, content: string) => void; onGenerate: (schemaId: string) => void; }; @@ -59,6 +59,7 @@ type Props = { export const VisualFormDialogSchemaEditor = React.memo((props: Props) => { const { projectId, schemaId, templates, onChange, onGenerate, generationInProgress = false } = props; + const locale = useRecoilValue(localeState(projectId)); const schema = useRecoilValue(formDialogSchemaState({ projectId, schemaId })); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -118,6 +119,7 @@ export const VisualFormDialogSchemaEditor = React.memo((props: Props) => { allowUndo editorId={`${projectId}:${schema.id}`} isGenerating={generationInProgress} + locale={locale} schema={schema} schemaExtension={FileExtensions.FormDialogSchema} templates={templates} diff --git a/Composer/packages/client/src/recoilModel/dispatchers/formDialogs.ts b/Composer/packages/client/src/recoilModel/dispatchers/formDialogs.ts index 8cc63807a7..44e3ee7181 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/formDialogs.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/formDialogs.ts @@ -50,11 +50,10 @@ export const formDialogsDispatcher = () => { } try { - const { data } = await httpClient.get('/formDialogs/templateSchemas'); - const templates = Object.keys(data).map((key) => ({ - name: key, - isGlobal: data[key].$global, - })); + const { data } = await httpClient.get>>( + '/formDialogs/templateSchemas' + ); + const templates = Object.keys(data).map((id) => ({ id, ...data[id] })); set(formDialogLibraryTemplatesState, templates); } catch (ex) { diff --git a/Composer/packages/form-dialogs/package.json b/Composer/packages/form-dialogs/package.json index f80fd8e062..4008ad4763 100644 --- a/Composer/packages/form-dialogs/package.json +++ b/Composer/packages/form-dialogs/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "react-beautiful-dnd": "^13.0.0", + "@bfc/shared": "*", "@bfc/ui-shared": "*" }, "peerDependencies": { diff --git a/Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx b/Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx index 1f311e33f1..a474e6ea19 100644 --- a/Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx +++ b/Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { FormDialogSchemaTemplate } from '@bfc/shared'; import * as React from 'react'; -import { useRecoilValue } from 'recoil'; // eslint-disable-next-line @typescript-eslint/camelcase -import { RecoilRoot, useRecoilTransactionObserver_UNSTABLE } from 'recoil'; +import { RecoilRoot, useRecoilTransactionObserver_UNSTABLE, useRecoilValue } from 'recoil'; import { formDialogSchemaJsonSelector, trackedAtomsSelector } from './atoms/appState'; import { useHandlers } from './atoms/handlers'; @@ -12,6 +12,7 @@ import { FormDialogPropertiesEditor } from './components/FormDialogPropertiesEdi import { UndoRoot } from './undo/UndoRoot'; export type FormDialogSchemaEditorProps = { + locale: string; /** * Unique id for the visual editor. */ @@ -31,7 +32,7 @@ export type FormDialogSchemaEditorProps = { /** * Record of available schema templates. */ - templates?: string[]; + templates?: FormDialogSchemaTemplate[]; /** * Indicates of caller is running generation logic. */ @@ -48,10 +49,11 @@ export type FormDialogSchemaEditorProps = { const InternalFormDialogSchemaEditor = React.memo((props: FormDialogSchemaEditorProps) => { const { + locale, editorId, schema, templates = [], - schemaExtension = '.schema', + schemaExtension = '.template', isGenerating = false, onSchemaUpdated, onGenerateDialog, @@ -59,15 +61,23 @@ const InternalFormDialogSchemaEditor = React.memo((props: FormDialogSchemaEditor } = props; const trackedAtoms = useRecoilValue(trackedAtomsSelector); - const { setTemplates, reset, importSchemaString } = useHandlers(); + const { setTemplates, reset, updateLocale, importSchemaString } = useHandlers(); + + React.useEffect(() => { + if (locale) { + updateLocale({ locale }); + } + }, [locale]); React.useEffect(() => { setTemplates({ templates }); }, [templates]); React.useEffect(() => { - importSchemaString(schema); - }, [editorId]); + if (templates.length) { + importSchemaString({ ...schema, templates }); + } + }, [editorId, templates]); const startOver = React.useCallback(() => { reset({ name: schema.id }); diff --git a/Composer/packages/form-dialogs/src/atoms/appState.ts b/Composer/packages/form-dialogs/src/atoms/appState.ts index 9e90341c35..bd46dfec08 100644 --- a/Composer/packages/form-dialogs/src/atoms/appState.ts +++ b/Composer/packages/form-dialogs/src/atoms/appState.ts @@ -3,13 +3,32 @@ /* eslint-disable @typescript-eslint/consistent-type-assertions */ +import { FormDialogSchemaTemplate } from '@bfc/shared'; import { atom, atomFamily, RecoilState, selector, selectorFamily } from 'recoil'; -import { FormDialogProperty, FormDialogSchema } from './types'; -import { spreadSchemaPropertyStore, validateSchemaPropertyStore } from './utils'; +import { PropertyCardData } from '../components/property/types'; + +import { FormDialogSchema } from './types'; +import { spreadCardData, validateSchemaPropertyStore } from './utils'; const schemaDraftUrl = 'http://json-schema.org/draft-07/schema'; +/** + * Locale + */ +export const formDialogLocale = atom({ + key: 'FormDialogLocale', + default: '', +}); + +/** + * This atom represents the list of the available templates. + */ +export const formDialogTemplatesAtom = atom({ + key: 'FormDialogTemplatesAtom', + default: [], +}); + /** * This atom represents a form dialog schema. */ @@ -24,18 +43,16 @@ export const formDialogSchemaAtom = atom({ }); /** - * This atom family represent a form dialog schema property. + * This atom family represent a form dialog schema property card data. */ -export const formDialogPropertyAtom = atomFamily({ - key: 'FormDialogPropertyAtom', +export const propertyCardDataAtom = atomFamily({ + key: 'PropertyCardDataAtom', default: (id) => ({ id, name: '', - kind: 'string', - payload: { kind: 'string' }, - required: true, - array: false, - examples: [], + isArray: false, + isRequired: true, + propertyType: 'string', }), }); @@ -57,7 +74,7 @@ export const formDialogSchemaPropertyNamesSelector = selector({ key: 'FormDialogSchemaPropertyNamesSelector', get: ({ get }) => { const propertyIds = get(allFormDialogPropertyIdsSelector); - return propertyIds.map((pId) => get(formDialogPropertyAtom(pId)).name); + return propertyIds.map((pId) => get(propertyCardDataAtom(pId)).name); }, }); @@ -67,8 +84,8 @@ export const formDialogSchemaPropertyNamesSelector = selector({ export const formDialogPropertyJsonSelector = selectorFamily({ key: 'FormDialogPropertyJsonSelector', get: (id) => ({ get }) => { - const schemaPropertyStore = get(formDialogPropertyAtom(id)); - return spreadSchemaPropertyStore(schemaPropertyStore); + const cardData = get(propertyCardDataAtom(id)); + return spreadCardData(cardData); }, }); @@ -78,8 +95,9 @@ export const formDialogPropertyJsonSelector = selectorFamily({ export const formDialogPropertyValidSelector = selectorFamily({ key: 'FormDialogPropertyValidSelector', get: (id) => ({ get }) => { - const schemaPropertyStore = get(formDialogPropertyAtom(id)); - return validateSchemaPropertyStore(schemaPropertyStore); + const templates = get(formDialogTemplatesAtom); + const cardData = get(propertyCardDataAtom(id)); + return validateSchemaPropertyStore(cardData, templates); }, }); @@ -101,53 +119,34 @@ export const formDialogSchemaJsonSelector = selector({ key: 'FormDialogSchemaJsonSelector', get: ({ get }) => { const propertyIds = get(allFormDialogPropertyIdsSelector); - const schemaPropertyStores = propertyIds.map((pId) => get(formDialogPropertyAtom(pId))); + const propertyCards = propertyIds.map((pId) => get(propertyCardDataAtom(pId))); let jsonObject: object = { $schema: schemaDraftUrl, type: 'object', - $requires: ['standard.schema'], }; - if (schemaPropertyStores.length) { + if (propertyCards.length) { jsonObject = { ...jsonObject, properties: propertyIds.reduce>((acc, propId, idx) => { - const property = schemaPropertyStores[idx]; + const property = propertyCards[idx]; acc[property.name] = get(formDialogPropertyJsonSelector(propId)); return acc; }, >{}), }; } - const required = schemaPropertyStores.filter((property) => property.required).map((property) => property.name); - const examples = schemaPropertyStores.reduce>((acc, property) => { - if (property.examples?.length) { - acc[property.name] = property.examples; - } - return acc; - }, >{}); + const required = propertyCards.filter((property) => property.isRequired).map((property) => property.name); if (required.length) { jsonObject = { ...jsonObject, required }; } - if (Object.keys(examples)?.length) { - jsonObject = { ...jsonObject, $examples: examples }; - } - return JSON.stringify(jsonObject, null, 2); }, }); -/** - * This atom represents the list of the available templates. - */ -export const formDialogTemplatesAtom = atom({ - key: 'FormDialogTemplatesAtom', - default: [], -}); - export const activePropertyIdAtom = atom({ key: 'ActivePropertyIdAtom', default: '', @@ -158,7 +157,6 @@ export const trackedAtomsSelector = selector[]>({ key: 'TrackedAtoms', get: ({ get }) => { const propIds = get(allFormDialogPropertyIdsSelector) || []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return [formDialogSchemaAtom, activePropertyIdAtom, ...propIds.map((pId) => formDialogPropertyAtom(pId))]; + return [formDialogSchemaAtom, activePropertyIdAtom, ...propIds.map((pId) => propertyCardDataAtom(pId))]; }, }); diff --git a/Composer/packages/form-dialogs/src/atoms/handlers.ts b/Composer/packages/form-dialogs/src/atoms/handlers.ts index 212042a270..ca66582d7a 100644 --- a/Composer/packages/form-dialogs/src/atoms/handlers.ts +++ b/Composer/packages/form-dialogs/src/atoms/handlers.ts @@ -3,39 +3,58 @@ /* eslint-disable react-hooks/rules-of-hooks */ +import { FormDialogSchemaTemplate } from '@bfc/shared'; +import cloneDeep from 'lodash/cloneDeep'; +import merge from 'lodash/merge'; +import pick from 'lodash/pick'; import * as React from 'react'; import { useRecoilCallback } from 'recoil'; +import { PropertyCardData } from '../components/property/types'; import { generateId } from '../utils/base'; import { readFileContent } from '../utils/file'; import { activePropertyIdAtom, allFormDialogPropertyIdsSelector, - formDialogPropertyAtom, + formDialogLocale, + formDialogLocale as formDialogLocaleAtom, formDialogSchemaAtom, formDialogSchemaPropertyNamesSelector, formDialogTemplatesAtom, + propertyCardDataAtom, } from './appState'; -import { FormDialogPropertyKind, FormDialogPropertyPayload, PropertyRequiredKind } from './types'; -import { createSchemaStoreFromJson, getDefaultPayload, getDuplicateName } from './utils'; +import { PropertyRequiredKind } from './types'; +import { createSchemaStoreFromJson, getDuplicateName } from './utils'; const getHandlers = () => { - const importSchemaString = useRecoilCallback(({ set }) => ({ id, content }: { id: string; content: string }) => { - const schema = createSchemaStoreFromJson(id, content); - - set(formDialogSchemaAtom, { - id: schema.name, - name: schema.name, - requiredPropertyIds: schema.properties.filter((p) => p.required).map((p) => p.id), - optionalPropertyIds: schema.properties.filter((p) => !p.required).map((p) => p.id), - }); - schema.properties.forEach((property) => set(formDialogPropertyAtom(property.id), property)); - }); + const importSchemaString = useRecoilCallback( + ({ set }) => async ({ + id, + content, + templates, + }: { + id: string; + content: string; + templates: FormDialogSchemaTemplate[]; + }) => { + const schema = createSchemaStoreFromJson(id, content, templates); + + set(formDialogSchemaAtom, { + id: schema.name, + name: schema.name, + requiredPropertyIds: schema.properties.filter((p) => p.isRequired).map((p) => p.id), + optionalPropertyIds: schema.properties.filter((p) => !p.isRequired).map((p) => p.id), + }); + schema.properties.forEach((property) => set(propertyCardDataAtom(property.id), property)); + } + ); - const importSchema = useRecoilCallback(() => async ({ id, file }: { id: string; file: File }) => { + const importSchema = useRecoilCallback(({ snapshot }) => async ({ id, file }: { id: string; file: File }) => { + const templates = await snapshot.getPromise(formDialogTemplatesAtom); const content = await readFileContent(file); - importSchemaString({ id, content }); + + importSchemaString({ id, content, templates }); }); const activatePropertyId = useRecoilCallback(({ set }) => ({ id }: { id: string }) => { @@ -45,61 +64,101 @@ const getHandlers = () => { const addProperty = useRecoilCallback(({ set }) => () => { const newPropertyId = generateId(); set(formDialogSchemaAtom, (currentSchema) => { - return { ...currentSchema, requiredPropertyIds: [newPropertyId, ...currentSchema.requiredPropertyIds] }; + return { ...currentSchema, requiredPropertyIds: [...currentSchema.requiredPropertyIds, newPropertyId] }; }); - set(formDialogPropertyAtom(newPropertyId), { + set(propertyCardDataAtom(newPropertyId), { id: newPropertyId, - kind: 'string', + propertyType: 'string', name: '', - payload: { kind: 'string' }, - examples: [], - required: true, - array: false, + isArray: false, + isRequired: true, }); activatePropertyId({ id: newPropertyId }); }); - const changePropertyKind = useRecoilCallback( - ({ set }) => ({ - id, - kind, - payload, - }: { - id: string; - kind: FormDialogPropertyKind; - payload: FormDialogPropertyPayload; - }) => { - set(formDialogPropertyAtom(id), (currentProperty) => { - return { ...currentProperty, kind, examples: [], payload: payload || getDefaultPayload(kind) }; + const changePropertyType = useRecoilCallback( + ({ set }) => ({ id, propertyType }: { id: string; propertyType: string }) => { + set(propertyCardDataAtom(id), (currentPropertyCardData) => { + const basicCardData = pick(currentPropertyCardData, ['id', 'name', 'isRequired', 'isArray']); + return { ...basicCardData, propertyType }; }); } ); const changePropertyRequired = useRecoilCallback( - ({ set }) => ({ id, required }: { id: string; required: boolean }) => { - set(formDialogPropertyAtom(id), (currentProperty) => { - return { ...currentProperty, required }; + ({ set }) => ({ id, isRequired }: { id: string; isRequired: boolean }) => { + set(propertyCardDataAtom(id), (currentPropertyCardData) => { + return { ...currentPropertyCardData, isRequired }; }); } ); - const changePropertyName = useRecoilCallback(({ set }) => ({ id, name }: { id: string; name: string }) => { - set(formDialogPropertyAtom(id), (currentProperty) => { - return { ...currentProperty, name }; - }); - }); + const changePropertyName = useRecoilCallback( + ({ set, snapshot }) => async ({ id, name }: { id: string; name: string }) => { + const locale = await snapshot.getPromise(formDialogLocale); + set(propertyCardDataAtom(id), (currentPropertyCardData) => { + const currentName = currentPropertyCardData.name; + const cardData = { ...currentPropertyCardData, name } as PropertyCardData; - const changePropertyPayload = useRecoilCallback( - ({ set }) => ({ id, payload }: { id: string; payload: FormDialogPropertyPayload }) => { - set(formDialogPropertyAtom(id), (currentProperty) => { - return { ...currentProperty, payload }; + // Change the name of the entity for examples if the type is enum + if (cardData.propertyType === 'enum' && cardData.$examples?.[locale]?.[`${currentName}Value`]) { + const examples = cloneDeep(cardData.$examples); + examples[locale][`${name}Value`] = { ...examples[locale][`${currentName}Value`] }; + delete examples[locale][`${currentName}Value`]; + + cardData.$examples = examples; + } + + return cardData; + }); + } + ); + + const changePropertyCardData = useRecoilCallback( + ({ set, snapshot }) => async ({ id, data }: { id: string; data: Record }) => { + const locale = await snapshot.getPromise(formDialogLocale); + set(propertyCardDataAtom(id), (currentPropertyCardData) => { + const hadEnum = !!currentPropertyCardData.enum?.length; + const cardData = { ...currentPropertyCardData, ...data } as PropertyCardData; + + if (cardData.propertyType === 'enum') { + // If $examples is empty, deleted current locale examples + if (hadEnum && !cardData.$examples?.[locale]) { + return cardData; + } + + const defaultExamples = { + [locale]: { + [`${cardData.name}Value`]: (cardData?.enum ?? []).reduce((acc, e) => { + acc[e] = []; + return acc; + }, {}), + }, + }; + + const newExamples = data?.$examples?.[locale]?.[`${cardData.name}Value`] ? data.$examples : defaultExamples; + const oldExamples = currentPropertyCardData?.$examples?.[locale]?.[`${cardData.name}Value`] + ? currentPropertyCardData.$examples + : defaultExamples; + + // Merge default, old and new and only keep the examples for current enum values + const mergedExamples = pick( + merge(defaultExamples, newExamples, oldExamples), + cardData.enum.map((e: string) => `${locale}.${cardData.name}Value.${e}`) + ); + cardData.$examples = mergedExamples; + + return cardData; + } + + return cardData; }); } ); const changePropertyArray = useRecoilCallback(({ set }) => ({ id, isArray }: { id: string; isArray: boolean }) => { - set(formDialogPropertyAtom(id), (currentProperty) => { - return { ...currentProperty, array: isArray }; + set(propertyCardDataAtom(id), (currentPropertyCardData) => { + return { ...currentPropertyCardData, isArray: isArray }; }); }); @@ -120,7 +179,7 @@ const getHandlers = () => { const toggleRequired = source !== destination; if (toggleRequired) { - changePropertyRequired({ id, required: source === 'optional' }); + changePropertyRequired({ id, isRequired: source === 'optional' }); } set(formDialogSchemaAtom, (currentSchema) => { @@ -157,19 +216,19 @@ const getHandlers = () => { ); const removeProperty = useRecoilCallback(({ set, reset, snapshot }) => async ({ id }: { id: string }) => { - const property = await snapshot.getPromise(formDialogPropertyAtom(id)); + const property = await snapshot.getPromise(propertyCardDataAtom(id)); set(formDialogSchemaAtom, (currentSchema) => ({ ...currentSchema, - requiredPropertyIds: property.required + requiredPropertyIds: property.isRequired ? currentSchema.requiredPropertyIds.filter((pId) => pId !== id) : currentSchema.requiredPropertyIds, - optionalPropertyIds: !property.required + optionalPropertyIds: !property.isRequired ? currentSchema.optionalPropertyIds.filter((pId) => pId !== id) : currentSchema.optionalPropertyIds, })); - reset(formDialogPropertyAtom(id)); + reset(propertyCardDataAtom(id)); const activePropertyId = await snapshot.getPromise(activePropertyIdAtom); @@ -181,52 +240,47 @@ const getHandlers = () => { const duplicateProperty = useRecoilCallback(({ set, snapshot }) => async ({ id }: { id: string }) => { const newId = generateId(); - const property = await snapshot.getPromise(formDialogPropertyAtom(id)); + const propertyCardData = await snapshot.getPromise(propertyCardDataAtom(id)); const propertyNames = await snapshot.getPromise(formDialogSchemaPropertyNamesSelector); - const name = getDuplicateName(property.name, propertyNames); + const name = getDuplicateName(propertyCardData.name, propertyNames); set(formDialogSchemaAtom, (currentSchema) => { - const propertyIds = (property.required + const propertyIds = (propertyCardData.isRequired ? currentSchema.requiredPropertyIds : currentSchema.optionalPropertyIds ).slice(); - if (property.required) { + if (propertyCardData.isRequired) { return { ...currentSchema, requiredPropertyIds: [...propertyIds, newId] }; } else { return { ...currentSchema, optionalPropertyIds: [...propertyIds, newId] }; } }); - set(formDialogPropertyAtom(newId), { ...property, name, id: newId }); + set(propertyCardDataAtom(newId), { ...cloneDeep(propertyCardData), name, id: newId }); activatePropertyId({ id: newId }); }); - const changePropertyExamples = useRecoilCallback( - ({ set }) => ({ id, examples }: { id: string; examples: readonly string[] }) => { - set(formDialogPropertyAtom(id), (currentProperty) => { - return { ...currentProperty, examples: examples.slice() }; - }); - } - ); - const reset = useRecoilCallback(({ reset, set, snapshot }) => async ({ name }: { name: string }) => { const propertyIds = await snapshot.getPromise(allFormDialogPropertyIdsSelector); - propertyIds.forEach((pId) => reset(formDialogPropertyAtom(pId))); + propertyIds.forEach((pId) => reset(propertyCardDataAtom(pId))); set(formDialogSchemaAtom, { id: name, name, requiredPropertyIds: [], optionalPropertyIds: [] }); }); - const setTemplates = useRecoilCallback(({ set }) => ({ templates }: { templates: string[] }) => { + const setTemplates = useRecoilCallback(({ set }) => ({ templates }: { templates: FormDialogSchemaTemplate[] }) => { set(formDialogTemplatesAtom, templates); }); + const updateLocale = useRecoilCallback(({ set }) => ({ locale }: { locale: string }) => { + set(formDialogLocaleAtom, locale); + }); + return { activatePropertyId, addProperty, - changePropertyExamples, - changePropertyKind, + changePropertyType, changePropertyName, - changePropertyPayload, + changePropertyCardData, changePropertyArray, reset, setTemplates, @@ -235,6 +289,7 @@ const getHandlers = () => { moveProperty, importSchema, importSchemaString, + updateLocale, }; }; diff --git a/Composer/packages/form-dialogs/src/atoms/types.ts b/Composer/packages/form-dialogs/src/atoms/types.ts index d819057ab8..c95e0c3b1e 100644 --- a/Composer/packages/form-dialogs/src/atoms/types.ts +++ b/Composer/packages/form-dialogs/src/atoms/types.ts @@ -3,16 +3,19 @@ export type PropertyRequiredKind = 'required' | 'optional'; -export type FormDialogPropertyKind = 'ref' | 'number' | 'integer' | 'string' | 'array'; +export type FormDialogPropertyKind = 'ref' | 'number' | 'integer' | 'string' | 'array' | 'boolean'; export type RefPropertyPayload = TypedPropertyPayload & { ref: string; }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type GenericPayloadData = Record; + export type TypedPropertyPayload = { kind: FormDialogPropertyKind; entities?: string[]; -}; +} & GenericPayloadData; const builtInFormats = ['date-time', 'date', 'time', 'email', 'uri', 'iri'] as const; @@ -29,6 +32,8 @@ export const builtInStringFormats: readonly StringFormatItem[] = [ { displayName: 'iri', value: 'iri' }, ]; +export type BooleanPropertyPayload = TypedPropertyPayload; + export type StringPropertyPayload = TypedPropertyPayload & { kind: 'string'; enums?: string[]; @@ -58,6 +63,7 @@ export type ArrayPropertyPayload = Pick & { }; export type FormDialogPropertyPayload = + | BooleanPropertyPayload | RefPropertyPayload | StringPropertyPayload | NumberPropertyPayload diff --git a/Composer/packages/form-dialogs/src/atoms/utils.ts b/Composer/packages/form-dialogs/src/atoms/utils.ts index c19b4f99bc..a3fd8306bd 100644 --- a/Composer/packages/form-dialogs/src/atoms/utils.ts +++ b/Composer/packages/form-dialogs/src/atoms/utils.ts @@ -3,99 +3,108 @@ /* eslint-disable @typescript-eslint/consistent-type-assertions */ -import formatMessage from 'format-message'; +import { FormDialogSchemaTemplate } from '@bfc/shared'; +import { PropertyCardData } from '../components/property/types'; import { generateId } from '../utils/base'; import { nameRegex } from '../utils/constants'; -import { - builtInStringFormats, - FormDialogProperty, - FormDialogPropertyKind, - FormDialogPropertyPayload, - IntegerPropertyPayload, - NumberPropertyPayload, - RefPropertyPayload, - StringPropertyPayload, - TypedPropertyPayload, -} from './types'; - -export const getDefaultPayload = (kind: FormDialogPropertyKind) => { - switch (kind) { - case 'ref': - return { kind: 'ref' }; - case 'string': - return { kind: 'string', entities: [] }; - case 'number': - return { kind: 'number', entities: [] }; - case 'integer': - return { kind: 'integer', entities: [] }; - default: - throw new Error(`Property type: "${kind}" is not supported!`); +export const templateTypeToJsonSchemaType = (cardData: PropertyCardData, templates: FormDialogSchemaTemplate[]) => { + const template = templates.find((t) => t.id === cardData.propertyType); + const isRef = template.type === 'object' && template.$template; + + if (isRef) { + return { kind: 'ref', ref: template.id }; } + + const hasEnum = !!cardData.enum; + if (hasEnum) { + return { kind: 'string', enums: true }; + } + + return { + kind: cardData.propertyType, + format: cardData.format, + }; }; const $refToRef = ($ref: string) => { - const [, ref] = $ref.match('template:(.*)'); - return ref; + const [, ref] = $ref.match(/^template:(.*?)(\.template)?$/); + // Lower case is necessary because in generator parser dereferencing always converts to lower case + return ref.toLowerCase(); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const retrievePayload = (kind: FormDialogPropertyKind, payloadData: any, array = false): FormDialogPropertyPayload => { - if (array) { - return retrievePayload(payloadData.items.type || 'ref', payloadData.items); - } - switch (kind) { - case 'ref': - return { ref: $refToRef(payloadData.$ref) }; - case 'string': - return { kind: 'string', entities: payloadData.$entities, enums: payloadData.enum }; +export const jsonSchemaTypeToTemplateType = ( + propertyJson: any, + templates: FormDialogSchemaTemplate[] +): { propertyType: string; isArray?: boolean } => { + const jsonType = propertyJson.type ?? 'ref'; + + switch (jsonType ?? 'ref') { + case 'array': { + return { ...jsonSchemaTypeToTemplateType(propertyJson.items, templates), isArray: true }; + } + case 'boolean': case 'number': - return { - kind: 'number', - minimum: payloadData.minimum, - maximum: payloadData.maximum, - }; case 'integer': - return { - kind: 'integer', - minimum: payloadData.minimum, - maximum: payloadData.maximum, - }; + return { propertyType: jsonType }; + case 'string': { + if (propertyJson.enum) { + return { propertyType: 'enum' }; + } + + if (propertyJson.format) { + const template = templates.find( + (template) => template.format === propertyJson.format && template.type === jsonType + ); + return { propertyType: template.id }; + } + + return { propertyType: 'string' }; + } + case 'ref': { + const ref = $refToRef(propertyJson.$ref); + + const template = templates.find((template) => template.id === ref); + + return { propertyType: template.id }; + } default: - throw new Error(`Property of type: ${kind} is not currently supported!`); + throw new Error(`${jsonType} is not supported!`); } }; -export const createSchemaStoreFromJson = (schemaName: string, jsonString: string) => { +export const createSchemaStoreFromJson = ( + name: string, + jsonString: string, + templates: FormDialogSchemaTemplate[] +): { name: string; properties: PropertyCardData[] } => { const json = JSON.parse(jsonString); - const properties = json.properties || []; + const propertiesJson = json.properties || []; const requiredArray = (json.required || []); - const examplesRecord = >(json.$examples || {}); - const propertyStores = Object.keys(properties).map((name) => { - const propertyData = properties[name]; + const properties = Object.keys(propertiesJson).map((name) => { + const { $examples, ...propertyJson } = propertiesJson[name]; - const propertyType = propertyData?.type || 'ref'; - const array = propertyType === 'array'; - const payload = retrievePayload(propertyType, propertyData, array); - const kind = (propertyType === 'array' ? payload.kind || 'ref' : propertyType); - const required = requiredArray.indexOf(name) !== -1; - const examples = examplesRecord[name] || []; + const { isArray, propertyType } = jsonSchemaTypeToTemplateType(propertyJson, templates); + const cardData = isArray ? propertyJson.items : propertyJson; + const isRequired = requiredArray.includes(name); + + delete propertyJson.type; return { id: generateId(), name, - kind, - required, - examples, - array, - payload, + propertyType, + isRequired, + isArray: !!isArray, + $examples, + ...cardData, }; }); - return { name: schemaName, properties: propertyStores }; + return { name, properties }; }; const findFirstMissingIndex = (arr: number[], start: number, end: number): number => { @@ -118,19 +127,16 @@ export const getDuplicateName = (name: string, allNames: readonly string[]) => { } const getBestIndex = (origName: string) => { - const pattern = `${origName} - copy `; - const otherNames = allNames.filter((n) => n.startsWith(pattern) && n.endsWith(')')); + const pattern = `${origName}-copy([0-9])+`; + // eslint-disable-next-line security/detect-non-literal-regexp + const regex = new RegExp(pattern); + const otherNames = allNames.filter((n) => regex.test(n)); const indices: number[] = []; for (const otherName of otherNames) { - const idx = otherName.indexOf(pattern); - const openPIdx = otherName.indexOf('(', idx); - const closePIdx = otherName.length - 1; - try { - if (openPIdx !== -1 && closePIdx !== -1) { - const otherIdx = parseInt(otherName.substring(openPIdx + 1, closePIdx), 10); - indices.push(otherIdx); - } + const { 1: otherIdxString } = otherName.match(regex); + const otherIdx = parseInt(otherIdxString); + indices.push(otherIdx); } catch { continue; } @@ -148,145 +154,66 @@ export const getDuplicateName = (name: string, allNames: readonly string[]) => { return firstAvailableIdx === -1 ? maxIdx + 1 : firstAvailableIdx + 1; }; - const cpIndex = name.indexOf(' - copy '); + const cpIndex = name.indexOf('-copy'); const originalName = cpIndex === -1 ? name : name.substring(0, cpIndex); const bestIndex = getBestIndex(originalName); - return `${originalName} - copy (${bestIndex})`; + return `${originalName}-copy${bestIndex}`; }; //----------------------------JSON spreading---------------------------- -const spreadEntities = (payload: TypedPropertyPayload) => - payload?.entities?.length ? { $entities: payload.entities } : {}; - -const spreadStringSchemaProperty = (payload: StringPropertyPayload) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const payloadJson: any = payload?.enums?.length ? { enum: payload.enums } : {}; - if (payload.format) { - payloadJson.format = payload.format; - } - - return payloadJson; -}; - -const spreadNumberSchemaProperty = (payload: NumberPropertyPayload | IntegerPropertyPayload) => { - return { minimum: payload.minimum, maximum: payload.maximum }; -}; - -const spreadRefSchemaProperty = (payload: RefPropertyPayload) => ({ $ref: `template:${payload.ref}` }); - -const spreadArraySchemaProperty = (payload: FormDialogPropertyPayload) => { - const helper = () => { - switch (payload.kind) { - case 'string': { - return { - type: 'string', - ...spreadStringSchemaProperty(payload), - }; - } - case 'number': { - return { - type: 'number', - ...spreadNumberSchemaProperty(payload), - }; - } - case 'integer': { - return { - type: 'integer', - ...spreadNumberSchemaProperty(payload), - }; - } - default: - case 'ref': - return spreadRefSchemaProperty(payload); - } - }; +const spreadCardDataNormal = (propertyType: string, cardValues: Record) => { + cardValues = cardValues.items ?? cardValues; return { - type: 'array', - items: helper(), + $ref: `template:${propertyType}`, + ...cardValues, }; }; -export const spreadSchemaPropertyStore = (property: FormDialogProperty) => { - if (property.array) { - return spreadArraySchemaProperty(property.payload); - } - switch (property.kind) { - case 'ref': - return spreadRefSchemaProperty(property.payload); - case 'string': - return { - type: property.kind, - ...spreadEntities(property.payload), - ...spreadStringSchemaProperty(property.payload), - }; - case 'number': - return { - type: property.kind, - ...spreadEntities(property.payload), - ...spreadNumberSchemaProperty(property.payload), - }; - case 'integer': - return { - type: property.kind, - ...spreadEntities(property.payload), - ...spreadNumberSchemaProperty(property.payload), +const spreadCardDataArray = (propertyType: string, cardValues: Record) => { + const wasArray = !!cardValues.items; + + return wasArray + ? { type: 'array', ...cardValues } + : { + type: 'array', + items: spreadCardDataNormal(propertyType, cardValues), }; - default: - throw new Error(`Property type: "${property.kind}" is not supported!`); - } }; -//----------------------------JSON validation---------------------------- +export const spreadCardData = (cardData: PropertyCardData) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, isArray, isRequired, propertyType, name, ...cardValues } = cardData; + const { $examples, ...restData } = cardValues; -export const validateSchemaPropertyStore = (property: FormDialogProperty) => { - let payloadValid = false; - switch (property.kind) { - case 'ref': - payloadValid = !!(property.payload).ref; - break; - case 'string': { - const stringPayload = property.payload; - payloadValid = !stringPayload.enums || !!stringPayload.enums.length; - break; - } - case 'number': { - const numberPayload = property.payload; - payloadValid = !numberPayload.minimum || !numberPayload.maximum || numberPayload.minimum <= numberPayload.maximum; - break; - } - case 'integer': { - const numberPayload = property.payload; - payloadValid = !numberPayload.minimum || !numberPayload.maximum || numberPayload.minimum <= numberPayload.maximum; - break; - } + if (isArray) { + return { ...spreadCardDataArray(propertyType, restData), $examples }; } - return !!(payloadValid && property.name && nameRegex.test(property.name)); + return { ...spreadCardDataNormal(propertyType, restData), $examples }; }; -export const getPropertyTypeDisplayName = (property: FormDialogProperty) => { - switch (property.kind) { - case 'number': - return formatMessage('number'); - case 'integer': - return formatMessage('integer'); - case 'ref': { - const refPayload = property.payload as RefPropertyPayload; - return refPayload.ref.split('.')[0]; - } - default: - case 'string': { - const stringPayload = property.payload as StringPropertyPayload; - if (stringPayload.enums) { - return formatMessage('list - {count} values', { count: stringPayload.enums.length }); - } +//----------------------------JSON validation---------------------------- - return stringPayload.format - ? builtInStringFormats.find((f) => f.value === stringPayload.format).displayName - : formatMessage('any string'); +// For now we assume all the values are optional +const shouldValidateData = false; +export const validateSchemaPropertyStore = (cardData: PropertyCardData, templates: FormDialogSchemaTemplate[]) => { + let dataValid = true; + if (shouldValidateData) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, isArray, isRequired, name, propertyType, ...cardValues } = cardData; + const template = templates.find((t) => t.id === propertyType); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { title, description, array, $examples, ...templateVariables } = template.$generator; + + for (const variable of Object.keys(templateVariables)) { + if (cardValues[variable] === undefined) { + dataValid = false; + } } } + + return dataValid && cardData.name && nameRegex.test(cardData.name); }; diff --git a/Composer/packages/form-dialogs/src/components/common/ValuePicker.tsx b/Composer/packages/form-dialogs/src/components/common/ValuePicker.tsx index e3323ad85c..91468871c7 100644 --- a/Composer/packages/form-dialogs/src/components/common/ValuePicker.tsx +++ b/Composer/packages/form-dialogs/src/components/common/ValuePicker.tsx @@ -6,13 +6,13 @@ import { Label } from 'office-ui-fabric-react/lib/Label'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import * as React from 'react'; -import { TagInput } from '../tags/TagInput'; +import { TagInput } from './tags/TagInput'; type Props = { /** * The label of the value picker control. */ - label: string; + label?: string; /** * Callback for custom rendering of label. */ @@ -34,11 +34,11 @@ const defaultLabelRender = (props: Props) => { }; export const ValuePicker = (props: Props) => { - const { values, onChange, onRenderLabel = defaultLabelRender } = props; + const { label, values, onChange, onRenderLabel = defaultLabelRender } = props; return ( - - {onRenderLabel(props, defaultLabelRender)} + + {label && onRenderLabel(props, defaultLabelRender)} ); diff --git a/Composer/packages/form-dialogs/src/components/tags/ContentEditable.tsx b/Composer/packages/form-dialogs/src/components/common/tags/ContentEditable.tsx similarity index 100% rename from Composer/packages/form-dialogs/src/components/tags/ContentEditable.tsx rename to Composer/packages/form-dialogs/src/components/common/tags/ContentEditable.tsx diff --git a/Composer/packages/form-dialogs/src/components/tags/Tag.tsx b/Composer/packages/form-dialogs/src/components/common/tags/Tag.tsx similarity index 100% rename from Composer/packages/form-dialogs/src/components/tags/Tag.tsx rename to Composer/packages/form-dialogs/src/components/common/tags/Tag.tsx diff --git a/Composer/packages/form-dialogs/src/components/tags/TagInput.tsx b/Composer/packages/form-dialogs/src/components/common/tags/TagInput.tsx similarity index 100% rename from Composer/packages/form-dialogs/src/components/tags/TagInput.tsx rename to Composer/packages/form-dialogs/src/components/common/tags/TagInput.tsx diff --git a/Composer/packages/form-dialogs/src/components/tags/utils.ts b/Composer/packages/form-dialogs/src/components/common/tags/utils.ts similarity index 100% rename from Composer/packages/form-dialogs/src/components/tags/utils.ts rename to Composer/packages/form-dialogs/src/components/common/tags/utils.ts diff --git a/Composer/packages/form-dialogs/src/components/property/AdvancedOptions.tsx b/Composer/packages/form-dialogs/src/components/property/AdvancedOptions.tsx deleted file mode 100644 index 9005ca238d..0000000000 --- a/Composer/packages/form-dialogs/src/components/property/AdvancedOptions.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import formatMessage from 'format-message'; -import { Stack } from 'office-ui-fabric-react/lib/Stack'; -import { Text } from 'office-ui-fabric-react/lib/Text'; -import * as React from 'react'; - -import { - ArrayPropertyPayload, - FormDialogPropertyPayload, - FormDialogProperty, - TypedPropertyPayload, -} from '../../atoms/types'; -import { ValuePicker } from '../common/ValuePicker'; -import { Collapsible } from '../common/Collapsible'; - -const getDefaultCollapsed = (property: FormDialogProperty) => { - const payload = - property.kind === 'array' - ? (property.payload as ArrayPropertyPayload).items - : (property.payload as TypedPropertyPayload); - return !(payload as TypedPropertyPayload).entities?.length && !property.examples?.length; -}; - -type Props = { - /** - * Property to show advanced options for. - */ - property: FormDialogProperty; - /** - * Callback to update the property payload that includes advanced options. - */ - onChangePayload: (payload: FormDialogPropertyPayload) => void; - /** - * Callback to update the examples for the property. - */ - onChangeExamples: (examples: readonly string[]) => void; -}; - -export const AdvancedOptions = (props: Props) => { - const { property, onChangePayload, onChangeExamples } = props; - - const defaultCollapsed = React.useMemo(() => getDefaultCollapsed(property), [property.id]); - - const payload = - property.kind === 'array' - ? (property.payload as ArrayPropertyPayload).items - : (property.payload as TypedPropertyPayload); - - return ( - {formatMessage('Advanced options')}} - > - - - onChangePayload({ - ...payload, - entities, - } as FormDialogPropertyPayload) - } - /> - - - - ); -}; diff --git a/Composer/packages/form-dialogs/src/components/property/FormDialogPropertyCard.tsx b/Composer/packages/form-dialogs/src/components/property/FormDialogPropertyCard.tsx index 093034cd49..eefa5f96a1 100644 --- a/Composer/packages/form-dialogs/src/components/property/FormDialogPropertyCard.tsx +++ b/Composer/packages/form-dialogs/src/components/property/FormDialogPropertyCard.tsx @@ -13,23 +13,16 @@ import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import * as React from 'react'; import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; +import { useRecoilValue } from 'recoil'; +import { formDialogTemplatesAtom } from '../../atoms/appState'; import { nameRegex } from '../../utils/constants'; import { FieldLabel } from '../common/FieldLabel'; -import { - ArrayPropertyPayload, - FormDialogProperty, - FormDialogPropertyKind, - FormDialogPropertyPayload, - IntegerPropertyPayload, - NumberPropertyPayload, - StringPropertyPayload, -} from '../../atoms/types'; -import { NumberPropertyContent } from './NumberPropertyContent'; +import { PropertyCardContent } from './PropertyCardContent'; import { PropertyTypeSelector } from './PropertyTypeSelector'; import { RequiredPriorityIndicator } from './RequiredPriorityIndicator'; -import { StringPropertyContent } from './StringPropertyContent'; +import { PropertyCardData } from './types'; const ContentRoot = styled.div(({ isValid }: { isValid: boolean }) => ({ width: 720, @@ -61,40 +54,18 @@ const ContentRoot = styled.div(({ isValid }: { isValid: boolean }) => ({ })); const ArrayCheckbox = styled(Checkbox)({ - flex: 1, marginTop: 28, - justifyContent: 'flex-end', + alignSelf: 'flex-end', }); -const isNumerical = (kind: FormDialogPropertyKind) => kind === 'integer' || kind === 'number'; - -const renderProperty = ( - kind: FormDialogPropertyKind, - payload: FormDialogPropertyPayload, - onChangePayload: (payload: FormDialogPropertyPayload) => void -): React.ReactNode => { - switch (kind) { - case 'string': - return ; - case 'number': - return ; - case 'integer': - return ; - case 'ref': - return null; - default: - throw new Error(`${kind} is not a known property to render!`); - } -}; - export type FormDialogPropertyCardProps = { valid: boolean; - property: FormDialogProperty; + propertyCardData: PropertyCardData; dragHandleProps: DraggableProvidedDragHandleProps; - onChangeKind: (kind: FormDialogPropertyKind, payload: FormDialogPropertyPayload) => void; + onChangePropertyType: (propertyType: string) => void; onChangeName: (name: string) => void; onChangeArray: (isArray: boolean) => void; - onChangePayload: (payload: FormDialogPropertyPayload) => void; + onChangeData: (data: Record) => void; onActivateItem: (propertyId: string) => void; onRemove: () => void; onDuplicate: () => void; @@ -103,20 +74,26 @@ export type FormDialogPropertyCardProps = { export const FormDialogPropertyCard = React.memo((props: FormDialogPropertyCardProps) => { const { valid, - property, + propertyCardData, dragHandleProps, - onChangeKind, + onChangePropertyType, onChangeName, - onChangePayload, onChangeArray, onActivateItem, onRemove, onDuplicate, + onChangeData, } = props; + const templates = useRecoilValue(formDialogTemplatesAtom); + const selectedTemplate = React.useMemo(() => templates.find((t) => t.id === propertyCardData.propertyType), [ + propertyCardData, + templates, + ]); + // Indicates if the form in the card has been touched by the user. - const touchedRef = React.useRef(!!property.name); - const { id: propertyId, array, kind, name, payload, required } = property; + const touchedRef = React.useRef(!!propertyCardData.name); + const { id: propertyId, name, isArray, isRequired, propertyType, ...cardValues } = propertyCardData; const rootElmRef = React.useRef(); const propertyNameTooltipId = useId('propertyName'); @@ -159,7 +136,7 @@ export const FormDialogPropertyCard = React.memo((props: FormDialogPropertyCardP (overflowItems: IOverflowSetItemProps[]) => ( - + - - + + - - - {isNumerical(kind) ? renderProperty(kind, payload, onChangePayload) : null} - - + + {selectedTemplate.$generator.array && ( + )} - /> + + + {selectedTemplate?.$generator.array?.uniqueItems && ( + + )} + - {!isNumerical(kind) ? renderProperty(kind, payload, onChangePayload) : null} + {selectedTemplate && ( + + )} ); diff --git a/Composer/packages/form-dialogs/src/components/property/NumberPropertyContent.tsx b/Composer/packages/form-dialogs/src/components/property/NumberPropertyContent.tsx deleted file mode 100644 index d8fd7bd2d4..0000000000 --- a/Composer/packages/form-dialogs/src/components/property/NumberPropertyContent.tsx +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { useId } from '@uifabric/react-hooks'; -import formatMessage from 'format-message'; -import { Stack } from 'office-ui-fabric-react/lib/Stack'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; -import * as React from 'react'; - -import { IntegerPropertyPayload, NumberPropertyPayload } from '../../atoms/types'; -import { FieldLabel } from '../common/FieldLabel'; - -type Props = { - payload: NumberPropertyPayload | IntegerPropertyPayload; - onChangePayload: (payload: NumberPropertyPayload | IntegerPropertyPayload) => void; -}; - -export const NumberPropertyContent = React.memo((props: Props) => { - const { payload, onChangePayload } = props; - - const minTooltipId = useId('numberMin'); - const maxTooltipId = useId('numberMax'); - - const onRenderLabel = React.useCallback( - (helpText: string, tooltipId: string) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (props: any, defaultRender?: (props: any) => JSX.Element | null) => ( - - ), - [] - ); - - return ( - - - - onChangePayload({ - ...payload, - minimum: payload.kind === 'integer' ? parseInt(value, 10) : parseFloat(value), - }) - } - onRenderLabel={onRenderLabel( - formatMessage( - 'Optional. Setting a minimum value enables your bot to reject a value that is too small and re-prompt the user for a new value.' - ), - minTooltipId - )} - /> - - onChangePayload({ - ...payload, - maximum: payload.kind === 'integer' ? parseInt(value, 10) : parseFloat(value), - }) - } - onRenderLabel={onRenderLabel( - formatMessage( - 'Optional. Setting a maximum value enables your bot to reject a value that is too large and re-prompt the user for a new value.' - ), - maxTooltipId - )} - /> - - - ); -}); diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyCardContent.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyCardContent.tsx new file mode 100644 index 0000000000..966692d227 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/PropertyCardContent.tsx @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { FormDialogSchemaTemplate } from '@bfc/shared'; +import { ComboBox } from 'office-ui-fabric-react/lib/ComboBox'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import * as React from 'react'; +import { useRecoilValue } from 'recoil'; + +import { formDialogLocale } from '../../atoms/appState'; +import { FieldLabel } from '../common/FieldLabel'; +import { ValuePicker } from '../common/ValuePicker'; + +import { EnumExampleContent } from './examples/EnumExampleContent'; +import { StringExampleContent } from './examples/StringExampleContent'; + +type Props = { + propertyName: string; + template: FormDialogSchemaTemplate; + cardValues: Record; + onDataChange: (data: Record) => void; +}; + +const renderField = (variable: string, info: Record, value: any, onChange: (value: any) => void) => { + const renderLabel = (helpText: string, tooltipId: string) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: any, defaultRender?: (props: any) => JSX.Element | null) => ( + + ); + + const convertValue = (value: string) => { + switch (info.type) { + case 'number': + return parseInt(value, 10); + default: + return value; + } + }; + + switch (info.type) { + case 'array': + return ( + + ); + case 'string': { + const hasEnum = !!info.enum; + + return hasEnum ? ( + ({ key: v, text: v }))} + selectedKey={value} + styles={{ root: { maxWidth: 320 }, optionsContainer: { maxHeight: 320 } }} + onChange={(_, option) => onChange(option.key)} + onRenderLabel={renderLabel(info.description, variable)} + /> + ) : ( + onChange(newValue)} + onRenderLabel={renderLabel(info.description, variable)} + /> + ); + } + default: + return ( + onChange(convertValue(newValue))} + onRenderLabel={renderLabel(info.description, variable)} + /> + ); + } +}; + +export const PropertyCardContent = (props: Props) => { + const { propertyName, template, cardValues, onDataChange } = props; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { title, description, array, $examples, ...templateInfo } = template.$generator; + const locale = useRecoilValue(formDialogLocale); + + const formFieldChange = (variable: string) => (value: any) => { + const newFormData = { ...cardValues, [variable]: value }; + onDataChange(newFormData); + }; + + return ( + + {Object.keys(templateInfo).map((variable) => ( + + {renderField(variable, templateInfo[variable], cardValues[variable], formFieldChange(variable))} + + ))} + {$examples && template.id === 'enum' && ( + + )} + {$examples && template.id === 'string' && ( + + )} + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx index 0e32090121..9cc4d273af 100644 --- a/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx +++ b/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx @@ -11,6 +11,7 @@ import { Droppable } from 'react-beautiful-dnd'; import { PropertyRequiredKind } from '../../atoms/types'; import { jsPropertyListClassName } from '../../utils/constants'; +import { HelpTooltip } from '../common/HelpTooltip'; import { PropertyListItem } from './PropertyListItem'; @@ -19,14 +20,16 @@ const Root = styled(Stack)({ }); const List = styled.div(({ isDraggingOver }: { isDraggingOver: boolean }) => ({ - margin: '24px 0', + margin: '0 0 24px 0', backgroundColor: isDraggingOver ? NeutralColors.gray40 : 'transparent', })); -const Header = styled(Text)({ - position: 'absolute', - left: 24, - top: 24, +const Header = styled(Stack)({ + height: 56, + width: 740, +}); + +const HeaderText = styled(Text)({ color: NeutralColors.gray130, fontWeight: 600, }); @@ -64,8 +67,20 @@ export const PropertyList = (props: Props) => { return ( -
- {kind === 'required' ? formatMessage('Required properties') : formatMessage('Optional properties')} +
+ + {kind === 'required' ? formatMessage('Required properties') : formatMessage('Optional properties')} + +
{(provided, { isDraggingOver }) => ( diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyListItem.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyListItem.tsx index 0c229c3481..a9e27aa61e 100644 --- a/Composer/packages/form-dialogs/src/components/property/PropertyListItem.tsx +++ b/Composer/packages/form-dialogs/src/components/property/PropertyListItem.tsx @@ -14,13 +14,17 @@ import * as React from 'react'; import { Draggable, DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; import { useRecoilValue } from 'recoil'; +import { + activePropertyIdAtom, + formDialogPropertyValidSelector, + formDialogTemplatesAtom, + propertyCardDataAtom, +} from '../../atoms/appState'; import { useHandlers } from '../../atoms/handlers'; -import { activePropertyIdAtom, formDialogPropertyAtom, formDialogPropertyValidSelector } from '../../atoms/appState'; -import { FormDialogProperty, FormDialogPropertyKind, FormDialogPropertyPayload } from '../../atoms/types'; -import { getPropertyTypeDisplayName } from '../../atoms/utils'; import { FormDialogPropertyCard } from './FormDialogPropertyCard'; import { RequiredPriorityIndicator } from './RequiredPriorityIndicator'; +import { PropertyCardData } from './types'; const ItemRoot = styled.div(({ isDragging }: { isDragging: boolean }) => ({ boxShadow: isDragging ? '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)' : null, @@ -66,18 +70,23 @@ const ErrorIcon = styled(Stack.Item)({ type ContentProps = { valid: boolean; - property: FormDialogProperty; + propertyCardData: PropertyCardData; dragHandleProps: DraggableProvidedDragHandleProps; onActivateItem: (propertyId: string) => void; }; const PropertyListItemContent = React.memo((props: ContentProps) => { - const { property, valid, dragHandleProps, onActivateItem } = props; + const { propertyCardData, valid, dragHandleProps, onActivateItem } = props; + const templates = useRecoilValue(formDialogTemplatesAtom); const tooltipId = useId('PropertyListItemContent'); + const { title: typeDisplayText, description: typeDisplayTitle } = React.useMemo( + () => templates.find((template) => template.id === propertyCardData.propertyType).$generator, + [templates, propertyCardData.propertyType] + ); const activateItem = React.useCallback(() => { - onActivateItem(property.id); - }, [onActivateItem, property.id]); + onActivateItem(propertyCardData.id); + }, [onActivateItem, propertyCardData.id]); const keyUp = React.useCallback( (e: React.KeyboardEvent) => { @@ -88,8 +97,6 @@ const PropertyListItemContent = React.memo((props: ContentProps) => { [activateItem] ); - const propertyTypeDisplayName = React.useMemo(() => getPropertyTypeDisplayName(property), [property]); - return ( { {!valid && } - {property.name || formatMessage('[no name]')} - - {propertyTypeDisplayName} + {propertyCardData.name || formatMessage('[no name]')} + + {typeDisplayText} - {property.array ? formatMessage('Accepts multiple values') : ''} - + + {propertyCardData.array ? formatMessage('Accepts multiple values') : ''} + + ); }); @@ -128,24 +137,24 @@ export const PropertyListItem = React.memo((props: Props) => { const { propertyId, index } = props; const activePropertyId = useRecoilValue(activePropertyIdAtom); - const property = useRecoilValue(formDialogPropertyAtom(propertyId)); + const propertyCardData = useRecoilValue(propertyCardDataAtom(propertyId)); const valid = useRecoilValue(formDialogPropertyValidSelector(propertyId)); const { - changePropertyKind, + changePropertyType, changePropertyName, - changePropertyPayload, + changePropertyCardData, changePropertyArray, activatePropertyId, removeProperty, duplicateProperty, } = useHandlers(); - const onChangePropertyKind = React.useCallback( - (kind: FormDialogPropertyKind, payload: FormDialogPropertyPayload) => { - changePropertyKind({ id: propertyId, kind, payload }); + const onChangePropertyType = React.useCallback( + (propertyType: string) => { + changePropertyType({ id: propertyId, propertyType }); }, - [changePropertyKind, propertyId] + [changePropertyType, propertyId] ); const onChangePropertyName = React.useCallback( @@ -155,11 +164,11 @@ export const PropertyListItem = React.memo((props: Props) => { [changePropertyName, propertyId] ); - const onChangePayload = React.useCallback( - (payload: FormDialogPropertyPayload) => { - changePropertyPayload({ id: propertyId, payload }); + const onChangeData = React.useCallback( + (data: Record) => { + changePropertyCardData({ id: propertyId, data }); }, - [changePropertyPayload, propertyId] + [changePropertyCardData, propertyId] ); const onChangeArray = React.useCallback( @@ -179,8 +188,8 @@ export const PropertyListItem = React.memo((props: Props) => { const onRemove = React.useCallback(async () => { const res = await OpenConfirmModal( formatMessage('Delete property?'), - property.name - ? formatMessage('Are you sure you want to remove "{propertyName}"?', { propertyName: property.name }) + propertyCardData.name + ? formatMessage('Are you sure you want to remove "{propertyName}"?', { propertyName: propertyCardData.name }) : formatMessage('Are you sure you want to remove this property?') ); if (res) { @@ -200,20 +209,20 @@ export const PropertyListItem = React.memo((props: Props) => { {propertyId === activePropertyId ? ( ) : ( diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyTypeSelector.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyTypeSelector.tsx index 4e2916fa5b..a29d58c82e 100644 --- a/Composer/packages/form-dialogs/src/components/property/PropertyTypeSelector.tsx +++ b/Composer/packages/form-dialogs/src/components/property/PropertyTypeSelector.tsx @@ -3,161 +3,46 @@ import { useId } from '@uifabric/react-hooks'; import formatMessage from 'format-message'; -import { Dropdown, DropdownMenuItemType, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import * as React from 'react'; import { useRecoilValue } from 'recoil'; -import { - ArrayPropertyPayload, - BuiltInStringFormat, - builtInStringFormats, - FormDialogPropertyPayload, - IntegerPropertyPayload, - NumberPropertyPayload, - RefPropertyPayload, - FormDialogPropertyKind, - StringPropertyPayload, -} from '../../atoms/types'; -import { FieldLabel } from '../common/FieldLabel'; import { formDialogTemplatesAtom } from '../../atoms/appState'; - -const processSelection = ( - isArray: boolean, - oldKind: FormDialogPropertyKind, - newKind: FormDialogPropertyKind, - selectedKey: string, - payload: FormDialogPropertyPayload -): FormDialogPropertyPayload => { - if (isArray) { - return processSelection( - false, - (payload as ArrayPropertyPayload).items.kind, - newKind, - selectedKey, - (payload as ArrayPropertyPayload).items - ); - } - - // If the property type hasn't changed, reset the payload and update the type required payload (string => format, ref => $ref) - if (oldKind === newKind) { - switch (newKind) { - case 'string': - return { - ...payload, - format: - selectedKey !== 'string' && selectedKey !== 'enums' ? (selectedKey as BuiltInStringFormat) : undefined, - enums: selectedKey === 'enums' ? [] : undefined, - } as StringPropertyPayload; - case 'number': - return { kind: 'number' } as NumberPropertyPayload; - case 'integer': - return { kind: 'integer' } as IntegerPropertyPayload; - case 'ref': - return { ...payload, ref: selectedKey } as RefPropertyPayload; - } - } else { - switch (newKind) { - case 'string': - return { - kind: newKind, - format: - selectedKey !== 'string' && selectedKey !== 'enums' ? (selectedKey as BuiltInStringFormat) : undefined, - enums: selectedKey === 'enums' ? [] : undefined, - } as StringPropertyPayload; - case 'number': - return { kind: newKind } as NumberPropertyPayload; - case 'integer': - return { kind: newKind } as IntegerPropertyPayload; - case 'ref': - return { kind: newKind, ref: selectedKey } as RefPropertyPayload; - } - } -}; +import { FieldLabel } from '../common/FieldLabel'; type Props = { - isArray: boolean; - kind: FormDialogPropertyKind; - payload: FormDialogPropertyPayload; - onChange: (kind: FormDialogPropertyKind, payload?: FormDialogPropertyPayload) => void; + selectedPropertyType: string; + onChange: (propertyType: string) => void; }; export const PropertyTypeSelector = React.memo((props: Props) => { - const { isArray, kind, payload, onChange } = props; + const { selectedPropertyType, onChange } = props; const propertyTypeTooltipId = useId('propertyType'); - const isEnumList = kind === 'string' && !!(payload as StringPropertyPayload).enums; const templates = useRecoilValue(formDialogTemplatesAtom); - const templateOptions = React.useMemo( + const options = React.useMemo( () => - templates.map((t) => ({ - key: t, - text: t, - selected: kind === 'ref' && (payload as RefPropertyPayload).ref === t, - data: 'ref', - })), - [templates, payload, kind] - ); - - const stringOptions = React.useMemo( - () => - builtInStringFormats.map((builtInFormat) => ({ - key: builtInFormat.value, - text: builtInFormat.displayName, - selected: kind === 'string' && (payload as StringPropertyPayload).format === builtInFormat.value, - data: 'string', - })), - [payload, kind] - ); - - const dynamicOptions = React.useMemo( - () => - [ - { key: 'number', text: formatMessage('number'), selected: kind === 'number', data: 'number' }, - { key: 'integer', text: formatMessage('integer'), selected: kind === 'integer', data: 'integer' }, - { - key: 'string', - text: formatMessage('any string'), - selected: kind === 'string' && !(payload as StringPropertyPayload).format, - data: 'string', + templates.map((template) => ({ + key: template.id, + text: template.$generator.title, + title: template.$generator.description, + selected: selectedPropertyType === template.id, + data: { + template, }, - ...stringOptions, - ...templateOptions, - ].sort((a, b) => a.text.localeCompare(b.text)) as IDropdownOption[], - [kind, stringOptions, templateOptions] + })), + [templates, selectedPropertyType] ); - const options = React.useMemo(() => { - return [ - { - key: 'enums', - text: formatMessage('list'), - selected: isEnumList, - data: 'string', - } as IDropdownOption, - { - key: 'divider1', - itemType: DropdownMenuItemType.Divider, - } as IDropdownOption, - { - key: 'header1', - itemType: DropdownMenuItemType.Header, - text: formatMessage('Define by value type'), - } as IDropdownOption, - ...dynamicOptions, - ]; - }, [isEnumList, dynamicOptions]); - const selectedKey = React.useMemo(() => options.find((o) => o.selected).key, [options]); const change = React.useCallback( (_: React.FormEvent, option: IDropdownOption) => { - const newKind = option.data as FormDialogPropertyKind; - const selectedKey = option.key as string; - const newPayload = processSelection(isArray, kind, newKind, selectedKey, payload); - onChange(newKind, newPayload); + const newPropertyType = option.key as string; + onChange(newPropertyType); }, - [payload, isArray, kind, onChange] + [onChange] ); const onRenderLabel = React.useCallback( diff --git a/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx b/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx index c8fe1893ed..3dff0940ca 100644 --- a/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx +++ b/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx @@ -9,9 +9,10 @@ import * as React from 'react'; import { useRecoilValue } from 'recoil'; import { formDialogSchemaAtom } from '../../atoms/appState'; +import { HelpTooltip } from '../common/HelpTooltip'; const Root = styled(Stack)({ - width: 140, + width: 150, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', @@ -42,13 +43,26 @@ export const RequiredPriorityIndicator = React.memo((props: Props) => { const content = React.useMemo( () => required - ? formatMessage('{requiredText} | Priority: {priority}', { requiredText, priority }) + ? formatMessage.rich( + '{requiredText} | Priority Priority refers to the order in which the bot will ask for the required properties. : {priority}', + { + requiredText, + priority, + help: ({ children }) => ( + + ), + } + ) : formatMessage('{requiredText}', { requiredText }), [requiredText, priority] ); return ( - + {{content}} ); diff --git a/Composer/packages/form-dialogs/src/components/property/StringPropertyContent.tsx b/Composer/packages/form-dialogs/src/components/property/StringPropertyContent.tsx deleted file mode 100644 index 94334c442a..0000000000 --- a/Composer/packages/form-dialogs/src/components/property/StringPropertyContent.tsx +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { useId } from '@uifabric/react-hooks'; -import formatMessage from 'format-message'; -import * as React from 'react'; - -import { StringPropertyPayload } from '../../atoms/types'; -import { FieldLabel } from '../common/FieldLabel'; -import { ValuePicker } from '../common/ValuePicker'; - -type Props = { - payload: StringPropertyPayload; - onChangePayload: (payload: StringPropertyPayload) => void; -}; - -export const StringPropertyContent = React.memo((props: Props) => { - const { payload, onChangePayload } = props; - - const tooltipId = useId('enumValues'); - - const changeEnum = (enums: string[]) => { - onChangePayload({ ...payload, enums }); - }; - - const onRenderLabel = React.useCallback( - (helpText: string, tooltipId: string) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (props: any, defaultRender?: (props: any) => JSX.Element | null) => ( - - ), - [] - ); - - return ( - payload.enums && ( - - ) - ); -}); diff --git a/Composer/packages/form-dialogs/src/components/property/examples/EnumExampleContent.tsx b/Composer/packages/form-dialogs/src/components/property/examples/EnumExampleContent.tsx new file mode 100644 index 0000000000..36c24d0252 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/examples/EnumExampleContent.tsx @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { Label } from 'office-ui-fabric-react/lib/Label'; +import styled from '@emotion/styled'; +import { FluentTheme } from '@uifabric/fluent-theme'; +import formatMessage from 'format-message'; + +import { FieldLabel } from '../../common/FieldLabel'; + +import { EnumExampleList } from './EnumExampleList'; + +const Container = styled(Stack)(({ disabled }: { disabled: boolean }) => ({ + position: 'relative', + '&:before': disabled + ? { + position: 'absolute', + left: 0, + top: 0, + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: FluentTheme.palette.red, + fontSize: FluentTheme.fonts.small.fontSize, + content: `${formatMessage('Please add a name to this property to enable adding examples')}`, + zIndex: 1, + } + : null, + '&:after': disabled + ? { + position: 'absolute', + left: 0, + top: 0, + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.75)', + content: '""', + } + : null, +})); + +type Props = { + propertyName?: string; + locale: string; + $examples: any; + enums: string[]; + exampleData: Record>>; + onChange: (value: Record>>) => void; +}; + +export const EnumExampleContent = (props: Props) => { + const { + locale: currentLocale, + propertyName = '', + $examples: { title, description, ...rest }, + enums, + exampleData, + onChange, + } = props; + + const entityName = React.useMemo( + () => + exampleData[currentLocale] && Object.keys(exampleData[currentLocale]) + ? Object.keys(exampleData[currentLocale])[0] + : `${propertyName}Value`, + [exampleData, currentLocale, propertyName] + ); + + const addLocale = () => { + onChange({ ...exampleData, [currentLocale]: { [entityName]: {} } }); + }; + + return ( + + + {title}} helpText={description} tooltipId="examples" /> + + + + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/property/examples/EnumExampleList.tsx b/Composer/packages/form-dialogs/src/components/property/examples/EnumExampleList.tsx new file mode 100644 index 0000000000..54f1f25623 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/examples/EnumExampleList.tsx @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { OpenConfirmModal } from '@bfc/ui-shared'; +import formatMessage from 'format-message'; +import cloneDeep from 'lodash/cloneDeep'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { Label } from 'office-ui-fabric-react/lib/Label'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { Text } from 'office-ui-fabric-react/lib/Text'; +import React from 'react'; +import styled from '@emotion/styled'; + +import { FieldLabel } from '../../common/FieldLabel'; +import { ValuePicker } from '../../common/ValuePicker'; + +const ValuePickerContainer = styled.div({ + display: 'flex', + flex: 3, + alignItems: 'center', +}); + +type ExampleData = Record>>; +type ListItemData = { + word: string; + synonyms: string[]; +}; + +const Row = ({ item, onEdit }: { item: ListItemData; onEdit: () => void }) => { + return ( + + {item.word} + + {(item.synonyms ?? []).join(', ')} + + + + ); +}; + +const EditableRow = ({ + item, + onChange, + onSave, +}: { + item: ListItemData; + onChange: (word: string, synonyms: string[]) => void; + onSave: () => void; +}) => { + const valuePickerContainerRef = React.useRef(); + const valuePickerInputRef = React.useRef(); + + const synonymsChange = (synonyms: string[]) => onChange(item.word, synonyms); + + React.useEffect(() => { + valuePickerInputRef.current = valuePickerContainerRef.current.querySelector('input'); + valuePickerInputRef.current.focus(); + + const keydown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onSave(); + } + }; + + valuePickerInputRef.current.addEventListener('keydown', keydown); + + return () => valuePickerInputRef.current.removeEventListener('keydown', keydown); + }, []); + + return ( + + + {item.word} + + + + + + + ); +}; + +type Props = { + entityName: string; + $localeGenerator: Record; + exampleData?: ExampleData; + onChange: (exampleData: ExampleData) => void; +}; + +export const EnumExampleList = (props: Props) => { + const { entityName, $localeGenerator, exampleData = {}, onChange } = props; + + const [activeRowIndex, setActiveRowIndex] = React.useState(-1); + + const itemsRecord = React.useMemo>(() => { + return Object.keys(exampleData).reduce((a, locale) => { + a[locale] = + Object.keys(exampleData[locale]?.[entityName])?.reduce((b, word) => { + b.push({ word, synonyms: exampleData[locale]?.[entityName]?.[word] ?? [] }); + return b; + }, [] as ListItemData[]) ?? []; + + return a; + }, {} as Record); + }, [exampleData, entityName]); + + const rowChange = (locale: string, idx: number) => (word: string, synonyms: string[]) => { + const newExampleData = cloneDeep(exampleData); + const changingKey = Object.keys(newExampleData[locale][entityName])[idx]; + newExampleData[locale][entityName][word] = synonyms; + if (changingKey === '') { + delete newExampleData[locale][entityName][changingKey]; + } + + onChange(newExampleData); + }; + + const deleteLocale = (locale: string) => async () => { + const confirm = await OpenConfirmModal( + formatMessage('Delete "{locale}" examples?', { locale }), + formatMessage('Are you sure you want to remove examples for "{locale}" locale?', { locale }) + ); + if (confirm) { + const newExampleData = { ...exampleData }; + delete newExampleData[locale]; + onChange(newExampleData); + } + }; + + const editRow = (index: number) => () => setActiveRowIndex(index); + + const saveRow = () => setActiveRowIndex(-1); + + const renderCell = (locale: string) => (item: ListItemData, idx: number) => { + return ( + + {idx === activeRowIndex ? ( + + ) : ( + + )} + + ); + }; + + return ( + + {Object.keys(itemsRecord).map((locale) => ( + + + {`${$localeGenerator.title}: ${locale || formatMessage('All')}`}} + helpText={$localeGenerator.description} + tooltipId={locale} + /> + + + + + {formatMessage('Accepted values')}} + helpText={'Enum value'} + tooltipId="synonyms_header" + /> + + + {formatMessage('Synonyms')}} + helpText={formatMessage('Enum examples for each possible value.')} + tooltipId="synonyms_header" + /> + + + {itemsRecord[locale].map(renderCell(locale))} + + ))} + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/property/examples/StringExampleContent.tsx b/Composer/packages/form-dialogs/src/components/property/examples/StringExampleContent.tsx new file mode 100644 index 0000000000..4b602ce7e8 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/examples/StringExampleContent.tsx @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { OpenConfirmModal } from '@bfc/ui-shared'; +import styled from '@emotion/styled'; +import { FluentTheme } from '@uifabric/fluent-theme'; +import formatMessage from 'format-message'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { Label } from 'office-ui-fabric-react/lib/Label'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import * as React from 'react'; + +import { FieldLabel } from '../../common/FieldLabel'; +import { ValuePicker } from '../../common/ValuePicker'; + +const Container = styled(Stack)(({ disabled }: { disabled: boolean }) => ({ + position: 'relative', + '&:before': disabled + ? { + position: 'absolute', + left: 0, + top: 0, + height: '100%', + width: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: FluentTheme.palette.red, + fontSize: FluentTheme.fonts.small.fontSize, + content: `${formatMessage('Please add a name to this property to enable adding examples')}`, + zIndex: 1, + } + : null, + '&:after': disabled + ? { + position: 'absolute', + left: 0, + top: 0, + height: '100%', + width: '100%', + background: 'rgba(255, 255, 255, 0.75)', + content: '""', + } + : null, +})); + +type Props = { + locale: string; + propertyType: string; + $examples: any; + exampleData: Record>; + onChange: (value: Record>) => void; +}; + +export const StringExampleContent = (props: Props) => { + const { + locale: currentLocale, + propertyType, + $examples: { title, description, ...$localeGenerator }, + exampleData, + onChange, + } = props; + + const entityName = React.useMemo( + () => + exampleData[currentLocale] && Object.keys(exampleData[currentLocale]) + ? Object.keys(exampleData[currentLocale])[0] + : propertyType, + [exampleData, propertyType, currentLocale] + ); + + const addLocale = () => { + onChange({ ...exampleData, [currentLocale]: { [entityName]: [] } }); + }; + + const deleteLocale = (locale: string) => async () => { + const confirm = await OpenConfirmModal( + formatMessage('Delete "{locale}" examples?', { locale }), + formatMessage('Are you sure you want to remove examples for "{locale}" locale?', { locale }) + ); + if (confirm) { + const newExampleData = { ...exampleData }; + delete newExampleData[locale]; + onChange(newExampleData); + } + }; + + const changeValues = (values: string[]) => { + onChange({ ...exampleData, [currentLocale]: { [entityName]: values } }); + }; + + const renderLabel = (helpText: string, tooltipId: string) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (props: any, defaultRender?: (props: any) => JSX.Element | null) => ( + + ); + + return ( + + + {title}} helpText={description} tooltipId="examples" /> + + + {Object.keys(exampleData).map((locale) => ( + + + {`${$localeGenerator.additionalProperties.title}: ${locale || formatMessage('All')}`} + } + helpText={$localeGenerator.additionalProperties.description} + tooltipId={locale} + /> + + + {exampleData?.[locale]?.[propertyType] && ( + + )} + + ))} + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/property/types.ts b/Composer/packages/form-dialogs/src/components/property/types.ts new file mode 100644 index 0000000000..587194a513 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/types.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type PropertyCardData = { + id: string; + name: string; + isArray: boolean; + isRequired: boolean; + propertyType: string; +} & Record; diff --git a/Composer/packages/form-dialogs/src/undo/UndoRoot.tsx b/Composer/packages/form-dialogs/src/undo/UndoRoot.tsx index 8678eb40f4..e7cd59e3a8 100644 --- a/Composer/packages/form-dialogs/src/undo/UndoRoot.tsx +++ b/Composer/packages/form-dialogs/src/undo/UndoRoot.tsx @@ -25,22 +25,26 @@ const getAtomMap = (snapshot: Snapshot, trackedAtoms: RecoilState[]): AtomM }; const didAtomMapsChange = (prevMap: AtomMap, currMap: AtomMap) => { + if (prevMap.size !== currMap.size) { + return true; + } + for (const key of prevMap.keys()) { if (!currMap.has(key)) { return true; } - const prevVal = prevMap.get(key); - const currVal = currMap.get(key); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const prevVal = prevMap.get(key)!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const currVal = currMap.get(key)!; - if (prevVal && currVal) { - if (prevVal.state !== currVal.state) { - return true; - } + if (prevVal.state !== currVal.state) { + return true; + } - if (prevVal.state === 'hasValue' && currVal.state === 'hasValue' && prevVal.contents !== currVal.contents) { - return true; - } + if (prevVal.state === 'hasValue' && currVal.state === 'hasValue' && prevVal.contents !== currVal.contents) { + return true; } } @@ -90,7 +94,7 @@ export const UndoRoot = React.memo(({ trackedAtoms, children }: Props) => { const { 0: history, 1: setHistory } = React.useState({ past: [], - present: { snapshot: currentSnapshot, trackedAtoms }, + present: currentSnapshot, future: [], }); @@ -107,55 +111,55 @@ export const UndoRoot = React.memo(({ trackedAtoms, children }: Props) => { const currMap = getAtomMap(snapshot, trackedAtoms); if (!didAtomMapsChange(prevMap, currMap)) { - setHistory({ ...history, present: { snapshot, trackedAtoms } }); + setHistory({ ...history, present: snapshot }); return; } } setHistory({ - past: [...history.past, { snapshot: previousSnapshot, trackedAtoms }], - present: { snapshot, trackedAtoms }, + past: [...history.past, previousSnapshot], + present: snapshot, future: [], }); }); const undo = React.useCallback(() => { - setHistory((history: History) => { - if (!history.past.length) { - return history; + setHistory((currentHistory: History) => { + if (!currentHistory.past.length) { + return currentHistory; } isUndoingRef.current = true; - const target = history.past[history.past.length - 1]; - const { present } = history; - const newPresent = mapTrackedAtomsOntoSnapshot(present.snapshot, target.snapshot, target.trackedAtoms); + const target = currentHistory.past[currentHistory.past.length - 1]; + const { present } = currentHistory; + const newPresent = mapTrackedAtomsOntoSnapshot(present, target, trackedAtoms); gotoSnapshot(newPresent); return { - past: history.past.slice(0, history.past.length - 1), - present: { snapshot: newPresent, trackedAtoms }, - future: [history.present, ...history.future], + past: currentHistory.past.slice(0, currentHistory.past.length - 1), + present: newPresent, + future: [currentHistory.present, ...currentHistory.future], }; }); }, [setHistory, gotoSnapshot, trackedAtoms]); const redo = React.useCallback(() => { - setHistory((history: History) => { - if (!history.future.length) { - return history; + setHistory((currentHistory: History) => { + if (!currentHistory.future.length) { + return currentHistory; } isUndoingRef.current = true; - const target = history.future[0]; - const { present } = history; - const newPresent = mapTrackedAtomsOntoSnapshot(present.snapshot, target.snapshot, target.trackedAtoms); + const target = currentHistory.future[0]; + const { present } = currentHistory; + const newPresent = mapTrackedAtomsOntoSnapshot(present, target, trackedAtoms); gotoSnapshot(newPresent); return { - past: [...history.past, history.present], - present: { snapshot: newPresent, trackedAtoms }, - future: history.future.slice(1), + past: [...currentHistory.past, currentHistory.present], + present: newPresent, + future: currentHistory.future.slice(1), }; }); }, [setHistory, gotoSnapshot, trackedAtoms]); diff --git a/Composer/packages/form-dialogs/src/undo/types.ts b/Composer/packages/form-dialogs/src/undo/types.ts index 16c524a297..4bbef1e3fb 100644 --- a/Composer/packages/form-dialogs/src/undo/types.ts +++ b/Composer/packages/form-dialogs/src/undo/types.ts @@ -6,9 +6,9 @@ import { RecoilState, Snapshot, Loadable } from 'recoil'; export type History = { - past: { snapshot: Snapshot; trackedAtoms: RecoilState[] }[]; - present: { snapshot: Snapshot; trackedAtoms: RecoilState[] }; - future: { snapshot: Snapshot; trackedAtoms: RecoilState[] }[]; + past: Snapshot[]; + present: Snapshot; + future: Snapshot[]; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchemaMocks.ts b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchemaMocks.ts index e13b92d786..5b2344a511 100644 --- a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchemaMocks.ts +++ b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchemaMocks.ts @@ -212,58 +212,47 @@ export const SwitchConditionSchema: JSONSchema7 = { }; export const SetPropertiesSchema: JSONSchema7 = { - "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", - "$role": "implements(Microsoft.IDialog)", - "title": "Set property", - "description": "Set one or more property values.", - "type": "object", - "required": [ - "assignments" - ], - "properties": { - "id": { - "type": "string", - "title": "Id", - "description": "Optional id for the dialog" + $schema: 'https://schemas.botframework.com/schemas/component/v1.0/component.schema', + $role: 'implements(Microsoft.IDialog)', + title: 'Set property', + description: 'Set one or more property values.', + type: 'object', + required: ['assignments'], + properties: { + id: { + type: 'string', + title: 'Id', + description: 'Optional id for the dialog', }, - "disabled": { - "$ref": "schema:#/definitions/booleanExpression", - "title": "Disabled", - "description": "Optional condition which if true will disable this action.", - "examples": [ - true, - "=user.age > 3" - ] + disabled: { + $ref: 'schema:#/definitions/booleanExpression', + title: 'Disabled', + description: 'Optional condition which if true will disable this action.', + examples: [true, '=user.age > 3'], }, - "assignments": { - "type": "array", - "title": "Assignments", - "description": "Property value assignments to set.", - "items": { - "type": "object", - "title": "Assignment", - "description": "Property assignment.", - "properties": { - "property": { - "$ref": "schema:#/definitions/stringExpression", - "title": "Property", - "description": "Property (named location to store information).", - "examples": [ - "user.age" - ] + assignments: { + type: 'array', + title: 'Assignments', + description: 'Property value assignments to set.', + items: { + type: 'object', + title: 'Assignment', + description: 'Property assignment.', + properties: { + property: { + $ref: 'schema:#/definitions/stringExpression', + title: 'Property', + description: 'Property (named location to store information).', + examples: ['user.age'], + }, + value: { + $ref: 'schema:#/definitions/valueExpression', + title: 'Value', + description: 'New value or expression.', + examples: ["='milk'", '=dialog.favColor', "=dialog.favColor == 'red'"], }, - "value": { - "$ref": "schema:#/definitions/valueExpression", - "title": "Value", - "description": "New value or expression.", - "examples": [ - "='milk'", - "=dialog.favColor", - "=dialog.favColor == 'red'" - ] - } - } - } - } - } -} + }, + }, + }, + }, +}; diff --git a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/schemaUtils.test.ts b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/schemaUtils.test.ts index 8020e3eaa1..2cc1c09740 100644 --- a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/schemaUtils.test.ts +++ b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/schemaUtils.test.ts @@ -38,11 +38,9 @@ describe('#schemaUtils', () => { $designer: { id: 'sJzdQm', }, - assignments: [ - { property: 'username', value: 'test' } - ] + assignments: [{ property: 'username', value: 'test' }], }; expect(discoverNestedPaths(setPropertiesStub, SetPropertiesSchema)).toEqual([]); - }) + }); }); diff --git a/Composer/packages/lib/indexers/src/groupTriggers.ts b/Composer/packages/lib/indexers/src/groupTriggers.ts index 0d8a0ccc58..a24e3913a4 100644 --- a/Composer/packages/lib/indexers/src/groupTriggers.ts +++ b/Composer/packages/lib/indexers/src/groupTriggers.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { DialogInfo, ITrigger } from '@bfc/shared'; -import { ExpressionParser } from 'adaptive-expressions'; import uniq from 'lodash/uniq'; export const NoGroupingTriggerGroupName = '(none)'; @@ -12,31 +11,18 @@ const getPropertyReferences = (content: any) => { const foundProperties: string[] = []; if (content) { - // has $designer: { "propertyGroups": ["", "", "" - if (content.property && typeof content.property === 'string') { + } else if (content.property && typeof content.property === 'string') { + // has "property": "" foundProperties.push(content.property); - } - - // had "expectedProperties" : ["", "" - if (content.condition) { - try { - const expressionParser = new ExpressionParser(); - const expression = expressionParser.parse(content.condition); - const references = expression.references().map((r) => (r.startsWith('$') ? r.substring(1) : r)); - foundProperties.push(...references); - } catch (err) { - // eslint-disable-next-line no-console - console.error(`Could not parse condition expression ${content.condition}. ${err}`); + } else if (content.actions && Array.isArray(content.actions)) { + // extract from action expectedProperties + for (const action of content.actions) { + if (action.expectedProperties && Array.isArray(action.expectedProperties)) { + foundProperties.push(...action.expectedProperties); + } } } } @@ -45,18 +31,8 @@ const getPropertyReferences = (content: any) => { }; const getTriggerPropertyReferences = (trigger: ITrigger, isValidProperty: (name: string) => boolean) => { - const content = trigger.content; - - // inspect trigger + // inspect trigger and actions const foundProperties: string[] = getPropertyReferences(trigger.content); - - // inspect actions - if (content.actions && Array.isArray(content.actions)) { - for (let i = 0; i < content.actions.length; i++) { - foundProperties.push(...getPropertyReferences(content.actions[i])); - } - } - const result = uniq(foundProperties).filter(isValidProperty); if (result.length === 0) { @@ -70,10 +46,8 @@ const getTriggerPropertyReferences = (trigger: ITrigger, isValidProperty: (name: * Groups triggers by the property name they reference in: * - $designer: { "propertyGroups": ["", "" - * - "expectedProperties" : ["", "" - * - Any of the trigger's action that reference a property. - * If a trigger does not reference a property, it will be grouped under "(none)" + * - Action.expectedProperties : ["", " { addResult(p, t); }); - } else if (properties.length === 1) { + } else if (properties.length >= 1) { addResult(properties[0], t); } }); diff --git a/Composer/packages/lib/ui-shared/package.json b/Composer/packages/lib/ui-shared/package.json index 8c7b1f6cad..1a7ce0e881 100644 --- a/Composer/packages/lib/ui-shared/package.json +++ b/Composer/packages/lib/ui-shared/package.json @@ -14,7 +14,6 @@ "test": "jest", "prepare": "yarn build", "watch": "yarn build --watch" - }, "keywords": [ "botframework", diff --git a/Composer/packages/server/package.json b/Composer/packages/server/package.json index db248d1615..08a2a7f120 100644 --- a/Composer/packages/server/package.json +++ b/Composer/packages/server/package.json @@ -79,7 +79,7 @@ "@botframework-composer/types": "*", "@microsoft/bf-dialog": "4.13.1", "@microsoft/bf-dispatcher": "^4.11.0-beta.20201016.393c6b2", - "@microsoft/bf-generate-library": "^4.10.0-daily.20210317.224602", + "@microsoft/bf-generate-library": "^4.14.0-preview.20210605.ff079e5", "@microsoft/bf-lu": "4.14.0-dev.20210602.be805a8", "@microsoft/bf-orchestrator": "4.13.0-rc0", "applicationinsights": "^1.8.7", diff --git a/Composer/packages/server/src/controllers/formDialog.ts b/Composer/packages/server/src/controllers/formDialog.ts index 82a879e1c4..2b4ac33240 100644 --- a/Composer/packages/server/src/controllers/formDialog.ts +++ b/Composer/packages/server/src/controllers/formDialog.ts @@ -44,32 +44,12 @@ const expandJsonSchemaProperty = async (req: Request, res: Response) => { } }; -const skipSchemas: string[] = [ - 'boolean', - 'date-time', - 'date', - 'email', - 'enum', - 'integer', - 'iri', - 'number', - 'string', - 'time', - 'uri', -]; const getTemplateSchemas = async (req: Request, res: Response) => { const templatesDirs = await getTemplateDirs(); const result = await schemas(templatesDirs); if (result !== undefined) { - // TODO: Filter out base type schemas for now - const filteredSchemas = {}; - for (const [name, definition] of Object.entries(result)) { - if (!skipSchemas.includes(name)) { - filteredSchemas[name] = definition; - } - } - res.status(200).json(filteredSchemas); + res.status(200).json(result); } else { res.status(404).json({ message: 'Failed to retrieve form dialog template schemas.', diff --git a/Composer/packages/server/src/locales/cs.json b/Composer/packages/server/src/locales/cs.json index ee658b4ec1..336357b3e4 100644 --- a/Composer/packages/server/src/locales/cs.json +++ b/Composer/packages/server/src/locales/cs.json @@ -4400,4 +4400,4 @@ "zoom_out_e4302632": { "message": "Zoom out" } -} \ No newline at end of file +} diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index dbbb333b34..2745cd48a9 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -257,9 +257,6 @@ "advanced_events_2cbfa47d": { "message": "Advanced Events" }, - "advanced_options_4dcc8385": { - "message": "Advanced options" - }, "advanced_settings_view_json_b83638b4": { "message": "Advanced Settings View (json)" }, @@ -326,9 +323,6 @@ "any_or_expression_acad7d37": { "message": "Any or expression" }, - "any_string_f22dc2e1": { - "message": "any string" - }, "application_language_settings_85b1f06": { "message": "Application language settings" }, @@ -350,6 +344,9 @@ "are_you_sure_you_want_to_exit_the_onboarding_produ_c2de1b23": { "message": "Are you sure you want to exit the Onboarding Product Tour? You can restart the tour in the onboarding settings." }, + "are_you_sure_you_want_to_remove_examples_for_local_3a4742d4": { + "message": "Are you sure you want to remove examples for \"{ locale }\" locale?" + }, "are_you_sure_you_want_to_remove_form_dialog_schema_67c927ce": { "message": "Are you sure you want to remove form dialog schema \"{ id }\"?" }, @@ -701,9 +698,6 @@ "choose_a_valid_publish_profile_to_continue_d4bd0b96": { "message": "Choose a valid publish profile to continue" }, - "choose_how_to_create_your_bot_a97f7b3e": { - "message": "Choose how to create your bot" - }, "clear_all_da755751": { "message": "Clear all" }, @@ -977,9 +971,6 @@ "create_and_configure_new_azure_resources_302c574a": { "message": "Create and configure new Azure resources" }, - "create_bot_from_template_or_scratch_92f0fefa": { - "message": "Create bot from template or scratch?" - }, "create_copy_to_translate_bot_content_efc872c": { "message": "Create copy to translate bot content" }, @@ -992,15 +983,6 @@ "create_form_dialog_bb98a4f2": { "message": "Create form dialog" }, - "create_from_knowledge_base_qna_maker_7422486": { - "message": "Create from knowledge base (QnA Maker)" - }, - "create_from_qna_7aa9dcbb": { - "message": "Create from QnA" - }, - "create_from_scratch_485c3045": { - "message": "Create from scratch" - }, "create_from_template_87e12c94": { "message": "Create from template" }, @@ -1100,9 +1082,6 @@ "default_recognizer_9c06c1a3": { "message": "Default recognizer" }, - "define_by_value_type_3cd3d1a8": { - "message": "Define by value type" - }, "define_conversation_objective_146d1cc6": { "message": "Define conversation objective" }, @@ -1145,6 +1124,9 @@ "delete_knowledge_base_66e3a7f1": { "message": "Delete knowledge base" }, + "delete_locale_examples_6f0c5926": { + "message": "Delete \"{ locale }\" examples?" + }, "delete_properties_c49a7892": { "message": "Delete properties" }, @@ -1442,12 +1424,6 @@ "enter_a_manifest_url_to_add_a_new_skill_to_your_bo_eb966c95": { "message": "Enter a manifest URL to add a new skill to your bot." }, - "enter_a_max_value_14e8ba52": { - "message": "Enter a max value" - }, - "enter_a_min_value_c3030813": { - "message": "Enter a min value" - }, "enter_a_url_to_import_qna_resource_223ded92": { "message": "Enter a URL to Import QnA resource" }, @@ -1472,12 +1448,15 @@ "enter_skill_manifest_url_583d54f4": { "message": "Enter skill manifest URL" }, - "entities_ef09392c": { - "message": "Entities" + "entity_92d59f72": { + "message": "Entity" }, "entity_defined_in_lu_files_entity_1812c172": { "message": "Entity defined in lu files: { entity }" }, + "enum_examples_for_each_possible_value_153b6663": { + "message": "Enum examples for each possible value." + }, "environment_68aed6d3": { "message": "Environment" }, @@ -1559,8 +1538,8 @@ "everything_you_need_to_build_sophisticated_convers_9c00cc01": { "message": "Everything you need to build sophisticated conversational experiences" }, - "examples_c435f08c": { - "message": "Examples" + "examples_for_entity_9b50e2e5": { + "message": "Examples for entity" }, "existing_files_in_scripts_folder_will_be_overwritt_afa8d787": { "message": "Existing files in scripts/folder will be overwritten. Are you sure you want to continue?" @@ -1709,9 +1688,6 @@ "for_each_page_198e66f4": { "message": "For each page" }, - "for_properties_of_type_list_or_enum_your_bot_accep_9e7649c6": { - "message": "For properties of type list (or enum), your bot accepts only the values you define. After your dialog is generated, you can provide synonyms for each value." - }, "for_security_purposes_your_bot_can_only_call_a_ski_57c22e66": { "message": "For security purposes, your bot can only call a skill if its Microsoft App ID is in the skill’s allowed callers list. Once you create a publishing profile, share your bot’s App ID with the skill’s author to add it. You may also need to include the skill’s App Id in the root bot’s allowed callers list." }, @@ -1988,9 +1964,6 @@ "instructions_2f88ee72": { "message": "Instructions" }, - "integer_7f378275": { - "message": "integer" - }, "integer_b08abbe9": { "message": "Integer" }, @@ -2177,12 +2150,6 @@ "list_6cc05": { "message": "List" }, - "list_a034633b": { - "message": "list" - }, - "list_count_values_33ea7088": { - "message": "list - { count } values" - }, "list_entity_a3502e75": { "message": "List entity" }, @@ -2327,9 +2294,6 @@ "manually_add_question_and_answer_pairs_to_create_a_39089442": { "message": "Manually add question and answer pairs to create a knowledge base" }, - "maximum_f0e8e5e4": { - "message": "Maximum" - }, "mb_8f9f9e84": { "message": "MB" }, @@ -2402,9 +2366,6 @@ "minimap_beb3be27": { "message": "Minimap" }, - "minimum_f31b05ab": { - "message": "Minimum" - }, "missing_definition_for_defname_33f2b594": { "message": "Missing definition for { defName }" }, @@ -2603,9 +2564,6 @@ "number_constant_or_expression_to_evaluate_1098771": { "message": "Number constant or expression to evaluate." }, - "number_dc1c178": { - "message": "number" - }, "number_or_expression_55c7f9f": { "message": "Number or expression" }, @@ -2690,11 +2648,8 @@ "optional_properties_2c23c7c6": { "message": "Optional properties" }, - "optional_setting_a_maximum_value_enables_your_bot__1ec30919": { - "message": "Optional. Setting a maximum value enables your bot to reject a value that is too large and re-prompt the user for a new value." - }, - "optional_setting_a_minimum_value_enables_your_bot__f1fd4bce": { - "message": "Optional. Setting a minimum value enables your bot to reject a value that is too small and re-prompt the user for a new value." + "optional_properties_are_properties_the_bot_accepts_2bf34da9": { + "message": "Optional properties are properties the bot accepts if given but does not ask for." }, "options_3ab0ea65": { "message": "Options" @@ -2729,8 +2684,8 @@ "overview_58268c72": { "message": "Overview" }, - "p_copyright_c_microsoft_corporation_p_p_mit_licens_cd145fd6": { - "message": "

Copyright (c) Microsoft Corporation.

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" + "p_copyright_c_microsoft_corporation_p_p_mit_licens_6d0992f9": { + "message": "

Copyright (c) Microsoft Corporation.

\n

MIT License

\n

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

\n

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

\n

THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" }, "page_number_cdee4179": { "message": "Page number" @@ -2744,6 +2699,9 @@ "paste_token_here_eccec7e4": { "message": "Paste token here" }, + "please_add_a_name_to_this_property_to_enable_addin_916ab99c": { + "message": "Please add a name to this property to enable adding examples" + }, "please_enter_a_value_for_key_77cfc097": { "message": "Please enter a value for { key }" }, @@ -3107,14 +3065,17 @@ "required_5f7ef8c0": { "message": "Required" }, + "required_properties_are_properties_that_your_bot_w_d41caf66": { + "message": "Required properties are properties that your bot will ask the user to provide. The user must provide values for all required properties." + }, "required_properties_dfb0350d": { "message": "Required properties" }, "requiredtext_ff8f722f": { "message": "{ requiredText }" }, - "requiredtext_priority_priority_4293288f": { - "message": "{ requiredText } | Priority: { priority }" + "requiredtext_priority_help_priority_refers_to_the__54ceda82": { + "message": "{ requiredText } | Priority Priority refers to the order in which the bot will ask for the required properties. : { priority }" }, "reset_view_d5f8245a": { "message": "Reset view" @@ -3623,6 +3584,9 @@ "suggestion_for_card_or_activity_type_b257066a": { "message": "Suggestion for Card or Activity: { type }" }, + "synonyms_31f2e91e": { + "message": "Synonyms" + }, "synonyms_optional_afe5cdb1": { "message": "Synonyms (Optional)" }, @@ -4131,7 +4095,7 @@ "message": "Use machine learning to understand natural language input and direct the conversation flow." }, "use_orchestrator_for_multi_bot_projects_bots_that__1b481cdd": { - "message": "Use Orchestrator for multi-bot projects (bots that consist of multiple bots or connect to skills)." + "message": "Use Orchestrator for multi-bot projects (bots that consist of multiple bots or connect to skills)." }, "use_speech_to_enable_voice_input_and_output_for_yo_742c511d": { "message": "Use Speech to enable voice input and output for your bot." @@ -4331,9 +4295,6 @@ "you_are_about_to_remove_the_skill_from_this_projec_2ba31a6d": { "message": "You are about to remove the skill from this project. Removing this skill won’t delete the files." }, - "you_can_create_a_new_bot_from_scratch_with_compose_1486288c": { - "message": "You can create a new bot from scratch with Composer, or start with a template." - }, "you_can_only_connect_to_a_skill_in_the_root_bot_d8cb3f53": { "message": "You can only connect to a skill in the root bot." }, diff --git a/Composer/packages/server/src/locales/hu.json b/Composer/packages/server/src/locales/hu.json index 73d119f046..c0a1d171b8 100644 --- a/Composer/packages/server/src/locales/hu.json +++ b/Composer/packages/server/src/locales/hu.json @@ -4400,4 +4400,4 @@ "zoom_out_e4302632": { "message": "Zoom out" } -} \ No newline at end of file +} diff --git a/Composer/packages/server/src/locales/ko.json b/Composer/packages/server/src/locales/ko.json index 6a0635906c..ada7d6439f 100644 --- a/Composer/packages/server/src/locales/ko.json +++ b/Composer/packages/server/src/locales/ko.json @@ -4400,4 +4400,4 @@ "zoom_out_e4302632": { "message": "Zoom out" } -} \ No newline at end of file +} diff --git a/Composer/packages/server/src/locales/pt-BR.json b/Composer/packages/server/src/locales/pt-BR.json index d12afee7ec..f9a93402c5 100644 --- a/Composer/packages/server/src/locales/pt-BR.json +++ b/Composer/packages/server/src/locales/pt-BR.json @@ -4400,4 +4400,4 @@ "zoom_out_e4302632": { "message": "Zoom out" } -} \ No newline at end of file +} diff --git a/Composer/packages/server/src/locales/ru.json b/Composer/packages/server/src/locales/ru.json index 4f49c6893b..5aee75973b 100644 --- a/Composer/packages/server/src/locales/ru.json +++ b/Composer/packages/server/src/locales/ru.json @@ -4400,4 +4400,4 @@ "zoom_out_e4302632": { "message": "Zoom out" } -} \ No newline at end of file +} diff --git a/Composer/packages/server/src/locales/zh-Hant.json b/Composer/packages/server/src/locales/zh-Hant.json index 46640c59e9..7b9f61cb11 100644 --- a/Composer/packages/server/src/locales/zh-Hant.json +++ b/Composer/packages/server/src/locales/zh-Hant.json @@ -4400,4 +4400,4 @@ "zoom_out_e4302632": { "message": "Zoom out" } -} \ No newline at end of file +} diff --git a/Composer/packages/server/src/models/bot/botProject.ts b/Composer/packages/server/src/models/bot/botProject.ts index 464730b321..f680afb700 100644 --- a/Composer/packages/server/src/models/bot/botProject.ts +++ b/Composer/packages/server/src/models/bot/botProject.ts @@ -691,18 +691,7 @@ export class BotProject implements IBotProject { // merge - if generated assets should be merged with any user customized assets // singleton - if the generated assets should be merged into a single dialog // feeback - a callback for status and progress and generation happens - const success = await generate( - generateParams.schemaPath, - generateParams.prefix, - generateParams.outDir, - generateParams.metaSchema, - generateParams.allLocales, - generateParams.templateDirs, - generateParams.force, - generateParams.merge, - generateParams.singleton, - generateParams.feedback - ); + const success = await generate(generateParams.schemaPath, generateParams); return { success, errors }; } diff --git a/Composer/packages/types/src/indexers.ts b/Composer/packages/types/src/indexers.ts index 5fc9b708a3..2b098e894b 100644 --- a/Composer/packages/types/src/indexers.ts +++ b/Composer/packages/types/src/indexers.ts @@ -294,8 +294,15 @@ export type FormDialogSchema = { }; export type FormDialogSchemaTemplate = { - name: string; - isGlobal: boolean; + id: string; + type: string; + $global: boolean; + $template?: string; + format?: string; + $generator: Record & { + title: string; + description: string; + }; }; export type RecognizerFile = { diff --git a/Composer/yarn.lock b/Composer/yarn.lock index 111f79a67e..78e351848e 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -7,6 +7,15 @@ resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f" integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA== +"@apidevtools/json-schema-ref-parser@9.0.7", "@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.0.7" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@apidevtools/json-schema-ref-parser/-/@apidevtools/json-schema-ref-parser-9.0.7.tgz#64aa7f5b34e43d74ea9e408b90ddfba02050dde3" + integrity sha1-ZKp/WzTkPXTqnkCLkN37oCBQ3eM= + dependencies: + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + js-yaml "^3.13.1" + "@apidevtools/json-schema-ref-parser@^9.0.1": version "9.0.6" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c" @@ -16,6 +25,28 @@ call-me-maybe "^1.0.1" js-yaml "^3.13.1" +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@apidevtools/openapi-schemas/-/@apidevtools/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha1-n6CAF/tZ2AU4gS8D/HysWZLKqhc= + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@apidevtools/swagger-methods/-/@apidevtools/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha1-t4mjYuBVsDQNBHEur+cCfdwawmc= + +"@apidevtools/swagger-parser@10.0.2": + version "10.0.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@apidevtools/swagger-parser/-/@apidevtools/swagger-parser-10.0.2.tgz#f4145afb7c3a3bafe0376f003b5c3bdeae17a952" + integrity sha1-9BRa+3w6O6/gN28AO1w73q4XqVI= + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^4.2.3" + "@azure/abort-controller@^1.0.0": version "1.0.2" resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.2.tgz#822405c966b2aec16fb62c1b19d37eaccf231995" @@ -4024,23 +4055,23 @@ ts-md5 "^1.2.6" tslib "^1.10.0" -"@microsoft/bf-generate-library@^4.10.0-daily.20210317.224602": - version "4.10.0-daily.20210317.224602" - resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-generate-library/-/@microsoft/bf-generate-library-4.10.0-daily.20210317.224602.tgz#1f365a27779e4ec6bc1f9899c7bb3fdf9336aef1" - integrity sha1-HzZaJ3eeTsa8H5iZx7s/35M2rvE= +"@microsoft/bf-generate-library@^4.14.0-preview.20210605.ff079e5": + version "4.14.0-preview.20210605.ff079e5" + resolved "https://registry.yarnpkg.com/@microsoft/bf-generate-library/-/bf-generate-library-4.14.0-preview.20210605.ff079e5.tgz#8ffc164fdf245d21c2d2de1973263876ce4b8a8a" + integrity sha512-q91m8DawwE18g0sqKtgPnC6gwC+N4MLyWiIx+gRhoX5hDaxPjcPow3yUrwiBDvx/aIPAI7EOS8xAMA9fQpFlIA== dependencies: - "@microsoft/bf-lu" rc - adaptive-expressions "4.11.1" - ajv "^6.9.1" - botbuilder-lg "4.11.1" + "@microsoft/bf-lu" latest + adaptive-expressions next + ajv "^8.2.0" + botbuilder-lg next clone "^2.1.2" - fs-extra "^8.1.0" - globby "^11.0.1" - json-schema-merge-allof "^0.6.0" - json-schema-ref-parser "^7.1.0" - openapi-types "^1.3.5" + fs-extra "^10.0.0" + globby "^11.0.3" + json-schema-merge-allof "^0.8.1" + json-schema-ref-parser "^9.0.7" + openapi-types "^8.0.0" seedrandom "~3.0.5" - swagger-parser "^8.0.4" + swagger-parser "^10.0.2" "@microsoft/bf-lu@4.13.0-rc0": version "4.13.0-rc0" @@ -4085,15 +4116,17 @@ semver "^5.5.1" tslib "^2.0.3" -"@microsoft/bf-lu@next": - version "4.13.0-dev.20210218.d9e7d3a" - resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.13.0-dev.20210218.d9e7d3a.tgz#351004819bf9a130d324e687e774df621914e2a0" - integrity sha512-Z/cBw1nu6WzvPR4r8dVuGfp3Cw6hSws9TNntrIsSYr2hCcGw9fROf2GJP1JMIs+QZYAt6+AvnuKnH8L1BiZOTg== +"@microsoft/bf-lu@latest": + version "4.10.0-dev.20200730.3786d10" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@microsoft/bf-lu/-/@microsoft/bf-lu-4.10.0-dev.20200730.3786d10.tgz#ed2863a4565bb3ae550d17e1e26ae9e6f6600cf0" + integrity sha1-7ShjpFZbs65VDRfh4mrp5vZgDPA= dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" + "@oclif/command" "~1.5.19" + "@oclif/errors" "~1.2.2" "@types/node-fetch" "~2.5.5" - antlr4 "~4.8.0" + antlr4 "^4.7.2" chalk "2.4.1" console-stream "^0.1.1" deep-equal "^1.0.1" @@ -4105,12 +4138,12 @@ lodash "^4.17.19" node-fetch "~2.6.0" semver "^5.5.1" - tslib "^2.0.3" + tslib "^1.10.0" -"@microsoft/bf-lu@rc": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.11.1.tgz#b5e65d519b4e801c7254ff2df0dac4b74b80b7db" - integrity sha512-T0DmGhS2u1Aw9aMxVBXToTFfe0NIlKNg/kgxqBDHKIE7mbJeXUl7GCNIUt8U6WNSA+56+2G7wwK484mifmd36A== +"@microsoft/bf-lu@next": + version "4.13.0-dev.20210218.d9e7d3a" + resolved "https://registry.yarnpkg.com/@microsoft/bf-lu/-/bf-lu-4.13.0-dev.20210218.d9e7d3a.tgz#351004819bf9a130d324e687e774df621914e2a0" + integrity sha512-Z/cBw1nu6WzvPR4r8dVuGfp3Cw6hSws9TNntrIsSYr2hCcGw9fROf2GJP1JMIs+QZYAt6+AvnuKnH8L1BiZOTg== dependencies: "@azure/cognitiveservices-luis-authoring" "4.0.0-preview.1" "@azure/ms-rest-azure-js" "2.0.1" @@ -4156,11 +4189,6 @@ resolved "https://registry.yarnpkg.com/@microsoft/load-themed-styles/-/load-themed-styles-1.10.128.tgz#02e0821beb2042762bcec072d5440e0f49d864ad" integrity sha512-RUi3YQyPIIf1vZc2yYjG4p2nC1Rxj7Cv77Uc9bnNwb307sgAyuuxG/un+FLAE4I0crGfVDWdVRRVqdRu18BlZA== -"@microsoft/recognizers-text-data-types-timex-expression@1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@microsoft/recognizers-text-data-types-timex-expression/-/recognizers-text-data-types-timex-expression-1.1.4.tgz#623453ae65e8df212d8156f6a314675c30696c1d" - integrity sha512-2vICaEJfV9EpaDKs5P1PLAEs+WpNqrtpkl7CLsmc5gKmxgpQtsojG4tk6km5JRKg1mYuLV5ZzJ/65oOEeyTMvQ== - "@microsoft/recognizers-text-data-types-timex-expression@1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@microsoft/recognizers-text-data-types-timex-expression/-/recognizers-text-data-types-timex-expression-1.3.0.tgz#fc5d586c826e478e8477b7fcb21e9e2830e81c67" @@ -4648,6 +4676,11 @@ "@types/connect" "*" "@types/node" "*" +"@types/btoa-lite@^1.0.0": + version "1.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/@types/btoa-lite/-/@types/btoa-lite-1.0.0.tgz#e190a5a548e0b348adb0df9ac7fa5f1151c7cca4" + integrity sha1-4ZClpUjgs0itsN+ax/pfEVHHzKQ= + "@types/classnames@^2.2.9": version "2.2.11" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.11.tgz#2521cc86f69d15c5b90664e4829d84566052c1cf" @@ -4990,13 +5023,6 @@ dependencies: "@types/node" "*" -"@types/moment-timezone@^0.5.13": - version "0.5.30" - resolved "https://registry.yarnpkg.com/@types/moment-timezone/-/moment-timezone-0.5.30.tgz#340ed45fe3e715f4a011f5cfceb7cb52aad46fc7" - integrity sha512-aDVfCsjYnAQaV/E9Qc24C5Njx1CoDjXsEgkxtp9NyXDpYu4CCbmclb6QhWloS9UTU/8YROUEEdEkWI0D7DxnKg== - dependencies: - moment-timezone "*" - "@types/morgan@^1.7.35": version "1.7.35" resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.7.35.tgz#6358f502931cc2583d7a94248c41518baa688494" @@ -5363,7 +5389,7 @@ dependencies: "@types/node" "*" -"@types/xmldom@^0.1.29": +"@types/xmldom@^0.1.29", "@types/xmldom@^0.1.30": version "0.1.30" resolved "https://registry.yarnpkg.com/@types/xmldom/-/xmldom-0.1.30.tgz#d36d9a7d64af4693d3b18d5dc02ce432a95be12e" integrity sha512-edqgAFXMEtVvaBZ3YnhamvmrHjoYpuxETmnb0lbTZmf/dXpAsO9ZKotUO4K2rn2SIZBDFCMOuA7fOe0H6dRZcA== @@ -5994,47 +6020,51 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ== -adaptive-expressions@4.11.1: - version "4.11.1" - resolved "https://registry.yarnpkg.com/adaptive-expressions/-/adaptive-expressions-4.11.1.tgz#ccd0249f754e891d15354be2f8b77f3fb10ae05f" - integrity sha512-lj9yfCs/RYws70BrjxIzqkRgmVu9Xrc9XR67Kc8fv6KmuaoddVrrq3CE1cdz20mtkTWrRLSYEk/N+zvmYlRiww== +adaptive-expressions@4.12.0-rc1: + version "4.12.0-rc1" + resolved "https://registry.yarnpkg.com/adaptive-expressions/-/adaptive-expressions-4.12.0-rc1.tgz#d7f90d78b2b4242495471047074d0f5111f1cc46" + integrity sha512-PA5FVt1zpCZFzA708FzdOc3qE+6ltFsvqSVirHjZFRnNBMmLJnS5UJqJ3Y4P+B0+w2/f6B9ZY/gBIRGOPekNwQ== dependencies: - "@microsoft/recognizers-text-data-types-timex-expression" "1.1.4" + "@microsoft/recognizers-text-data-types-timex-expression" "1.3.0" "@types/atob-lite" "^2.0.0" "@types/lru-cache" "^5.1.0" - "@types/moment-timezone" "^0.5.13" "@types/xmldom" "^0.1.29" antlr4ts "0.5.0-alpha.3" atob-lite "^2.0.0" big-integer "^1.6.48" d3-format "^1.4.4" + dayjs "^1.10.3" jspath "^0.4.0" lodash "^4.17.19" lru-cache "^5.1.1" - moment "^2.25.1" - moment-timezone "^0.5.28" + uuid "^8.3.2" + x2js "^3.4.0" + xml2js "^0.4.23" + xmldom "^0.4.0" + xpath "^0.0.32" -adaptive-expressions@4.12.0-rc1: - version "4.12.0-rc1" - resolved "https://registry.yarnpkg.com/adaptive-expressions/-/adaptive-expressions-4.12.0-rc1.tgz#d7f90d78b2b4242495471047074d0f5111f1cc46" - integrity sha512-PA5FVt1zpCZFzA708FzdOc3qE+6ltFsvqSVirHjZFRnNBMmLJnS5UJqJ3Y4P+B0+w2/f6B9ZY/gBIRGOPekNwQ== +adaptive-expressions@4.14.0-dev.c7a22fb, adaptive-expressions@next: + version "4.14.0-dev.c7a22fb" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/adaptive-expressions/-/adaptive-expressions-4.14.0-dev.c7a22fb.tgz#d82c0cfd184ba4da8f6515fb5c3a050108d1b5e4" + integrity sha1-2CwM/RhLpNqPZRX7XDoFAQjRteQ= dependencies: "@microsoft/recognizers-text-data-types-timex-expression" "1.3.0" "@types/atob-lite" "^2.0.0" + "@types/btoa-lite" "^1.0.0" "@types/lru-cache" "^5.1.0" - "@types/xmldom" "^0.1.29" + "@types/xmldom" "^0.1.30" antlr4ts "0.5.0-alpha.3" atob-lite "^2.0.0" big-integer "^1.6.48" + btoa-lite "^1.0.0" d3-format "^1.4.4" dayjs "^1.10.3" jspath "^0.4.0" - lodash "^4.17.19" lru-cache "^5.1.1" uuid "^8.3.2" x2js "^3.4.0" xml2js "^0.4.23" - xmldom "^0.4.0" + xmldom "^0.5.0" xpath "^0.0.32" adaptivecards@2.5.0: @@ -6142,6 +6172,16 @@ ajv@^6.12.0, ajv@^6.12.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.2.0: + version "8.2.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/ajv/-/ajv-8.2.0.tgz#c89d3380a784ce81b2085f48811c4c101df4c602" + integrity sha1-yJ0zgKeEzoGyCF9IgRxMEB30xgI= + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ally.js@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/ally.js/-/ally.js-1.4.1.tgz#9fb7e6ba58efac4ee9131cb29aa9ee3b540bcf1e" @@ -6239,6 +6279,11 @@ ansicolors@~0.3.2: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" integrity sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk= +antlr4@^4.7.2: + version "4.9.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/antlr4/-/antlr4-4.9.2.tgz#abbc53d275954b1b6f4d8b3468b4a2cb258121fc" + integrity sha1-q7xT0nWVSxtvTYs0aLSiyyWBIfw= + antlr4@~4.8.0: version "4.8.0" resolved "https://registry.yarnpkg.com/antlr4/-/antlr4-4.8.0.tgz#f938ec171be7fc2855cd3a533e87647185b32b6a" @@ -7246,17 +7291,6 @@ boolean@^3.0.0: resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.0.1.tgz#35ecf2b4a2ee191b0b44986f14eb5f052a5cbb4f" integrity sha512-HRZPIjPcbwAVQvOTxR4YE3o8Xs98NqbbL1iEZDCz7CL8ql0Lt5iOyJFxfnAB0oFs8Oh02F/lLlg30Mexv46LjA== -botbuilder-lg@4.11.1: - version "4.11.1" - resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/botbuilder-lg/-/botbuilder-lg-4.11.1.tgz#4bae2f22f0c35abf0809c5768f2f64256c88a58f" - integrity sha1-S64vIvDDWr8ICcV2jy9kJWyIpY8= - dependencies: - adaptive-expressions "4.11.1" - antlr4ts "0.5.0-alpha.3" - lodash "^4.17.19" - path "^0.12.7" - uuid "^3.4.0" - botbuilder-lg@4.12.0-rc1: version "4.12.0-rc1" resolved "https://registry.yarnpkg.com/botbuilder-lg/-/botbuilder-lg-4.12.0-rc1.tgz#32e5f6dd8bdbf176d059a2d4f6faad8c1effc140" @@ -7268,6 +7302,17 @@ botbuilder-lg@4.12.0-rc1: path "^0.12.7" uuid "^8.3.2" +botbuilder-lg@next: + version "4.14.0-dev.c7a22fb" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/botbuilder-lg/-/botbuilder-lg-4.14.0-dev.c7a22fb.tgz#222f48255ea5445849b1c5f75904bae274d665c9" + integrity sha1-Ii9IJV6lRFhJscX3WQS64nTWZck= + dependencies: + adaptive-expressions "4.14.0-dev.c7a22fb" + antlr4ts "0.5.0-alpha.3" + lodash "^4.17.19" + path "^0.12.7" + uuid "^8.3.2" + botframework-directlinejs@0.14.1: version "0.14.1" resolved "https://registry.yarnpkg.com/botframework-directlinejs/-/botframework-directlinejs-0.14.1.tgz#34290bf1e07214bad66f73539e70fced91927dce" @@ -7590,6 +7635,11 @@ bser@^2.0.0: dependencies: node-int64 "^0.4.0" +btoa-lite@^1.0.0: + version "1.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" + integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= + btoa@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" @@ -8615,7 +8665,7 @@ compute-gcd@^1.2.1: validate.io-function "^1.0.2" validate.io-integer-array "^1.0.0" -compute-lcm@^1.1.0: +compute-lcm@^1.1.0, compute-lcm@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/compute-lcm/-/compute-lcm-1.1.2.tgz#9107c66b9dca28cefb22b4ab4545caac4034af23" integrity sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ== @@ -12285,6 +12335,15 @@ fs-extra@7.0.1, fs-extra@^7.0.0, fs-extra@^7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha1-n/YbZV3eU/s0qC34S7IUzoAuF8E= + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" @@ -12826,6 +12885,18 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" +globby@^11.0.3: + version "11.0.3" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb" + integrity sha1-mx8MtSPhcd0a2MeyqftLZEuVk8s= + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -15073,14 +15144,14 @@ json-schema-defaults@^0.4.0: dependencies: argparse "^1.0.9" -json-schema-merge-allof@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/json-schema-merge-allof/-/json-schema-merge-allof-0.6.0.tgz#64d48820fec26b228db837475ce3338936bf59a5" - integrity sha512-LEw4VMQVRceOPLuGRWcxW5orTTiR9ZAtqTAe4rQUjNADTeR81bezBVFa0MqIwp0YmHIM1KkhSjZM7o+IQhaPbQ== +json-schema-merge-allof@^0.8.1: + version "0.8.1" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz#ed2828cdd958616ff74f932830a26291789eaaf2" + integrity sha1-7SgozdlYYW/3T5MoMKJikXieqvI= dependencies: - compute-lcm "^1.1.0" + compute-lcm "^1.1.2" json-schema-compare "^0.2.2" - lodash "^4.17.4" + lodash "^4.17.20" json-schema-merge-allof@~0.7.0: version "0.7.0" @@ -15091,14 +15162,12 @@ json-schema-merge-allof@~0.7.0: json-schema-compare "^0.2.2" lodash "^4.17.4" -json-schema-ref-parser@^7.1.0, json-schema-ref-parser@^7.1.3: - version "7.1.4" - resolved "https://registry.yarnpkg.com/json-schema-ref-parser/-/json-schema-ref-parser-7.1.4.tgz#abb3f2613911e9060dc2268477b40591753facf0" - integrity sha512-AD7bvav0vak1/63w3jH8F7eHId/4E4EPdMAEZhGxtjktteUv9dnNB/cJy6nVnMyoTPBJnLwFK6tiQPSTeleCtQ== +json-schema-ref-parser@^9.0.7: + version "9.0.7" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/json-schema-ref-parser/-/json-schema-ref-parser-9.0.7.tgz#c0ccc5aaee34844f0865889b67e0b67d616f7375" + integrity sha1-wMzFqu40hE8IZYibZ+C2fWFvc3U= dependencies: - call-me-maybe "^1.0.1" - js-yaml "^3.13.1" - ono "^6.0.0" + "@apidevtools/json-schema-ref-parser" "9.0.7" json-schema-traverse@^0.4.1: version "0.4.1" @@ -16528,19 +16597,7 @@ mock-socket@9.0.3: dependencies: url-parse "^1.4.4" -moment-timezone@*, moment-timezone@^0.5.28: - version "0.5.33" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" - integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== - dependencies: - moment ">= 2.9.0" - -"moment@>= 2.9.0": - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== - -moment@^2.15.1, moment@^2.24.0, moment@^2.25.1, moment@^2.27.0, moment@^2.29.1: +moment@^2.15.1, moment@^2.24.0, moment@^2.27.0, moment@^2.29.1: version "2.29.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -17355,11 +17412,6 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -ono@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ono/-/ono-6.0.1.tgz#1bc14ffb8af1e5db3f7397f75b88e4a2d64bbd71" - integrity sha512-5rdYW/106kHqLeG22GE2MHKq+FlsxMERZev9DCzQX1zwkxnFwBivSn5i17a5O/rDmOJOdf4Wyt80UZljzx9+DA== - open@^6.3.0: version "6.4.0" resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" @@ -17375,15 +17427,10 @@ open@^7.3.0: is-docker "^2.0.0" is-wsl "^2.1.1" -openapi-schemas@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/openapi-schemas/-/openapi-schemas-1.0.3.tgz#0fa2f19e44ce8a1cdab9c9f616df4babe1aa026b" - integrity sha512-KtMWcK2VtOS+nD8RKSIyScJsj8JrmVWcIX7Kjx4xEHijFYuvMTDON8WfeKOgeSb4uNG6UsqLj5Na7nKbSav9RQ== - -openapi-types@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-1.3.5.tgz#6718cfbc857fe6c6f1471f65b32bdebb9c10ce40" - integrity sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg== +openapi-types@^8.0.0: + version "8.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/openapi-types/-/openapi-types-8.0.0.tgz#7e1979538798d31a3c3bfed667e5e9295402f9bc" + integrity sha1-fhl5U4eY0xo8O/7WZ+XpKVQC+bw= opener@^1.5.1: version "1.5.1" @@ -21675,23 +21722,12 @@ svgo@^1.1.1: unquote "~1.1.1" util.promisify "~1.0.0" -swagger-methods@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/swagger-methods/-/swagger-methods-2.0.2.tgz#5891d5536e54d5ba8e7ae1007acc9170f41c9590" - integrity sha512-/RNqvBZkH8+3S/FqBPejHxJxZenaYq3MrpeXnzi06aDIS39Mqf5YCUNb/ZBjsvFFt8h9FxfKs8EXPtcYdfLiRg== - -swagger-parser@^8.0.4: - version "8.0.4" - resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-8.0.4.tgz#ddec68723d13ee3748dd08fd5b7ba579327595da" - integrity sha512-KGRdAaMJogSEB7sPKI31ptKIWX8lydEDAwWgB4pBMU7zys5cd54XNhoPSVlTxG/A3LphjX47EBn9j0dOGyzWbA== +swagger-parser@^10.0.2: + version "10.0.2" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/swagger-parser/-/swagger-parser-10.0.2.tgz#d7f18faa09c9c145e938977c9bd6c3435998b667" + integrity sha1-1/GPqgnJwUXpOJd8m9bDQ1mYtmc= dependencies: - call-me-maybe "^1.0.1" - json-schema-ref-parser "^7.1.3" - ono "^6.0.0" - openapi-schemas "^1.0.2" - openapi-types "^1.3.5" - swagger-methods "^2.0.1" - z-schema "^4.2.2" + "@apidevtools/swagger-parser" "10.0.2" symbol-observable@1.0.1: version "1.0.1" @@ -22647,6 +22683,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +universalify@^2.0.0: + version "2.0.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha1-daSYTv7cSwiXXFrrc/Uw0C3yVxc= + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" @@ -23969,6 +24010,11 @@ xmldom@^0.4.0: resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/xmldom/-/xmldom-0.4.0.tgz#8771e482a333af44587e30ce026f0998c23f3830" integrity sha1-h3HkgqMzr0RYfjDOAm8JmMI/ODA= +xmldom@^0.5.0: + version "0.5.0" + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e" + integrity sha1-GTy5a4SqNIYSfqYnLEWWNUy0li4= + xmlhttprequest-ts@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/xmlhttprequest-ts/-/xmlhttprequest-ts-1.0.1.tgz#7b3cb4a197aee38cf2d4f9dd6189d1d21f0835b2" @@ -24242,10 +24288,10 @@ yup@^0.26.10: synchronous-promise "^2.0.5" toposort "^2.0.2" -z-schema@^4.2.2: +z-schema@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-4.2.3.tgz#85f7eea7e6d4fe59a483462a98f511bd78fe9882" - integrity sha512-zkvK/9TC6p38IwcrbnT3ul9in1UX4cm1y/VZSs4GHKIiDCrlafc+YQBgQBUdDXLAoZHf2qvQ7gJJOo6yT1LH6A== + resolved "https://botbuilder.myget.org/F/botframework-cli/npm/z-schema/-/z-schema-4.2.3.tgz#85f7eea7e6d4fe59a483462a98f511bd78fe9882" + integrity sha1-hffup+bU/lmkg0YqmPURvXj+mII= dependencies: lodash.get "^4.4.2" lodash.isequal "^4.5.0"