diff --git a/Composer/package.json b/Composer/package.json index 42088ce4f8..f2e93589c4 100644 --- a/Composer/package.json +++ b/Composer/package.json @@ -25,6 +25,7 @@ "packages/electron-server", "packages/extension", "packages/extension-client", + "packages/form-dialogs", "packages/intellisense", "packages/lib", "packages/lib/*", @@ -43,7 +44,7 @@ "build:test": "yarn workspace @bfc/test-utils build", "build:lib": "yarn workspace @bfc/libs build:all", "build:electron": "yarn workspace @bfc/electron-server build && yarn workspace @bfc/electron-server l10n", - "build:extensions": "wsrun -lt -p @bfc/extension @bfc/intellisense @bfc/extension-client @bfc/adaptive-form @bfc/adaptive-flow @bfc/ui-plugin-* -c build", + "build:extensions": "wsrun -lt -p @bfc/extension @bfc/intellisense @bfc/extension-client @bfc/adaptive-form @bfc/adaptive-flow @bfc/ui-plugin-* @bfc/form-dialogs -c build", "build:server": "yarn workspace @bfc/server build", "build:client": "yarn workspace @bfc/client build", "build:tools": "yarn workspace @bfc/tools build:all", diff --git a/Composer/packages/form-dialogs/.eslintrc.js b/Composer/packages/form-dialogs/.eslintrc.js new file mode 100644 index 0000000000..17b57b8148 --- /dev/null +++ b/Composer/packages/form-dialogs/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../.eslintrc.react.js'], + parserOptions: { + project: './tsconfig.lib.json', + tsconfigRootDir: __dirname, + }, +}; diff --git a/Composer/packages/form-dialogs/.gitignore b/Composer/packages/form-dialogs/.gitignore new file mode 100644 index 0000000000..d6bff14868 --- /dev/null +++ b/Composer/packages/form-dialogs/.gitignore @@ -0,0 +1,9 @@ +node_modules +dist +lib + +packages +bin +obj + +**/*.log diff --git a/Composer/packages/form-dialogs/package.json b/Composer/packages/form-dialogs/package.json new file mode 100644 index 0000000000..2b077a956c --- /dev/null +++ b/Composer/packages/form-dialogs/package.json @@ -0,0 +1,63 @@ +{ + "name": "@bfc/form-dialogs", + "version": "0.0.1", + "license": "MIT", + "author": "Microsoft", + "description": "Form Dialog components for Bot Framework Composer", + "main": "./lib/index.js", + "typings": "./lib/VisualSchemaEditor.d.ts", + "files": [ + "lib/*index*", + "lib/VisualSchemaEditor.d.ts", + "LICENSE" + ], + "scripts": { + "clean": "rimraf lib dist", + "start": "node tools/devServer.js", + "build": "rimraf lib && webpack --config webpack.lib.config.js --mode production", + "lint": "eslint --quiet ./src", + "lint:fix": "yarn lint --fix" + }, + "dependencies": { + "react-beautiful-dnd": "^13.0.0" + }, + "peerDependencies": { + "recoil": "^0.0.13", + "react": "16.13.1", + "react-dom": "16.13.1", + "lodash": "^4.17.19", + "format-message": "^6.2.3", + "office-ui-fabric-react": "^7.121.11", + "@emotion/core": "^10.0.27", + "@emotion/styled": "^10.0.27", + "@uifabric/fluent-theme": "^7.1.107", + "@uifabric/react-hooks": "^7.4.12" + }, + "devDependencies": { + "@types/react": "^16.8.18", + "@types/react-beautiful-dnd": "^13.0.0", + "@types/react-dom": "^16.8.4", + "@types/lodash": "^4.14.146", + "@types/recoil": "^0.0.1", + "cross-env": "^5.2.0", + "css-loader": "^2.1.1", + "eslint": "^7.5.0", + "eslint-loader": "4.0.0", + "express": "^4.14.0", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.6.0", + "optimize-css-assets-webpack-plugin": "^5.0.1", + "peer-deps-externals-webpack-plugin": "^1.0.4", + "react-dev-utils": "7.0.3", + "rimraf": "^2.6.3", + "source-map-loader": "^0.2.4", + "style-loader": "^0.23.1", + "ts-loader": "^6.0.1", + "typescript": "^3.5.3", + "webpack": "^4.32.0", + "webpack-cli": "^3.3.2", + "webpack-dev-middleware": "^3.7.0", + "webpack-hot-middleware": "^2.25.0", + "webpack-notifier": "^1.7.0" + } +} diff --git a/Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx b/Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx new file mode 100644 index 0000000000..132d6396ba --- /dev/null +++ b/Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; +// eslint-disable-next-line @typescript-eslint/camelcase +import { RecoilRoot, useRecoilTransactionObserver_UNSTABLE } from 'recoil'; +import { formDialogSchemaJsonSelector } from 'src/atoms/appState'; +import { useHandlers } from 'src/atoms/handlers'; +import { FormDialogPropertiesEditor } from 'src/components/FormDialogPropertiesEditor'; + +export type FormDialogSchemaEditorProps = { + /** + * Unique id for the visual editor. + */ + editorId: string; + /** + * Initial json schema content. + */ + schema: { id: string; content: string }; + /** + * Form dialog schema file extension. + */ + schemaExtension?: string; + /** + * Record of available schema templates. + */ + templates?: string[]; + /** + * Callback for when the json schema update is updated. + */ + onSchemaUpdated: (id: string, content: string) => void; + /** + * Callback for generating dialog using current valid form dialog schema. + */ + onGenerateDialog: (formDialogSchemaJson: string) => void; +}; + +const InternalFormDialogSchemaEditor = React.memo((props: FormDialogSchemaEditorProps) => { + const { editorId, schema, templates = [], schemaExtension = '.schema', onSchemaUpdated, onGenerateDialog } = props; + + const { setTemplates, reset, importSchemaString } = useHandlers(); + + React.useEffect(() => { + setTemplates({ templates }); + }, [templates]); + + React.useEffect(() => { + importSchemaString(schema); + }, [editorId]); + + const startOver = React.useCallback(() => { + reset({ name: editorId }); + }, [reset, editorId]); + + useRecoilTransactionObserver_UNSTABLE(async ({ snapshot, previousSnapshot }) => { + const content = await snapshot.getPromise(formDialogSchemaJsonSelector); + const prevContent = await previousSnapshot.getPromise(formDialogSchemaJsonSelector); + if (content !== prevContent) { + onSchemaUpdated(schema.id, content); + } + }); + + return ( + + ); +}); + +export const FormDialogSchemaEditor = (props: FormDialogSchemaEditorProps) => { + return ( + + + + ); +}; diff --git a/Composer/packages/form-dialogs/src/atoms/appState.ts b/Composer/packages/form-dialogs/src/atoms/appState.ts new file mode 100644 index 0000000000..b8cf86d4ca --- /dev/null +++ b/Composer/packages/form-dialogs/src/atoms/appState.ts @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + +import { atom, atomFamily, selector, selectorFamily } from 'recoil'; +import { FormDialogProperty, FormDialogSchema } from 'src/atoms/types'; +import { spreadSchemaPropertyStore, validateSchemaPropertyStore } from 'src/atoms/utils'; + +const schemaDraftUrl = 'http://json-schema.org/draft-07/schema'; + +/** + * This atom represents a form dialog schema. + */ +export const formDialogSchemaAtom = atom({ + key: 'FormDialogSchemaAtom', + default: { + id: '', + name: '', + requiredPropertyIds: [], + optionalPropertyIds: [], + }, +}); + +/** + * This atom family represent a form dialog schema property. + */ +export const formDialogPropertyAtom = atomFamily({ + key: 'FormDialogPropertyAtom', + default: (id) => ({ + id, + name: '', + kind: 'string', + payload: { kind: 'string' }, + required: false, + array: false, + examples: [], + }), +}); + +/** + * This selector separates required and optional properties within a form dialog schema. + */ +export const allFormDialogPropertyIdsSelector = selector({ + key: 'RequiredFormDialogPropertyIdsSelector', + get: ({ get }) => { + const { requiredPropertyIds, optionalPropertyIds } = get(formDialogSchemaAtom); + return [...requiredPropertyIds, ...optionalPropertyIds]; + }, +}); + +/** + * This selector computes the names of all properties within a form dialog schema. + */ +export const formDialogSchemaPropertyNamesSelector = selector({ + key: 'FormDialogSchemaPropertyNamesSelector', + get: ({ get }) => { + const propertyIds = get(allFormDialogPropertyIdsSelector); + return propertyIds.map((pId) => get(formDialogPropertyAtom(pId)).name); + }, +}); + +/** + * This selector computes the json representing a form dialog property. + */ +export const formDialogPropertyJsonSelector = selectorFamily({ + key: 'FormDialogPropertyJsonSelector', + get: (id) => ({ get }) => { + const schemaPropertyStore = get(formDialogPropertyAtom(id)); + return spreadSchemaPropertyStore(schemaPropertyStore); + }, +}); + +/** + * This selector computes if a form dialog property is valid. + */ +export const formDialogPropertyValidSelector = selectorFamily({ + key: 'FormDialogPropertyValidSelector', + get: (id) => ({ get }) => { + const schemaPropertyStore = get(formDialogPropertyAtom(id)); + return validateSchemaPropertyStore(schemaPropertyStore); + }, +}); + +/** + * This selector computes if a form dialog schema is valid. + */ +export const formDialogSchemaValidSelector = selector({ + key: 'FormDialogSchemaValidSelector', + get: ({ get }) => { + const propertyIds = get(allFormDialogPropertyIdsSelector); + return propertyIds.every((pId) => get(formDialogPropertyValidSelector(pId))); + }, +}); + +/** + * This selector computes the json representing a form dialog schema. + */ +export const formDialogSchemaJsonSelector = selector({ + key: 'FormDialogSchemaJsonSelector', + get: ({ get }) => { + const propertyIds = get(allFormDialogPropertyIdsSelector); + const schemaPropertyStores = propertyIds.map((pId) => get(formDialogPropertyAtom(pId))); + + let jsonObject: object = { + schema: schemaDraftUrl, + type: 'object', + $requires: ['standard.schema'], + }; + + if (schemaPropertyStores.length) { + jsonObject = { + ...jsonObject, + properties: propertyIds.reduce>((acc, propId, idx) => { + const property = schemaPropertyStores[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; + }, >{}); + + 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: '', +}); diff --git a/Composer/packages/form-dialogs/src/atoms/handlers.ts b/Composer/packages/form-dialogs/src/atoms/handlers.ts new file mode 100644 index 0000000000..1fed16b717 --- /dev/null +++ b/Composer/packages/form-dialogs/src/atoms/handlers.ts @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable react-hooks/rules-of-hooks */ + +import * as React from 'react'; +import { useRecoilCallback } from 'recoil'; +import { + activePropertyIdAtom, + allFormDialogPropertyIdsSelector, + formDialogPropertyAtom, + formDialogSchemaAtom, + formDialogSchemaPropertyNamesSelector, + formDialogTemplatesAtom, +} from 'src/atoms/appState'; +import { FormDialogPropertyPayload, PropertyRequiredKind, FormDialogPropertyKind } from 'src/atoms/types'; +import { createSchemaStoreFromJson, getDefaultPayload, getDuplicateName } from 'src/atoms/utils'; +import { generateId } from 'src/utils/base'; +import { readFileContent } from 'src/utils/file'; + +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 importSchema = useRecoilCallback(() => async ({ id, file }: { id: string; file: File }) => { + const content = await readFileContent(file); + importSchemaString({ id, content }); + }); + + const activatePropertyId = useRecoilCallback(({ set }) => ({ id }: { id: string }) => { + set(activePropertyIdAtom, id); + }); + + const addProperty = useRecoilCallback(({ set }) => () => { + const newPropertyId = generateId(); + set(formDialogSchemaAtom, (currentSchema) => { + return { ...currentSchema, requiredPropertyIds: [newPropertyId, ...currentSchema.requiredPropertyIds] }; + }); + set(formDialogPropertyAtom(newPropertyId), { + id: newPropertyId, + kind: 'string', + name: '', + payload: { kind: 'string' }, + examples: [], + required: true, + array: false, + }); + 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 changePropertyRequired = useRecoilCallback( + ({ set }) => ({ id, required }: { id: string; required: boolean }) => { + set(formDialogPropertyAtom(id), (currentProperty) => { + return { ...currentProperty, required }; + }); + } + ); + + const changePropertyName = useRecoilCallback(({ set }) => ({ id, name }: { id: string; name: string }) => { + set(formDialogPropertyAtom(id), (currentProperty) => { + return { ...currentProperty, name }; + }); + }); + + const changePropertyPayload = useRecoilCallback( + ({ set }) => ({ id, payload }: { id: string; payload: FormDialogPropertyPayload }) => { + set(formDialogPropertyAtom(id), (currentProperty) => { + return { ...currentProperty, payload }; + }); + } + ); + + const changePropertyArray = useRecoilCallback(({ set }) => ({ id, isArray }: { id: string; isArray: boolean }) => { + set(formDialogPropertyAtom(id), (currentProperty) => { + return { ...currentProperty, array: isArray }; + }); + }); + + const moveProperty = useRecoilCallback( + ({ set }) => ({ + id, + source, + destination, + fromIndex, + toIndex, + }: { + id: string; + source: PropertyRequiredKind; + destination: PropertyRequiredKind; + fromIndex: number; + toIndex: number; + }) => { + const toggleRequired = source !== destination; + + if (toggleRequired) { + changePropertyRequired({ id, required: source === 'optional' }); + } + + set(formDialogSchemaAtom, (currentSchema) => { + if (toggleRequired) { + //Move between two lists + const requiredPropertyIds = currentSchema.requiredPropertyIds.slice(); + const optionalPropertyIds = currentSchema.optionalPropertyIds.slice(); + + if (source === 'required') { + requiredPropertyIds.splice(fromIndex, 1); + optionalPropertyIds.splice(toIndex, 0, id); + } else { + optionalPropertyIds.splice(fromIndex, 1); + requiredPropertyIds.splice(toIndex, 0, id); + } + + return { ...currentSchema, requiredPropertyIds, optionalPropertyIds }; + } else { + // Move within a list + const propertyIds = (source === 'required' + ? currentSchema.requiredPropertyIds + : currentSchema.optionalPropertyIds + ).slice(); + + propertyIds.splice(fromIndex, 1); + propertyIds.splice(toIndex, 0, id); + + return source === 'required' + ? { ...currentSchema, requiredPropertyIds: propertyIds } + : { ...currentSchema, optionalPropertyIds: propertyIds }; + } + }); + } + ); + + const removeProperty = useRecoilCallback(({ set, reset, snapshot }) => async ({ id }: { id: string }) => { + const property = await snapshot.getPromise(formDialogPropertyAtom(id)); + + set(formDialogSchemaAtom, (currentSchema) => ({ + ...currentSchema, + requiredPropertyIds: property.required + ? currentSchema.requiredPropertyIds.filter((pId) => pId !== id) + : currentSchema.requiredPropertyIds, + optionalPropertyIds: !property.required + ? currentSchema.optionalPropertyIds.filter((pId) => pId !== id) + : currentSchema.optionalPropertyIds, + })); + + reset(formDialogPropertyAtom(id)); + + const activePropertyId = await snapshot.getPromise(activePropertyIdAtom); + + if (activePropertyId === id) { + activatePropertyId({ id: '' }); + } + }); + + const duplicateProperty = useRecoilCallback(({ set, snapshot }) => async ({ id }: { id: string }) => { + const newId = generateId(); + + const property = await snapshot.getPromise(formDialogPropertyAtom(id)); + const propertyNames = await snapshot.getPromise(formDialogSchemaPropertyNamesSelector); + const name = getDuplicateName(property.name, propertyNames); + + set(formDialogSchemaAtom, (currentSchema) => { + const propertyIds = (property.required + ? currentSchema.requiredPropertyIds + : currentSchema.optionalPropertyIds + ).slice(); + + if (property.required) { + return { ...currentSchema, requiredPropertyIds: [...propertyIds, newId] }; + } else { + return { ...currentSchema, optionalPropertyIds: [...propertyIds, newId] }; + } + }); + + set(formDialogPropertyAtom(newId), { ...property, 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))); + set(formDialogSchemaAtom, { id: name, name, requiredPropertyIds: [], optionalPropertyIds: [] }); + }); + + const setTemplates = useRecoilCallback(({ set }) => ({ templates }: { templates: string[] }) => { + set(formDialogTemplatesAtom, templates); + }); + + return { + activatePropertyId, + addProperty, + changePropertyExamples, + changePropertyKind, + changePropertyName, + changePropertyPayload, + changePropertyArray, + reset, + setTemplates, + removeProperty, + duplicateProperty, + moveProperty, + importSchema, + importSchemaString, + }; +}; + +type Handler = ReturnType; + +export const useHandlers = () => { + const handlerFuncsRef = React.useRef(getHandlers()); + return { ...handlerFuncsRef.current }; +}; diff --git a/Composer/packages/form-dialogs/src/atoms/types.ts b/Composer/packages/form-dialogs/src/atoms/types.ts new file mode 100644 index 0000000000..d819057ab8 --- /dev/null +++ b/Composer/packages/form-dialogs/src/atoms/types.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type PropertyRequiredKind = 'required' | 'optional'; + +export type FormDialogPropertyKind = 'ref' | 'number' | 'integer' | 'string' | 'array'; + +export type RefPropertyPayload = TypedPropertyPayload & { + ref: string; +}; + +export type TypedPropertyPayload = { + kind: FormDialogPropertyKind; + entities?: string[]; +}; + +const builtInFormats = ['date-time', 'date', 'time', 'email', 'uri', 'iri'] as const; + +export type BuiltInStringFormat = typeof builtInFormats[number]; + +export type StringFormatItem = { displayName: string; value: BuiltInStringFormat }; + +export const builtInStringFormats: readonly StringFormatItem[] = [ + { displayName: 'date-time', value: 'date-time' }, + { displayName: 'date', value: 'date' }, + { displayName: 'time', value: 'time' }, + { displayName: 'email', value: 'email' }, + { displayName: 'uri', value: 'uri' }, + { displayName: 'iri', value: 'iri' }, +]; + +export type StringPropertyPayload = TypedPropertyPayload & { + kind: 'string'; + enums?: string[]; + pattern?: string; + format?: BuiltInStringFormat; +}; + +export type NumberPropertyPayload = TypedPropertyPayload & { + kind: 'number'; + minimum: number; + maximum: number; +}; + +export type IntegerPropertyPayload = TypedPropertyPayload & { + kind: 'integer'; + minimum: number; + maximum: number; +}; + +export type ArrayPropertyPayload = Pick & { + kind: 'array'; + items: + | (IntegerPropertyPayload & { maxItems?: number }) + | (NumberPropertyPayload & { maxItems?: number }) + | (StringPropertyPayload & { maxItems?: number }) + | RefPropertyPayload; +}; + +export type FormDialogPropertyPayload = + | RefPropertyPayload + | StringPropertyPayload + | NumberPropertyPayload + | IntegerPropertyPayload + | ArrayPropertyPayload; + +export type FormDialogProperty = { + id: string; + kind: FormDialogPropertyKind; + name: string; + payload: FormDialogPropertyPayload; + required: boolean; + array: boolean; + examples: string[]; +}; + +export type FormDialogSchema = { + id: string; + name: string; + requiredPropertyIds: string[]; + optionalPropertyIds: string[]; +}; diff --git a/Composer/packages/form-dialogs/src/atoms/utils.ts b/Composer/packages/form-dialogs/src/atoms/utils.ts new file mode 100644 index 0000000000..168f359861 --- /dev/null +++ b/Composer/packages/form-dialogs/src/atoms/utils.ts @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + +import formatMessage from 'format-message'; +import { + builtInStringFormats, + FormDialogProperty, + FormDialogPropertyPayload, + IntegerPropertyPayload, + NumberPropertyPayload, + RefPropertyPayload, + FormDialogPropertyKind, + StringPropertyPayload, + TypedPropertyPayload, +} from 'src/atoms/types'; +import { generateId } from 'src/utils/base'; + +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!`); + } +}; + +const $refToRef = ($ref: string) => { + const [, ref] = $ref.match('template:(.*)'); + return ref; +}; + +// 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 }; + case 'number': + return { + kind: 'number', + minimum: payloadData.minimum, + maximum: payloadData.maximum, + }; + case 'integer': + return { + kind: 'integer', + minimum: payloadData.minimum, + maximum: payloadData.maximum, + }; + default: + throw new Error(`Property of type: ${kind} is not currently supported!`); + } +}; + +export const createSchemaStoreFromJson = (schemaName: string, jsonString: string) => { + const json = JSON.parse(jsonString); + + const properties = json.properties || []; + const requiredArray = (json.required || []); + const examplesRecord = >(json.$examples || {}); + + const propertyStores = Object.keys(properties).map((name) => { + const propertyData = properties[name]; + + const propertyType = propertyData?.type || 'ref'; + const array = propertyType === 'array'; + const payload = retrievePayload(propertyType, propertyData, array); + const kind = (propertyType === 'array' ? payload.kind : propertyType); + const required = requiredArray.indexOf(name) !== -1; + const examples = examplesRecord[name] || []; + + return { + id: generateId(), + name, + kind, + required, + examples, + array, + payload, + }; + }); + + return { name: schemaName, properties: propertyStores }; +}; + +const findFirstMissingIndex = (arr: number[], start: number, end: number): number => { + if (start > end) return end + 1; + + if (start + 1 !== arr[start]) return start; + + const mid = Math.floor(start + (end - start) / 2); + + if (arr[mid] === mid + 1) { + return findFirstMissingIndex(arr, mid + 1, end); + } + + return findFirstMissingIndex(arr, start, mid); +}; + +export const getDuplicateName = (name: string, allNames: readonly string[]) => { + if (!name) { + return ''; + } + + const getBestIndex = (origName: string) => { + const pattern = `${origName} - copy `; + const otherNames = allNames.filter((n) => n.startsWith(pattern) && n.endsWith(')')); + 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); + } + } catch { + continue; + } + } + + if (!indices.length) { + return 1; + } + + indices.sort((a, b) => a - b); + const maxIdx = Math.max(...indices); + + const firstAvailableIdx = findFirstMissingIndex(indices, 0, indices.length - 1); + + return firstAvailableIdx === -1 ? maxIdx + 1 : firstAvailableIdx + 1; + }; + + const cpIndex = name.indexOf(' - copy '); + const originalName = cpIndex === -1 ? name : name.substring(0, cpIndex); + + const bestIndex = getBestIndex(originalName); + + 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); + } + }; + return { + type: 'array', + items: helper(), + }; +}; + +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), + }; + default: + throw new Error(`Property type: "${property.kind}" is not supported!`); + } +}; + +//----------------------------JSON validation---------------------------- + +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; + } + } + + return !!(payloadValid && property.name); +}; + +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 }); + } + + return stringPayload.format + ? builtInStringFormats.find((f) => f.value === stringPayload.format).displayName + : formatMessage('any string'); + } + } +}; diff --git a/Composer/packages/form-dialogs/src/components/FormDialogPropertiesEditor.tsx b/Composer/packages/form-dialogs/src/components/FormDialogPropertiesEditor.tsx new file mode 100644 index 0000000000..de192298e0 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/FormDialogPropertiesEditor.tsx @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme'; +import formatMessage from 'format-message'; +import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { CommandBar, ICommandBarItemProps } from 'office-ui-fabric-react/lib/CommandBar'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { Text } from 'office-ui-fabric-react/lib/Text'; +import * as React from 'react'; +import { useRecoilValue } from 'recoil'; +import { + allFormDialogPropertyIdsSelector, + formDialogSchemaAtom, + formDialogSchemaJsonSelector, + formDialogSchemaValidSelector, +} from 'src/atoms/appState'; +import { useHandlers } from 'src/atoms/handlers'; +import { CommandBarUploadButton } from 'src/components/common/CommandBarUpload'; +import { FormDialogSchemaDetails } from 'src/components/property/FormDialogSchemaDetails'; +import { useUndoKeyBinding } from 'src/utils/hooks/useUndoKeyBinding'; + +const downloadFile = async (fileName: string, schemaExtension: string, content: string) => { + const blob = new Blob([content], { type: 'application/json' }); + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = `${fileName}.${schemaExtension}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; + +const Root = styled(Stack)({ + backgroundColor: NeutralColors.gray20, +}); + +const EditorRoot = styled.div({ + display: 'flex', + flex: 1, + justifyContent: 'center', + overflowY: 'auto', +}); + +const ListCommandBar = styled(CommandBar)({ + position: 'relative', + zIndex: 1, + margin: '0 0 1px 0', + '& .ms-CommandBar': { + padding: '0 12px', + }, + '& .ms-OverflowSet-item': { + alignItems: 'center', + }, +}); + +const SchemaName = styled(Stack)({ + height: 44, + padding: '0 24px', + backgroundColor: FluentTheme.palette.white, +}); + +type Props = { + schemaExtension: string; + onReset: () => void; + onGenerateDialog: (formDialogSchemaJson: string) => void; +}; + +export const FormDialogPropertiesEditor = React.memo((props: Props) => { + const { onReset, onGenerateDialog, schemaExtension } = props; + + const schema = useRecoilValue(formDialogSchemaAtom); + const propertyIds = useRecoilValue(allFormDialogPropertyIdsSelector); + const schemaValid = useRecoilValue(formDialogSchemaValidSelector); + const schemaJson = useRecoilValue(formDialogSchemaJsonSelector); + const { importSchema, addProperty } = useHandlers(); + + const schemaIdRef = React.useRef(schema.id); + + useUndoKeyBinding(); + + React.useEffect(() => { + schemaIdRef.current = schema.id; + }, [schema.id]); + + const upload = (file: File) => { + importSchema({ id: schema.id, file }); + }; + + const menuItems: ICommandBarItemProps[] = [ + { + key: 'add', + text: formatMessage('Add Property'), + iconProps: { iconName: 'Add' }, + title: formatMessage('Add Property'), + ariaLabel: formatMessage('Add Property'), + onClick: () => { + addProperty(); + }, + }, + { + key: 'import', + onRender: () => , + }, + { + key: 'export', + iconProps: { iconName: 'Export' }, + text: formatMessage('Export JSON'), + title: formatMessage('Export JSON'), + ariaLabel: formatMessage('Export JSON'), + disabled: !propertyIds.length || !schemaValid, + onClick: () => { + downloadFile(schema.name, schemaExtension, schemaJson); + }, + }, + { + key: 'reset', + iconProps: { iconName: 'Clear' }, + text: formatMessage('Clear all'), + title: formatMessage('Clear all'), + ariaLabel: formatMessage('Clear all'), + onClick: () => { + if (confirm(formatMessage('Are you sure you want to start over? Your progress will be lost.'))) { + onReset(); + } + }, + }, + ]; + + const generateDialog = React.useCallback(() => { + onGenerateDialog(schemaJson); + }, [schemaJson, onGenerateDialog]); + + const farItems: ICommandBarItemProps[] = [ + { + key: 'generate', + onRender: () => ( + + ), + }, + ]; + + return ( + + + + {schema.name} + + + + + + ); +}); diff --git a/Composer/packages/form-dialogs/src/components/common/Collapsible.tsx b/Composer/packages/form-dialogs/src/components/common/Collapsible.tsx new file mode 100644 index 0000000000..71be98a1d9 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/common/Collapsible.tsx @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { NeutralColors } from '@uifabric/fluent-theme'; +import { ActionButton } from 'office-ui-fabric-react/lib/Button'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import * as React from 'react'; + +const Title = styled(ActionButton)(({ isCollapsed }: { isCollapsed: boolean }) => ({ + width: 'fit-content', + margin: 0, + padding: 0, + '& i': { + transform: isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)', + transition: '250ms transform ease', + color: NeutralColors.gray120, + }, +})); + +type Props = React.PropsWithChildren<{ + collapsed?: boolean; + onRenderTitle: () => React.ReactNode; + onToggleCollapsed?: (collapsed: boolean) => void; +}>; + +export const Collapsible = (props: Props) => { + const { collapsed = false, onRenderTitle, onToggleCollapsed, children } = props; + + const { 0: isCollapsed, 1: setCollapsed } = React.useState(collapsed); + + React.useEffect(() => { + if (collapsed !== isCollapsed) { + setCollapsed(collapsed); + } + }, [collapsed]); + + const click = React.useCallback(() => { + const newCollapsed = !isCollapsed; + setCollapsed(newCollapsed); + if (onToggleCollapsed) { + onToggleCollapsed(newCollapsed); + } + }, [isCollapsed, onToggleCollapsed]); + + return ( + + + {onRenderTitle()} + + {!isCollapsed && children} + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/common/CommandBarUpload.tsx b/Composer/packages/form-dialogs/src/components/common/CommandBarUpload.tsx new file mode 100644 index 0000000000..d609fe482c --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/common/CommandBarUpload.tsx @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import formatMessage from 'format-message'; +import { CommandBarButton } from 'office-ui-fabric-react/lib/Button'; +import * as React from 'react'; + +type Props = { + /** + * Allowed files extension. + */ + accept: string; + /** + * Callback for when a file is selected. + */ + onUpload: (file: File) => void; +}; + +export const CommandBarUploadButton = (props: Props) => { + const { onUpload, accept } = props; + + const inputFileRef = React.useRef(); + const onClickImport = () => { + inputFileRef.current.click(); + }; + + const onChange = () => { + if (inputFileRef.current.files) { + onUpload(inputFileRef.current.files.item(0)); + } + }; + + const title = formatMessage('Import schema'); + + return ( + <> + + + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/common/FieldLabel.tsx b/Composer/packages/form-dialogs/src/components/common/FieldLabel.tsx new file mode 100644 index 0000000000..07a1b435fa --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/common/FieldLabel.tsx @@ -0,0 +1,25 @@ +// 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 { HelpTooltip } from 'src/components/common/HelpTooltip'; + +type Props = { + tooltipId: string; + helpText: string; + defaultRender: React.ReactNode; + optional?: boolean; +}; + +export const FieldLabel = React.memo((props: Props) => { + return ( + + {props.defaultRender} + {props.optional ? ({formatMessage('optional')}) : null} + + + ); +}); diff --git a/Composer/packages/form-dialogs/src/components/common/HelpTooltip.tsx b/Composer/packages/form-dialogs/src/components/common/HelpTooltip.tsx new file mode 100644 index 0000000000..8ad5c8e316 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/common/HelpTooltip.tsx @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FluentTheme } from '@uifabric/fluent-theme'; +import { Icon, IIconProps, IIconStyles } from 'office-ui-fabric-react/lib/Icon'; +import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; +import { classNamesFunction } from 'office-ui-fabric-react/lib/Utilities'; +import * as React from 'react'; + +const iconStyles = classNamesFunction()({ + root: { + color: FluentTheme.palette.neutralSecondary, + fontSize: 12, + lineHeight: '12px', + cursor: 'default', + }, +}); + +type Props = { + tooltipId: string; + helpMessage: string; +}; + +export const HelpTooltip = React.memo((props: Props) => { + return ( + + + + ); +}); diff --git a/Composer/packages/form-dialogs/src/components/common/ValuePicker.tsx b/Composer/packages/form-dialogs/src/components/common/ValuePicker.tsx new file mode 100644 index 0000000000..b8e4c708c2 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/common/ValuePicker.tsx @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useId } from '@uifabric/react-hooks'; +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 'src/components/tags/TagInput'; + +type Props = { + /** + * The label of the value picker control. + */ + label: string; + /** + * Callback for custom rendering of label. + */ + onRenderLabel?: (props: Props, defaultRender: (props: Props) => JSX.Element) => JSX.Element; + /** + * Array of values to show. + */ + values: string[]; + /** + * Callback to update hte values. + */ + onChange: (tags: string[]) => void; +}; + +const defaultLabelRender = (props: Props) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const textFieldId = useId('valuePickerLabel'); + return ; +}; + +export const ValuePicker = (props: Props) => { + const { values, onChange, onRenderLabel = defaultLabelRender } = props; + + return ( + + {onRenderLabel(props, defaultLabelRender)} + + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/property/AdvancedOptions.tsx b/Composer/packages/form-dialogs/src/components/property/AdvancedOptions.tsx new file mode 100644 index 0000000000..dc8575d629 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/AdvancedOptions.tsx @@ -0,0 +1,74 @@ +// 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 'src/atoms/types'; +import { Collapsible } from 'src/components/common/Collapsible'; +import { ValuePicker } from 'src/components/common/ValuePicker'; + +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 new file mode 100644 index 0000000000..864fd215d7 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/FormDialogPropertyCard.tsx @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme'; +import { useId } from '@uifabric/react-hooks'; +import formatMessage from 'format-message'; +import { CommandBarButton, IconButton } from 'office-ui-fabric-react/lib/Button'; +import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox'; +import { FocusZone } from 'office-ui-fabric-react/lib/FocusZone'; +import { IOverflowSetItemProps, OverflowSet } from 'office-ui-fabric-react/lib/OverflowSet'; +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 { + ArrayPropertyPayload, + FormDialogProperty, + FormDialogPropertyPayload, + IntegerPropertyPayload, + NumberPropertyPayload, + FormDialogPropertyKind, + StringPropertyPayload, +} from 'src/atoms/types'; +import { FieldLabel } from 'src/components/common/FieldLabel'; +import { NumberPropertyContent } from 'src/components/property/NumberPropertyContent'; +import { PropertyTypeSelector } from 'src/components/property/PropertyTypeSelector'; +import { RequiredPriorityIndicator } from 'src/components/property/RequiredPriorityIndicator'; +import { StringPropertyContent } from 'src/components/property/StringPropertyContent'; + +const ContentRoot = styled.div(({ isValid }: { isValid: boolean }) => ({ + width: 720, + position: 'relative', + display: 'flex', + flexDirection: 'column', + background: FluentTheme.palette.white, + padding: '14px 24px 24px 24px', + transition: 'box-shadow 0.25s cubic-bezier(.25,.8,.25,1)', + boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)', + margin: '8px 0 10px 0', + transform: 'scale(1.025)', + transformOrigin: 'center', + '& > *:not(:last-of-type)': { + marginBottom: 16, + }, + '&::after': { + display: isValid ? 'none' : 'block', + content: '""', + position: 'absolute', + left: -1, + top: -1, + right: -1, + bottom: -1, + pointerEvents: 'none', + border: `2px solid ${FluentTheme.palette.red}`, + zIndex: 1, + }, +})); + +const ArrayCheckbox = styled(Checkbox)({ + flex: 1, + marginTop: 28, + justifyContent: '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; + dragHandleProps: DraggableProvidedDragHandleProps; + onChangeKind: (kind: FormDialogPropertyKind, payload: FormDialogPropertyPayload) => void; + onChangeName: (name: string) => void; + onChangeArray: (isArray: boolean) => void; + onChangePayload: (payload: FormDialogPropertyPayload) => void; + onActivateItem: (propertyId: string) => void; + onRemove: () => void; + onDuplicate: () => void; +}; + +export const FormDialogPropertyCard = React.memo((props: FormDialogPropertyCardProps) => { + const { + valid, + property, + dragHandleProps, + onChangeKind, + onChangeName, + onChangePayload, + onChangeArray, + onActivateItem, + onRemove, + onDuplicate, + } = props; + + const { id: propertyId, array, kind, name, payload, required } = property; + + const rootElmRef = React.useRef(); + const propertyNameTooltipId = useId('propertyName'); + const propertyArrayTooltipId = useId('propertyArray'); + + const changeArray = React.useCallback( + (_: React.FormEvent, checked: boolean) => { + onChangeArray(checked); + }, + [onChangeArray] + ); + + const changeName = React.useCallback( + (_: React.FormEvent, value: string) => { + onChangeName(value); + }, + [onChangeName] + ); + + const deactivateItem = React.useCallback(() => { + onActivateItem(''); + }, [onActivateItem]); + + 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) => ( + + ), + [] + ); + + const renderOverflowItem = React.useCallback( + (item: IOverflowSetItemProps) => , + [] + ); + + const renderOverflowButton = React.useCallback( + (overflowItems: IOverflowSetItemProps[]) => ( + + ), + [] + ); + + return ( + + + + + + + + + + + + + + + + + {isNumerical(kind) ? renderProperty(kind, payload, onChangePayload) : null} + + + + + {!isNumerical(kind) ? renderProperty(kind, payload, onChangePayload) : null} + + + ); +}); diff --git a/Composer/packages/form-dialogs/src/components/property/FormDialogSchemaDetails.tsx b/Composer/packages/form-dialogs/src/components/property/FormDialogSchemaDetails.tsx new file mode 100644 index 0000000000..3563efb2e0 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/FormDialogSchemaDetails.tsx @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { NeutralColors } from '@uifabric/fluent-theme'; +import * as React from 'react'; +import { DragDropContext, DropResult } from 'react-beautiful-dnd'; +import { useRecoilValue } from 'recoil'; +import { formDialogSchemaAtom } from 'src/atoms/appState'; +import { useHandlers } from 'src/atoms/handlers'; +import { PropertyRequiredKind } from 'src/atoms/types'; +import { PropertyList } from 'src/components/property/PropertyList'; +import { Lifetime } from 'src/utils/base'; +import { jsPropertyListClassName } from 'src/utils/constants'; + +const Root = styled.div({ + display: 'flex', + flexDirection: 'column', + height: '100%', + width: '100%', +}); + +const Separator = styled.div({ + background: NeutralColors.gray60, + height: 1, + width: 'calc(100% - 48px)', + margin: '4px 24px', +}); + +export const FormDialogSchemaDetails = () => { + const { optionalPropertyIds, requiredPropertyIds } = useRecoilValue(formDialogSchemaAtom); + const { moveProperty, activatePropertyId } = useHandlers(); + const containerRef = React.useRef(); + const dragEventRef = React.useRef(false); + + React.useEffect(() => { + const lifetime = new Lifetime(); + + const clickOutsideLists = (e: MouseEvent) => { + const { x, y } = e; + const elms = Array.prototype.slice.call( + containerRef.current.querySelectorAll(`.${jsPropertyListClassName}`) || [] + ) as HTMLDivElement[]; + if ( + !dragEventRef.current && + !elms.some((elm) => { + const { left, right, top, bottom } = elm.getBoundingClientRect(); + return x <= right && x >= left && y <= bottom && y >= top; + }) + ) { + activatePropertyId({ id: '' }); + } + }; + + if (containerRef.current) { + containerRef.current.addEventListener('click', clickOutsideLists); + lifetime.add(() => { + containerRef.current.removeEventListener('click', clickOutsideLists); + }); + } + + return () => lifetime.dispose(); + }, []); + + const dragStart = React.useCallback(() => { + dragEventRef.current = true; + }, []); + + const dragEnd = React.useCallback( + (result: DropResult) => { + dragEventRef.current = false; + + const { source, destination, draggableId } = result; + + if (!destination || (destination.droppableId === source.droppableId && destination.index === source.index)) { + return; + } + + moveProperty({ + id: draggableId, + source: source.droppableId as PropertyRequiredKind, + destination: destination.droppableId as PropertyRequiredKind, + fromIndex: source.index, + toIndex: destination.index, + }); + }, + [moveProperty] + ); + + return ( + + + + + + + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/property/NumberPropertyContent.tsx b/Composer/packages/form-dialogs/src/components/property/NumberPropertyContent.tsx new file mode 100644 index 0000000000..33c8583957 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/NumberPropertyContent.tsx @@ -0,0 +1,68 @@ +// 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 'src/atoms/types'; +import { FieldLabel } from 'src/components/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('Minimum help text'), minTooltipId)} + /> + + onChangePayload({ + ...payload, + maximum: payload.kind === 'integer' ? parseInt(value, 10) : parseFloat(value), + }) + } + onRenderLabel={onRenderLabel(formatMessage('Maximum help text'), maxTooltipId)} + /> + + + ); +}); diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx new file mode 100644 index 0000000000..1be751cf55 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/PropertyList.tsx @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { NeutralColors } from '@uifabric/fluent-theme'; +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 { Droppable } from 'react-beautiful-dnd'; +import { PropertyRequiredKind } from 'src/atoms/types'; +import { PropertyListItem } from 'src/components/property/PropertyListItem'; +import { jsPropertyListClassName } from 'src/utils/constants'; + +const Root = styled(Stack)({ + position: 'relative', +}); + +const List = styled.div(({ isDraggingOver }: { isDraggingOver: boolean }) => ({ + margin: '24px 0', + backgroundColor: isDraggingOver ? NeutralColors.gray40 : 'transparent', +})); + +const Header = styled(Text)({ + position: 'absolute', + left: 24, + top: 24, + color: NeutralColors.gray130, + fontWeight: 600, +}); + +const EmptyMessage = styled(Stack)({ + height: 48, + width: 768, +}); + +type Props = { + kind: PropertyRequiredKind; + propertyIds: string[]; +}; + +const InternalPropertyList = React.memo((props: Props) => ( +
+ {props.propertyIds.length ? ( + props.propertyIds.map((propertyId, index) => ( + + )) + ) : ( + + + {formatMessage('There are no {kind} properties.', { + kind: props.kind === 'required' ? formatMessage('required') : formatMessage('optional'), + })} + + + )} +
+)); + +export const PropertyList = (props: Props) => { + const { kind } = props; + + return ( + +
+ {kind === 'required' ? formatMessage('Required properties') : formatMessage('Optional properties')} +
+ + {(provided, { isDraggingOver }) => ( +
+ + + {provided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyListItem.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyListItem.tsx new file mode 100644 index 0000000000..1a0a790db8 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/PropertyListItem.tsx @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { FluentTheme } from '@uifabric/fluent-theme'; +import { useId } from '@uifabric/react-hooks'; +import formatMessage from 'format-message'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { Text } from 'office-ui-fabric-react/lib/Text'; +import { DirectionalHint, TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; +import * as React from 'react'; +import { Draggable, DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; +import { useRecoilValue } from 'recoil'; +import { activePropertyIdAtom, formDialogPropertyAtom, formDialogPropertyValidSelector } from 'src/atoms/appState'; +import { useHandlers } from 'src/atoms/handlers'; +import { FormDialogProperty, FormDialogPropertyPayload, FormDialogPropertyKind } from 'src/atoms/types'; +import { getPropertyTypeDisplayName } from 'src/atoms/utils'; +import { FormDialogPropertyCard } from 'src/components/property/FormDialogPropertyCard'; +import { RequiredPriorityIndicator } from 'src/components/property/RequiredPriorityIndicator'; + +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, +})); + +const ItemContentRoot = styled(Stack)({ + width: 720, + height: 48, + padding: '0 24px', + position: 'relative', + cursor: 'pointer', + boxSizing: 'content-box', + background: FluentTheme.palette.white, + transition: 'box-shadow 0.3s cubic-bezier(.25,.8,.25,1)', + marginBottom: 1, +}); + +const OneLinerText = styled(Text)({ + display: 'inline-block', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', +}); + +const PropertyName = styled(OneLinerText)({ + flex: 1, +}); + +const PropertyType = styled(OneLinerText)({ + color: FluentTheme.palette.themePrimary, + textTransform: 'uppercase', + width: 100, +}); + +const ArrayText = styled(Text)({ + display: 'inline-block', + width: 130, +}); + +const ErrorIcon = styled(Stack.Item)({ + width: 20, +}); + +type ContentProps = { + valid: boolean; + property: FormDialogProperty; + dragHandleProps: DraggableProvidedDragHandleProps; + onActivateItem: (propertyId: string) => void; +}; +const PropertyListItemContent = React.memo((props: ContentProps) => { + const { property, valid, dragHandleProps, onActivateItem } = props; + + const tooltipId = useId('PropertyListItemContent'); + + const activateItem = React.useCallback(() => { + onActivateItem(property.id); + }, [onActivateItem, property.id]); + + const propertyTypeDisplayName = React.useMemo(() => getPropertyTypeDisplayName(property), [property]); + + return ( + + + + + {!valid && } + + + {property.name || formatMessage('[no name]')} + + {propertyTypeDisplayName} + + {property.array ? formatMessage('Accepts multiple values') : ''} + + + ); +}); + +type Props = { + index: number; + propertyId: string; +}; + +export const PropertyListItem = React.memo((props: Props) => { + const { propertyId, index } = props; + + const activePropertyId = useRecoilValue(activePropertyIdAtom); + const property = useRecoilValue(formDialogPropertyAtom(propertyId)); + const valid = useRecoilValue(formDialogPropertyValidSelector(propertyId)); + + const { + changePropertyKind, + changePropertyName, + changePropertyPayload, + changePropertyArray, + activatePropertyId, + removeProperty, + duplicateProperty, + } = useHandlers(); + + const onChangePropertyKind = React.useCallback( + (kind: FormDialogPropertyKind, payload: FormDialogPropertyPayload) => { + changePropertyKind({ id: propertyId, kind, payload }); + }, + [changePropertyKind, propertyId] + ); + + const onChangePropertyName = React.useCallback( + (name: string) => { + changePropertyName({ id: propertyId, name }); + }, + [changePropertyName, propertyId] + ); + + const onChangePayload = React.useCallback( + (payload: FormDialogPropertyPayload) => { + changePropertyPayload({ id: propertyId, payload }); + }, + [changePropertyPayload, propertyId] + ); + + const onChangeArray = React.useCallback( + (isArray: boolean) => { + changePropertyArray({ id: propertyId, isArray }); + }, + [changePropertyArray] + ); + + const onActivateItem = React.useCallback( + (propertyId: string) => { + activatePropertyId({ id: propertyId }); + }, + [activatePropertyId] + ); + + const onRemove = React.useCallback(() => { + if (confirm(formatMessage('Are you sure you want to remove "{propertyName}"?', { propertyName: property.name }))) { + removeProperty({ id: propertyId }); + } + }, [removeProperty, propertyId]); + + const onDuplicate = React.useCallback(() => { + duplicateProperty({ id: propertyId }); + }, [duplicateProperty, propertyId]); + + return ( + + {(provided, { isDragging }) => ( +
+ + {propertyId === activePropertyId ? ( + + ) : ( + + )} + +
+ )} +
+ ); +}); diff --git a/Composer/packages/form-dialogs/src/components/property/PropertyTypeSelector.tsx b/Composer/packages/form-dialogs/src/components/property/PropertyTypeSelector.tsx new file mode 100644 index 0000000000..e99603cad6 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/PropertyTypeSelector.tsx @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useId } from '@uifabric/react-hooks'; +import formatMessage from 'format-message'; +import { Dropdown, DropdownMenuItemType, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; +import * as React from 'react'; +import { useRecoilValue } from 'recoil'; +import { formDialogTemplatesAtom } from 'src/atoms/appState'; +import { + ArrayPropertyPayload, + BuiltInStringFormat, + builtInStringFormats, + FormDialogPropertyPayload, + IntegerPropertyPayload, + NumberPropertyPayload, + RefPropertyPayload, + FormDialogPropertyKind, + StringPropertyPayload, +} from 'src/atoms/types'; +import { FieldLabel } from 'src/components/common/FieldLabel'; + +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; + } + } +}; + +type Props = { + isArray: boolean; + kind: FormDialogPropertyKind; + payload: FormDialogPropertyPayload; + onChange: (kind: FormDialogPropertyKind, payload?: FormDialogPropertyPayload) => void; +}; + +export const PropertyTypeSelector = React.memo((props: Props) => { + const { isArray, kind, payload, onChange } = props; + + const propertyTypeTooltipId = useId('propertyType'); + const isEnumList = kind === 'string' && !!(payload as StringPropertyPayload).enums; + + const templates = useRecoilValue(formDialogTemplatesAtom); + const templateOptions = 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', + }, + ...stringOptions, + ...templateOptions, + ].sort((a, b) => a.text.localeCompare(b.text)) as IDropdownOption[], + [kind, stringOptions, templateOptions] + ); + + 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); + }, + [payload, isArray, kind, onChange] + ); + + 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 ( + + ); +}); diff --git a/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx b/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx new file mode 100644 index 0000000000..e7cc5aa5a9 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/RequiredPriorityIndicator.tsx @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +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 { useRecoilValue } from 'recoil'; +import { formDialogSchemaAtom } from 'src/atoms/appState'; + +const Root = styled(Stack)({ + width: 140, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', +}); + +type Props = { + required: boolean; + propertyId: string; +}; + +const usePropertyPriority = (required: boolean, propertyId: string) => { + const { requiredPropertyIds, optionalPropertyIds } = useRecoilValue(formDialogSchemaAtom); + + return React.useMemo( + () => (required ? requiredPropertyIds.indexOf(propertyId) : optionalPropertyIds.indexOf(propertyId)), + [required, propertyId, requiredPropertyIds, optionalPropertyIds] + ); +}; + +export const RequiredPriorityIndicator = React.memo((props: Props) => { + const { required, propertyId } = props; + const priority = usePropertyPriority(required, propertyId) + 1; + + const requiredText = React.useMemo(() => (required ? formatMessage('Required') : formatMessage('Optional')), [ + required, + ]); + + const content = React.useMemo( + () => + required + ? formatMessage('{requiredText} | Priority: {priority}', { requiredText, priority }) + : 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 new file mode 100644 index 0000000000..804730359f --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/property/StringPropertyContent.tsx @@ -0,0 +1,44 @@ +// 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 'src/atoms/types'; +import { FieldLabel } from 'src/components/common/FieldLabel'; +import { ValuePicker } from 'src/components/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/tags/ContentEditable.tsx b/Composer/packages/form-dialogs/src/components/tags/ContentEditable.tsx new file mode 100644 index 0000000000..b7b42506ab --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/tags/ContentEditable.tsx @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable react/jsx-sort-props */ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable react/no-danger */ + +import * as React from 'react'; +import { safeHtmlString } from 'src/components/tags/utils'; + +type ContentEditableProps = { + value: string; + className?: string; + innerEditableRef: React.RefObject; + inputRef: React.RefObject; + change: (value: string) => void; + remove: () => void; + validator?: (value: string) => boolean; + removeOnBackspace?: boolean; +}; + +export const ContentEditable = (props: ContentEditableProps) => { + const { value, className = '', innerEditableRef, removeOnBackspace, change, validator } = props; + + let removed = false; + let preFocusedValue = ''; + + const getValue = () => props.innerEditableRef.current?.innerText || ''; + + const focusInputRef = () => { + const { inputRef } = props; + if (inputRef && inputRef.current) { + inputRef.current.focus(); + } + }; + + React.useEffect(() => { + preFocusedValue = getValue(); + }, []); + + const onPaste = (e: React.ClipboardEvent) => { + // Cancel paste event + e.preventDefault(); + + // Remove formatting from clipboard contents + const text = e.clipboardData.getData('text/plain'); + + // Insert text manually from paste command + document.execCommand('insertHTML', false, safeHtmlString(text)); + }; + + const onFocus = () => { + preFocusedValue = getValue(); + }; + + const onBlur = () => { + const ref = props.innerEditableRef.current; + if (!removed && ref) { + // On blur, if no content in tag, remove it + if (ref.innerText === '') { + props.remove(); + return; + } + + // Validate input if needed + if (validator) { + const valid = validator(getValue()); + // If invalidate, switch ref back to pre focused value + if (!valid) { + ref.innerText = preFocusedValue; + return; + } + } + change(ref.innerText); + } + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + // On enter, focus main tag input + if (e.keyCode === 13) { + e.preventDefault(); + focusInputRef(); + return; + } + + const currentVal = getValue(); + if (removeOnBackspace && e.keyCode === 8 && currentVal === '') { + removed = true; + props.remove(); + focusInputRef(); + return; + } + }; + + return ( +
+ ); +}; diff --git a/Composer/packages/form-dialogs/src/components/tags/Tag.tsx b/Composer/packages/form-dialogs/src/components/tags/Tag.tsx new file mode 100644 index 0000000000..8bd71fb640 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/tags/Tag.tsx @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import styled from '@emotion/styled'; +import { FluentTheme } from '@uifabric/fluent-theme'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import * as React from 'react'; +import { ContentEditable } from 'src/components/tags/ContentEditable'; + +const tagContentStyles = css` + outline: 0; + border: none; + white-space: nowrap; + padding: 0 4px; +`; + +const Root = styled.div({ + position: 'relative', + display: 'flex', + alignItems: 'center', + fontSize: '14px', + lineHeight: 1, + background: FluentTheme.palette.neutralLight, + color: FluentTheme.palette.neutralDark, +}); + +type TagProps = { + value: string; + index: number; + editable: boolean; + readOnly: boolean; + inputRef: React.RefObject; + update: (i: number, value: string) => void; + remove: (i: number) => void; + validator?: (val: string) => boolean; + removeOnBackspace?: boolean; +}; + +export const Tag = (props: TagProps) => { + const { value, index, editable, inputRef, validator, update, remove, readOnly, removeOnBackspace } = props; + const innerEditableRef = React.useRef(); + + const onRemove = () => remove(index); + + return ( + + {!editable &&
{value}
} + {editable && ( + update(index, newValue)} + css={tagContentStyles} + innerEditableRef={innerEditableRef} + inputRef={inputRef} + remove={onRemove} + removeOnBackspace={removeOnBackspace} + validator={validator} + value={value} + /> + )} + {!readOnly && ( + + )} +
+ ); +}; diff --git a/Composer/packages/form-dialogs/src/components/tags/TagInput.tsx b/Composer/packages/form-dialogs/src/components/tags/TagInput.tsx new file mode 100644 index 0000000000..b9a842e967 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/tags/TagInput.tsx @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { FluentTheme } from '@uifabric/fluent-theme'; +import formatMessage from 'format-message'; +import * as React from 'react'; +import { Tag } from 'src/components/tags/Tag'; + +const Root = styled.div({ + position: 'relative', + boxSizing: 'border-box', + borderRadius: 2, + width: '100%', + height: 'auto', + minHeight: '32px', + display: 'flex', + flexWrap: 'wrap', + flexDirection: 'row', + alignItems: 'stretch', + background: FluentTheme.palette.white, + padding: '0px 4px 4px 0', + border: `1px solid ${FluentTheme.palette.neutralTertiary}`, + '&:focus-within ': { + borderColor: FluentTheme.palette.neutralTertiary, + '&::after': { + content: '""', + position: 'absolute', + left: -1, + top: -1, + right: -1, + bottom: -1, + pointerEvents: 'none', + borderRadius: 2, + border: `2px solid ${FluentTheme.palette.themePrimary}`, + zIndex: 1, + }, + }, + '& *': { + boxSizing: 'border-box', + }, + '& > *:not(input)': { + margin: '4px 0 0 4px', + }, +}); + +const Input = styled.input({ + padding: '0px 8px', + width: 'auto', + minWidth: 0, + flexGrow: 1, + fontSize: '13px', + lineHeight: 1, + background: 'transparent', + color: FluentTheme.palette.neutralDark, + border: 'none', + outline: 0, + boxShadow: 'none', + height: '30px', + '&::placeholder': { + color: FluentTheme.palette.neutralSecondary, + }, + '&:focus': { + border: 'none', + outline: 'none', + outlineOffset: '-2px', + }, + '&:not(:first-of-type)': { + marginLeft: '4px', + }, +}); + +type TagInputProps = { + tags: string[]; + onChange: (tags: string[]) => void; + placeholder?: string; + maxTags?: number; + validator?: (val: string) => boolean; + editable?: boolean; + readOnly?: boolean; + removeOnBackspace?: boolean; +}; + +export const TagInput = (props: TagInputProps) => { + const { tags, validator, removeOnBackspace, onChange, editable, maxTags, placeholder, readOnly } = props; + + const inputRef = React.useRef(); + const { 0: input, 1: setInput } = React.useState(''); + + const onInputChange = (e: React.ChangeEvent) => { + setInput(e.target.value); + }; + + const addTag = (value: string) => { + const clonedTags = [...tags]; + if (clonedTags.indexOf(value) === -1) { + clonedTags.push(value); + onChange(clonedTags); + } + setInput(''); + }; + + const removeTag = (i: number) => { + const clonedTags = [...tags]; + clonedTags.splice(i, 1); + onChange(clonedTags); + }; + + const onInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + // Prevent form submission if tag input is nested in
+ e.preventDefault(); + + // If input is blank, do nothing + if (!input) { + return; + } + + // Check if input is valid + const valid = validator !== undefined ? validator(input) : true; + if (!valid) { + return; + } + + // Add input to tag list + addTag(input); + } else if (removeOnBackspace && (e.key === 'Backspace' || e.key === 'delete')) { + // If currently typing, do nothing + if (input) { + return; + } + + // If input is blank, remove previous tag + removeTag(tags.length - 1); + } + }; + + const updateTag = (i: number, value: string) => { + const clonedTags = [...tags]; + const numOccurrencesOfValue = tags.reduce( + (prev, currentValue, index) => prev + (currentValue === value && index !== i ? 1 : 0), + 0 + ); + if (numOccurrencesOfValue > 0) { + clonedTags.splice(i, 1); + } else { + clonedTags[i] = value; + } + onChange(clonedTags); + }; + + const maxTagsReached = maxTags !== undefined ? tags.length >= maxTags : false; + const isEditable = readOnly ? false : editable || false; + const showInput = !readOnly && !maxTagsReached; + + return ( + + {tags.map((tag, i) => ( + + ))} + {showInput && ( + + )} + + ); +}; diff --git a/Composer/packages/form-dialogs/src/components/tags/utils.ts b/Composer/packages/form-dialogs/src/components/tags/utils.ts new file mode 100644 index 0000000000..5df7253a80 --- /dev/null +++ b/Composer/packages/form-dialogs/src/components/tags/utils.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const removeLineBreaks = (value: string) => { + return value.replace(/(\r\n|\n|\r)/gm, ''); +}; + +// TAKEN FROM - https://github.com/janl/mustache.js/blob/master/mustache.js#L55 +const htmlEntityMap: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": "'", + '/': '/', + '`': '`', + '=': '=', +}; +const escapeHtml = (value: string) => { + // eslint-disable-next-line no-useless-escape + return String(value).replace(/[&<>"'`=\/]/g, (s) => htmlEntityMap[s]); +}; + +export const safeHtmlString = (value: string) => { + return escapeHtml(removeLineBreaks(value)); +}; diff --git a/Composer/packages/form-dialogs/src/demo/Demo.tsx b/Composer/packages/form-dialogs/src/demo/Demo.tsx new file mode 100644 index 0000000000..744fb09f5c --- /dev/null +++ b/Composer/packages/form-dialogs/src/demo/Demo.tsx @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// tslint:disable:no-var-requires no-require-imports + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { DemoApp } from 'src/demo/DemoApp'; + +require('./stylesheet.css'); + +const rootElement = document.getElementById('root'); + +const renderApp = () => { + ReactDOM.render(, rootElement); +}; + +renderApp(); + +// Hot Module Replacement APIs +if (module.hot) { + module.hot.accept('./DemoApp', () => { + renderApp(); + }); +} diff --git a/Composer/packages/form-dialogs/src/demo/DemoApp.tsx b/Composer/packages/form-dialogs/src/demo/DemoApp.tsx new file mode 100644 index 0000000000..4d21ba3332 --- /dev/null +++ b/Composer/packages/form-dialogs/src/demo/DemoApp.tsx @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { initializeIcons } from '@uifabric/icons'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import * as React from 'react'; +import { atom, atomFamily, RecoilRoot, selector, useRecoilCallback, useRecoilValue } from 'recoil'; +import { FormDialogSchemaEditor } from 'src/index'; + +initializeIcons(); + +const templates = [ + 'age.schema', + 'datetime.schema', + 'dimension.schema', + 'geography.schema', + 'money.schema', + 'ordinal.schema', + 'temperature.schema', +]; + +const sandwichContent = ` +{ + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": { + "Quantity": { + "type": "number", + "minimum": 1, + "maximum": 10 + }, + "Length": { + "$ref": "template:dimension.schema" + }, + "Name": { + "type": "string", + "$entities": [ + "personName", + "utterance" + ] + }, + "Bread": { + "type": "string", + "enum": [ + "multiGrainWheat", + "rye", + "white", + "wholeWheat" + ] + }, + "Meat": { + "type": "string", + "enum": [ + "bacon", + "chicken", + "ham", + "pulled pork", + "roast beef", + "salami", + "turkey", + "none" + ] + }, + "Cheese": { + "type": "string", + "enum": [ + "american", + "cheddar", + "feta", + "gouda", + "pepper jack", + "provolone", + "swiss", + "none" + ] + }, + "Toppings": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "tomato", + "lettuce", + "pickles", + "greenPeppers", + "redPepppers", + "whiteOnions", + "redOnions" + ], + "maxItems": 3 + } + }, + "Sauces": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "pepper", + "salt", + "yellowMustard", + "dijonMustard", + "mayo", + "vinegar" + ] + } + }, + "Price": { + "$ref": "template:money.schema" + } + }, + "required": [ + "Name", + "Bread", + "Cheese", + "Meat", + "Toppings", + "Sauces", + "Price" + ], + "$examples": { + "personName": [ + "Bart Simpson", + "Thomas Matthews", + "Christopher Robin" + ] + } +} +`; + +type Item = { id: string; content: string }; + +const storeAtom = atom({ + key: 'store', + default: ['sandwich'], +}); + +const itemNameAtom = atom({ + key: 'itemName', + default: '', +}); + +const selectedItemIdAtom = atom({ + key: 'selectedItemId', + default: '', +}); + +const storeItemAtom = atomFamily({ + key: 'storeItem', + default: { id: 'sandwich', content: sandwichContent }, +}); + +const selectedItemAtom = selector({ + key: 'selectedItem', + get: ({ get }) => { + const selectedId = get(selectedItemIdAtom); + return get(storeItemAtom(selectedId)); + }, +}); + +const useStore = () => { + const addItem = useRecoilCallback(({ set, snapshot }) => async (content: string) => { + const id = await snapshot.getPromise(itemNameAtom); + set(storeAtom, (cur) => { + return [...cur, id]; + }); + set(storeItemAtom(id), { id, content }); + }); + + const updateItem = useRecoilCallback(({ set }) => (id: string, content: string) => { + set(storeItemAtom(id), { id, content }); + }); + + const selectItem = useRecoilCallback(({ set }) => (id: string) => { + set(selectedItemIdAtom, id); + }); + + const setNewItemName = useRecoilCallback(({ set }) => (name: string) => { + set(itemNameAtom, name); + }); + + return { addItem, updateItem, selectItem, setNewItemName }; +}; + +const InternalDemoApp = () => { + const items = useRecoilValue(storeAtom); + const selectedItemId = useRecoilValue(selectedItemIdAtom); + const selectedItem = useRecoilValue(selectedItemAtom); + const newItemName = useRecoilValue(itemNameAtom); + const { addItem, selectItem, setNewItemName, updateItem } = useStore(); + + const onAddItem = React.useCallback(() => { + addItem('{}'); + setNewItemName(''); + }, []); + + const onUpdateItem = React.useCallback( + (id: string, content: string) => { + if (selectedItemId !== id) { + return; + } + updateItem(id, content); + }, + [selectedItemId, updateItem] + ); + + const onChangeName = React.useCallback( + (_: React.FormEvent, name: string) => { + setNewItemName(name); + }, + [setNewItemName] + ); + + return ( + + + + e.key === 'Enter' && onAddItem()} + > + + + {items.map((item) => ( + selectItem(item)} + > + {item} + + ))} + + {selectedItemId && ( + + console.log(schema)} + onSchemaUpdated={onUpdateItem} + /> + + )} + + ); +}; + +export const DemoApp = () => { + return ( + + + + ); +}; diff --git a/Composer/packages/form-dialogs/src/demo/index.html b/Composer/packages/form-dialogs/src/demo/index.html new file mode 100644 index 0000000000..3f9fcf714b --- /dev/null +++ b/Composer/packages/form-dialogs/src/demo/index.html @@ -0,0 +1,10 @@ + + + + + Dialog Generation + + +
+ + diff --git a/Composer/packages/form-dialogs/src/demo/stylesheet.css b/Composer/packages/form-dialogs/src/demo/stylesheet.css new file mode 100644 index 0000000000..f4f20af3f2 --- /dev/null +++ b/Composer/packages/form-dialogs/src/demo/stylesheet.css @@ -0,0 +1,11 @@ +@import url('https://fonts.googleapis.com/css2?family=Lato:wght@300;400&display=swap'); + +body { + font-family: 'Lato', sans-serif; + font-weight: 400; + margin: 0; +} + +* { + transition: background-color 250ms; +} diff --git a/Composer/packages/form-dialogs/src/index.ts b/Composer/packages/form-dialogs/src/index.ts new file mode 100644 index 0000000000..d6adb1ad28 --- /dev/null +++ b/Composer/packages/form-dialogs/src/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export * from 'src/FormDialogSchemaEditor'; +export * from 'src/components/property/FormDialogPropertyCard'; diff --git a/Composer/packages/form-dialogs/src/utils/base.ts b/Composer/packages/form-dialogs/src/utils/base.ts new file mode 100644 index 0000000000..327f1b2cc7 --- /dev/null +++ b/Composer/packages/form-dialogs/src/utils/base.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + +export const generateId = () => { + const arr = crypto.getRandomValues(new Uint32Array(1)); + return `${arr[0]}`; +}; + +type Disposable = { + dispose: () => void; +}; + +type DisposableAdditionOptions = Partial<{ + ignoreAfterDisposal: boolean; +}>; + +export class Lifetime implements Disposable { + private _disposables: Disposable[]; + private _isDisposed = false; + + constructor() { + this._disposables = []; + } + + public add(disposable: Disposable | Function, options?: DisposableAdditionOptions) { + const { ignoreAfterDisposal } = options || { ignoreAfterDisposal: false }; + + if (this._isDisposed) { + if (ignoreAfterDisposal) { + return this; + } + + throw new Error('A lifetime cannot add disposables after being disposed. Create a new lifetime instead.'); + } + + // tslint:disable-next-line: no-object-literal-type-assertion + const d = typeof disposable === 'function' ? { dispose: disposable } : disposable; + + this._disposables.push(d); + + return this; + } + + public dispose() { + if (!this._isDisposed) { + this._disposables.forEach((d) => d.dispose()); + this._isDisposed = true; + } + } +} diff --git a/Composer/packages/form-dialogs/src/utils/constants.ts b/Composer/packages/form-dialogs/src/utils/constants.ts new file mode 100644 index 0000000000..4c0422dbbf --- /dev/null +++ b/Composer/packages/form-dialogs/src/utils/constants.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const jsPropertyListClassName = 'jsPropertyListClassName'; diff --git a/Composer/packages/form-dialogs/src/utils/file.ts b/Composer/packages/form-dialogs/src/utils/file.ts new file mode 100644 index 0000000000..2a40f7a712 --- /dev/null +++ b/Composer/packages/form-dialogs/src/utils/file.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + +export const readFileContent = async (file: File): Promise => { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + + fileReader.readAsText(file); + fileReader.onload = () => resolve(fileReader.result); + fileReader.onerror = reject; + }); +}; diff --git a/Composer/packages/form-dialogs/src/utils/hooks/useKeyBinding.ts b/Composer/packages/form-dialogs/src/utils/hooks/useKeyBinding.ts new file mode 100644 index 0000000000..12dcc69dc1 --- /dev/null +++ b/Composer/packages/form-dialogs/src/utils/hooks/useKeyBinding.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; +import { Lifetime } from 'src/utils/base'; + +export const useKeyBinding = (kind: 'keydown' | 'keyup', cb: (e: KeyboardEvent) => void) => { + const callback = React.useCallback(cb, [cb]); + React.useEffect(() => { + const lifetime = new Lifetime(); + + document.body.addEventListener(kind, callback); + lifetime.add(() => { + document.body.removeEventListener(kind, callback); + }); + + return () => lifetime.dispose(); + }, [callback]); +}; diff --git a/Composer/packages/form-dialogs/src/utils/hooks/useTraceProps.ts b/Composer/packages/form-dialogs/src/utils/hooks/useTraceProps.ts new file mode 100644 index 0000000000..adfaf96cc7 --- /dev/null +++ b/Composer/packages/form-dialogs/src/utils/hooks/useTraceProps.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/consistent-type-assertions */ + +import * as React from 'react'; + +export const useTraceProps = >(props: T, prefix?: string) => { + const prev = React.useRef(props); + React.useEffect(() => { + const changedProps = Object.entries(props).reduce((ps, [k, v]) => { + if (prev.current[k] !== v) { + ps[k] = [prev.current[k], v]; + } + return ps; + }, >{}); + if (Object.keys(changedProps).length > 0) { + // eslint-disable-next-line no-console + console.log(`${prefix || ''}: changed props:`, changedProps); + } + prev.current = props; + }); +}; diff --git a/Composer/packages/form-dialogs/src/utils/hooks/useUndoKeyBinding.ts b/Composer/packages/form-dialogs/src/utils/hooks/useUndoKeyBinding.ts new file mode 100644 index 0000000000..753f9b1001 --- /dev/null +++ b/Composer/packages/form-dialogs/src/utils/hooks/useUndoKeyBinding.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { useKeyBinding } from 'src/utils/hooks/useKeyBinding'; + +export const useUndoKeyBinding = () => { + useKeyBinding('keyup', (e) => { + if (e.ctrlKey) { + switch (e.key.toLowerCase()) { + case 'z': { + if (e.shiftKey) { + //TODO + } else { + //TODO + } + } + } + } + }); +}; diff --git a/Composer/packages/form-dialogs/tools/devServer.js b/Composer/packages/form-dialogs/tools/devServer.js new file mode 100644 index 0000000000..dc88aec42a --- /dev/null +++ b/Composer/packages/form-dialogs/tools/devServer.js @@ -0,0 +1,24 @@ +'use strict'; + +const express = require('express'); +const webpack = require('webpack'); +const webpackDevMiddleware = require('webpack-dev-middleware'); +const webpackHotMiddleware = require('webpack-hot-middleware'); +const webpackConfig = require('../webpack.config.js'); + +const app = express(); +const compiler = webpack(webpackConfig); +const port = 3001; + +app.use( + webpackDevMiddleware(compiler, { + noInfo: true, + publicPath: webpackConfig.output.publicPath, + stats: { colors: true }, + }) +); +app.use(webpackHotMiddleware(compiler)); + +app.listen(port, function () { + console.log(`==> Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`); +}); diff --git a/Composer/packages/form-dialogs/tsconfig.json b/Composer/packages/form-dialogs/tsconfig.json new file mode 100644 index 0000000000..f11b7b0fa4 --- /dev/null +++ b/Composer/packages/form-dialogs/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "sourceMap": true, + "noImplicitAny": true, + "module": "esnext", + "target": "es6", + "importHelpers": true, + "strictNullChecks": false, + "noUnusedParameters": true, + "keyofStringsOnly": true, + "baseUrl": ".", + "paths": { + "src/*": ["src/*"] + } + }, + "include": ["src/**/*"] +} diff --git a/Composer/packages/form-dialogs/tsconfig.lib.json b/Composer/packages/form-dialogs/tsconfig.lib.json new file mode 100644 index 0000000000..11eb15067c --- /dev/null +++ b/Composer/packages/form-dialogs/tsconfig.lib.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noImplicitAny": true, + "declarationMap": true, + "declaration": true, + "outDir": "./lib/", + "declarationDir": "./lib/", + "module": "esnext", + "target": "es6", + "importHelpers": true, + "strictNullChecks": false, + "baseUrl": ".", + "paths": { + "src/*": ["src/*"] + } + }, + "include": ["src/**/*"] +} diff --git a/Composer/packages/form-dialogs/webpack.config.js b/Composer/packages/form-dialogs/webpack.config.js new file mode 100644 index 0000000000..0eab0af444 --- /dev/null +++ b/Composer/packages/form-dialogs/webpack.config.js @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-var-requires */ +'use strict'; + +const path = require('path'); + +const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); +const TerserJSPlugin = require('terser-webpack-plugin'); + +const entry = { app: ['./src/demo/Demo.tsx'] }; + +const plugins = [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + __DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'true')), + }), + new HtmlWebpackPlugin({ + template: 'src/demo/index.html', + }), +]; + +let cssLoader; + +if (process.env.NODE_ENV === 'production') { + const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + + cssLoader = { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }; + + plugins.push( + new MiniCssExtractPlugin({ + filename: '[name].min.css', + }) + ); +} else { + const WebpackNotifierPlugin = require('webpack-notifier'); + + entry.app.unshift('webpack-hot-middleware/client'); + + cssLoader = { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }; + + plugins.push(new WebpackNotifierPlugin(), new webpack.HotModuleReplacementPlugin()); +} + +module.exports = { + mode: 'development', + + devtool: 'source-map', + + resolve: { + // Add '.ts' and '.tsx' as resolvable extensions. + extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js'], + modules: [path.resolve('.'), 'node_modules'], + }, + + module: { + rules: [ + { + enforce: 'pre', + test: /\.js$/, + loader: 'source-map-loader', + exclude: path.resolve(__dirname, 'node_modules'), + }, + { + test: /\.(ts|tsx)$/, + enforce: 'pre', + use: [ + { + options: { + formatter: 'react-dev-utils/eslintFormatter', + eslintPath: 'eslint', + quiet: true, + }, + loader: 'eslint-loader', + }, + ], + include: path.resolve(__dirname, 'src'), + exclude: path.resolve(__dirname, 'node_modules'), + }, + { + test: /\.tsx?$/, + loader: 'ts-loader', + exclude: path.resolve(__dirname, 'node_modules'), + options: { transpileOnly: true }, + }, + cssLoader, + ], + }, + + entry, + + output: { + path: path.join(__dirname, 'dist'), + filename: '[name].[hash].js', + }, + + plugins, + + optimization: { + splitChunks: { + cacheGroups: { + vendors: { + test: (mod) => { + return /[\\/]node_modules[\\/]/.test(mod.context); + }, + chunks: 'initial', + name: 'vendors', + priority: 10, + enforce: true, + }, + }, + }, + minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], + }, +}; diff --git a/Composer/packages/form-dialogs/webpack.lib.config.js b/Composer/packages/form-dialogs/webpack.lib.config.js new file mode 100644 index 0000000000..bf86fba3bf --- /dev/null +++ b/Composer/packages/form-dialogs/webpack.lib.config.js @@ -0,0 +1,59 @@ +'use strict'; + +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const TerserJSPlugin = require('terser-webpack-plugin'); +const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); +const PeerDepsExternalsPlugin = require('peer-deps-externals-webpack-plugin'); + +module.exports = { + devtool: 'source-map', + stats: 'errors-only', + + resolve: { + extensions: ['.webpack.js', '.web.js', '.ts', '.tsx', '.js', '.jsx'], + modules: [path.resolve('.'), 'node_modules'], + }, + + module: { + rules: [ + { + enforce: 'pre', + test: /\.js$/, + loader: 'source-map-loader', + exclude: path.resolve(__dirname, 'node_modules'), + }, + { + test: /\.tsx?$/, + loader: 'ts-loader', + options: { + configFile: path.join(__dirname, './tsconfig.lib.json'), + }, + exclude: path.resolve(__dirname, 'node_modules'), + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + + entry: './src/index.ts', + + output: { + path: path.join(__dirname, 'lib'), + libraryTarget: 'commonjs', + filename: 'index.js', + }, + + plugins: [ + new PeerDepsExternalsPlugin(), + new MiniCssExtractPlugin({ + filename: '[name].min.css', + }), + ], + + optimization: { + minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], + }, +}; diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 5c8444ead3..6de8804d6b 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -35,6 +35,12 @@ "about_70c18bba": { "message": "About" }, + "accepted_values_b4224bcd": { + "message": "Accepted values" + }, + "accepts_multiple_values_73658f63": { + "message": "Accepts multiple values" + }, "access_external_resources_7e37fe21": { "message": "Access external resources" }, @@ -152,6 +158,9 @@ "add_new_validation_rule_here_eb675ccf": { "message": "Add new validation rule here" }, + "add_property_d381eba3": { + "message": "Add Property" + }, "add_qna_pair_bcb1624": { "message": "Add QnA Pair" }, @@ -167,6 +176,9 @@ "advanced_events_2cbfa47d": { "message": "Advanced Events" }, + "advanced_options_4dcc8385": { + "message": "Advanced options" + }, "all_4321c3a1": { "message": "All" }, @@ -197,6 +209,9 @@ "any_or_expression_acad7d37": { "message": "Any or expression" }, + "any_string_f22dc2e1": { + "message": "any string" + }, "append_choices_35c45a2d": { "message": "Append choices" }, @@ -218,6 +233,12 @@ "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_propertyname_8a793e4f": { + "message": "Are you sure you want to remove \"{ propertyName }\"?" + }, + "are_you_sure_you_want_to_start_over_your_progress__3b7cde9b": { + "message": "Are you sure you want to start over? Your progress will be lost." + }, "are_you_sure_you_want_to_stop_current_runtime_and__a5f883b2": { "message": "Are you sure you want to stop current runtime and replace them?" }, @@ -404,6 +425,9 @@ "chris_whitten_11df1f35": { "message": "Chris Whitten" }, + "clear_all_da755751": { + "message": "Clear all" + }, "click_on_the_b_add_b_button_in_the_toolbar_and_sel_4daf351a": { "message": "Click on the Add button in the toolbar, and select Add a new trigger. In the Create a trigger wizard, set the Trigger Type to Intent recognized and configure the Trigger Name and Trigger Phrases. Then add actions in the Visual Editor." }, @@ -635,6 +659,9 @@ "default_recognizer_9c06c1a3": { "message": "Default recognizer" }, + "define_by_value_type_3cd3d1a8": { + "message": "Define by value type" + }, "define_conversation_objective_146d1cc6": { "message": "Define conversation objective" }, @@ -836,12 +863,24 @@ "enter_a_manifest_url_to_add_a_new_skill_to_your_bo_57e9d660": { "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_question_74f2e045": { "message": "Enter a question" }, "enter_an_answer_84152a83": { "message": "Enter an answer" }, + "entities_ef09392c": { + "message": "Entities" + }, + "enum_help_text_434bd44f": { + "message": "Enum help text" + }, "environment_68aed6d3": { "message": "Environment" }, @@ -914,6 +953,9 @@ "export_de71cd8e": { "message": "Export" }, + "export_json_2e2981f5": { + "message": "Export JSON" + }, "expression_7f906a13": { "message": "Expression" }, @@ -995,6 +1037,9 @@ "generate_44e33e72": { "message": "Generate" }, + "generate_dialog_b80a85b2": { + "message": "Generate dialog" + }, "get_a_new_copy_of_the_runtime_code_84970bf": { "message": "Get a new copy of the runtime code" }, @@ -1067,6 +1112,9 @@ "import_qna_from_url_26a99161": { "message": "Import QnA From Url" }, + "import_schema_75659c5f": { + "message": "Import schema" + }, "in_production_5a70b8b4": { "message": "In production" }, @@ -1091,6 +1139,9 @@ "install_the_update_and_restart_composer_fac30a61": { "message": "Install the update and restart Composer." }, + "integer_7f378275": { + "message": "integer" + }, "integer_b08abbe9": { "message": "Integer" }, @@ -1223,6 +1274,12 @@ "link_to_where_this_luis_intent_is_defined_9be25fb7": { "message": "link to where this LUIS intent is defined" }, + "list_a034633b": { + "message": "list" + }, + "list_count_values_33ea7088": { + "message": "list - { count } values" + }, "list_view_e33843f0": { "message": "List view" }, @@ -1298,6 +1355,12 @@ "manifest_version_1edc004a": { "message": "Manifest Version" }, + "maximum_f0e8e5e4": { + "message": "Maximum" + }, + "maximum_help_text_7f3e6562": { + "message": "Maximum help text" + }, "mb_8f9f9e84": { "message": "MB" }, @@ -1337,6 +1400,12 @@ "minimap_beb3be27": { "message": "Minimap" }, + "minimum_f31b05ab": { + "message": "Minimum" + }, + "minimum_help_text_6ab5b190": { + "message": "Minimum help text" + }, "missing_definition_for_defname_33f2b594": { "message": "Missing definition for { defName }" }, @@ -1406,6 +1475,9 @@ "name_of_skill_dialog_to_call_201707f3": { "message": "Name of skill dialog to call" }, + "name_of_the_property_f3cae657": { + "message": "Name of the property" + }, "navigation_control_94f2649e": { "message": "navigation control" }, @@ -1451,6 +1523,9 @@ "no_lu_or_qna_file_with_name_id_21cfe9dc": { "message": "NO LU OR QNA FILE WITH NAME { id }" }, + "no_name_e082310e": { + "message": "[no name]" + }, "no_updates_available_cecd904d": { "message": "No updates available" }, @@ -1490,6 +1565,9 @@ "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" }, @@ -1538,6 +1616,15 @@ "open_inline_editor_a5aabcfa": { "message": "Open inline editor" }, + "optional_221bcc9d": { + "message": "Optional" + }, + "optional_db6daecb": { + "message": "optional" + }, + "optional_properties_2c23c7c6": { + "message": "Optional properties" + }, "or_4f7d4edb": { "message": "Or: " }, @@ -1652,18 +1739,42 @@ "prompt_with_multi_choice_f428542f": { "message": "Prompt with multi-choice" }, + "property_array_help_text_126845dc": { + "message": "Property array help text" + }, "property_description_8d21ea2e": { "message": "Property description." }, "property_editor_preferences_ad7a4b62": { "message": "Property editor preferences" }, + "property_has_error_s_please_fix_the_error_s_for_th_e994c143": { + "message": "Property has error(s), please fix the error(s) for this property." + }, + "property_name_914371f5": { + "message": "Property name" + }, + "property_name_help_text_421a5f7f": { + "message": "Property name help text" + }, + "property_quick_actions_d3498f14": { + "message": "Property quick actions" + }, + "property_required_help_text_57f027ad": { + "message": "Property required help text" + }, "property_title_f2b443b7": { "message": "Property title" }, "property_type_95689fa5": { "message": "Property type." }, + "property_type_e38cf7e4": { + "message": "Property Type" + }, + "property_type_help_text_8a2c202d": { + "message": "Property type help text" + }, "provide_a_brief_description_it_will_appear_on_the__f962eb38": { "message": "Provide a brief description. It will appear on the publish history list." }, @@ -1769,6 +1880,21 @@ "reprompt_dialog_event_c42d2c33": { "message": "Reprompt dialog event" }, + "required_5f7ef8c0": { + "message": "Required" + }, + "required_a6089a96": { + "message": "required" + }, + "required_properties_dfb0350d": { + "message": "Required properties" + }, + "requiredtext_ff8f722f": { + "message": "{ requiredText }" + }, + "requiredtext_priority_priority_4293288f": { + "message": "{ requiredText } | Priority: { priority }" + }, "response_is_response_3cd62f8f": { "message": "Response is { response }" }, @@ -2054,6 +2180,9 @@ "the_welcome_message_is_triggered_by_the_i_conversa_a3ff58f8": { "message": "The welcome message is triggered by the ConversationUpdate event. To add a new ConversationUpdate trigger:" }, + "there_are_no_kind_properties_e299287e": { + "message": "There are no { kind } properties." + }, "there_was_an_error_74ed3c58": { "message": "There was an error" }, @@ -2174,6 +2303,9 @@ "try_again_ad656c3c": { "message": "Try again" }, + "type_and_press_enter_33a2905d": { + "message": "Type and press enter" + }, "type_c8106334": { "message": "Type" }, diff --git a/Composer/yarn.lock b/Composer/yarn.lock index aa62239508..61148f14ba 100644 --- a/Composer/yarn.lock +++ b/Composer/yarn.lock @@ -2638,6 +2638,22 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@eslint/eslintrc@^0.1.3": + version "0.1.3" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@eslint/eslintrc/-/@eslint/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" + integrity sha1-fRorI1hVLMBINMCXm9QnU2LjcIU= + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@fluentui/date-time-utilities@^7.1.2": version "7.1.2" resolved "https://registry.yarnpkg.com/@fluentui/date-time-utilities/-/date-time-utilities-7.1.2.tgz#77bcc2865b1780473be38ad62a2a7f5662659a5b" @@ -3829,7 +3845,14 @@ "@types/history" "*" "@types/react" "*" -"@types/react-dom@*": +"@types/react-beautiful-dnd@^13.0.0": + version "13.0.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/react-beautiful-dnd/-/@types/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" + integrity sha1-5g09llMS/PFRaJSvktw+kklYfbQ= + dependencies: + "@types/react" "*" + +"@types/react-dom@*", "@types/react-dom@^16.8.4": version "16.9.8" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== @@ -3850,7 +3873,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@16.9.23": +"@types/react@*", "@types/react@16.9.23", "@types/react@^16.8.18": version "16.9.23" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.23.tgz#1a66c6d468ba11a8943ad958a8cb3e737568271c" integrity sha512-SsGVT4E7L2wLN3tPYLiF20hmZTPGuzaayVunfgXzUn1x4uHVsKH6QDJQ/TdpHqwsTLd4CwrmQ2vOgxN7gE24gw== @@ -3858,6 +3881,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/recoil@^0.0.1": + version "0.0.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/@types/recoil/-/@types/recoil-0.0.1.tgz#c69efee4871e0e3fc4fe19ba15a6bfc35f9b6bf1" + integrity sha1-xp7+5IceDj/E/hm6Faa/w1+ba/E= + dependencies: + "@types/react" "*" + "@types/rimraf@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e" @@ -4482,6 +4512,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" integrity sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ== +acorn@^7.4.0: + version "7.4.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" + integrity sha1-4a1IbmxUUBY0xsOXxcEh2qODYHw= + adaptive-expressions@4.10.0-preview-147186: version "4.10.0-preview-147186" resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/adaptive-expressions/-/adaptive-expressions-4.10.0-preview-147186.tgz#4468fcb00a27d955d4f1fd46566fda5a46baef87" @@ -4616,6 +4651,16 @@ ajv@^6.12.0, ajv@^6.12.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.4: + version "6.12.5" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha1-GbDouuj0duW6ZmMAOHd1+xoApNo= + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + 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" @@ -4641,6 +4686,11 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha1-y7muJWv3UK8eqzRPIpqif+lLo0g= + ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" @@ -6260,6 +6310,21 @@ chokidar@^3.2.2: optionalDependencies: fsevents "~2.1.2" +chokidar@^3.4.1: + version "3.4.2" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha1-ONyOZY3sOAl0HrPve7Ckf+QkIy0= + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -7035,6 +7100,13 @@ create-react-context@^0.2.1: fbjs "^0.8.0" gud "^1.0.0" +cross-env@^5.2.0: + version "5.2.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/cross-env/-/cross-env-5.2.1.tgz#b2c76c1ca7add66dc874d11798466094f551b34d" + integrity sha1-ssdsHKet1m3IdNEXmEZglPVRs00= + dependencies: + cross-spawn "^6.0.5" + cross-env@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-6.0.3.tgz#4256b71e49b3a40637a0ce70768a6ef5c72ae941" @@ -7108,6 +7180,13 @@ css-blank-pseudo@^0.1.4: dependencies: postcss "^7.0.5" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha1-WZUdO4H9ayB0pi1JREQVsNK018E= + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -8619,6 +8698,22 @@ enhanced-resolve@4.1.0, enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" +enhanced-resolve@^4.1.1, enhanced-resolve@^4.3.0: + version "4.3.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" + integrity sha1-O4BvO/r8HsfeaVUe+TzKRsFwQSY= + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +enquirer@^2.3.5: + version "2.3.6" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha1-Kn/l3WNKHkElqXXsmU/1RW3Dc00= + dependencies: + ansi-colors "^4.1.1" + entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -8979,6 +9074,14 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha1-54blmmbLkrP2wfsNUIqrF0hI9Iw= + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + eslint-utils@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" @@ -8986,6 +9089,13 @@ eslint-utils@^2.0.0: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha1-0t5eA0JOcH3BDHQGjd7a5wh0Gyc= + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" @@ -8996,6 +9106,11 @@ eslint-visitor-keys@^1.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== +eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha1-MOvR73wv3/AcOk8VEESvJfqwUj4= + eslint@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.0.0.tgz#c35dfd04a4372110bd78c69a8d79864273919a08" @@ -9079,6 +9194,49 @@ eslint@^3.19.0: text-table "~0.2.0" user-home "^2.0.0" +eslint@^7.5.0: + version "7.10.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/eslint/-/eslint-7.10.0.tgz#494edb3e4750fb791133ca379e786a8f648c72b9" + integrity sha1-SU7bPkdQ+3kRM8o3nnhqj2SMcrk= + dependencies: + "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.1.3" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^1.3.0" + espree "^7.3.0" + esquery "^1.2.0" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.19" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + espree@^3.4.0: version "3.5.4" resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" @@ -9096,6 +9254,15 @@ espree@^7.0.0: acorn-jsx "^5.2.0" eslint-visitor-keys "^1.1.0" +espree@^7.3.0: + version "7.3.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" + integrity sha1-3DBDfPZ5R89XYSHr14DxXurHI0g= + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.3.0" + esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -9115,6 +9282,13 @@ esrecurse@^4.1.0: dependencies: estraverse "^4.1.0" +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha1-eteWTWeauyi+5yzsY3WLHF0smSE= + dependencies: + estraverse "^5.2.0" + estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" @@ -9125,6 +9299,11 @@ estraverse@^5.1.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== +estraverse@^5.2.0: + version "5.2.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha1-MH30JUfmzHMk088DwVXVzbjFOIA= + esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -9302,7 +9481,7 @@ express-session@^1.17.0: safe-buffer "5.2.0" uid-safe "~2.1.5" -express@^4.15.2, express@^4.16.3, express@^4.17.1: +express@^4.14.0, express@^4.15.2, express@^4.16.3, express@^4.17.1: version "4.17.1" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" integrity sha1-RJH8OGBc9R+GKdOcK10Cb5ikwTQ= @@ -9723,7 +9902,7 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -findup-sync@3.0.0: +findup-sync@3.0.0, findup-sync@^3.0.0: version "3.0.0" resolved "https://botbuilder.myget.org/F/botbuilder-declarative/npm/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" integrity sha1-F7EI+e5RLft6XH88iyfqnhqcCNE= @@ -10276,7 +10455,7 @@ global-dirs@^2.0.1: dependencies: ini "^1.3.5" -global-modules@2.0.0: +global-modules@2.0.0, global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== @@ -10696,6 +10875,13 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha1-7OCsr3HWLClpwuxZ/v9CpLGoW0U= + dependencies: + react-is "^16.7.0" + homedir-polyfill@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" @@ -10752,7 +10938,7 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" -html-entities@^1.3.1: +html-entities@^1.2.0, html-entities@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== @@ -11051,6 +11237,14 @@ import-fresh@^3.0.0: parent-module "^1.0.0" resolve-from "^4.0.0" +import-fresh@^3.2.1: + version "3.2.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha1-Yz/2GFBueTr1rJG/SLcmd+FcvmY= + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-from@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" @@ -11213,7 +11407,7 @@ interpret@1.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -interpret@^1.0.0: +interpret@^1.0.0, interpret@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== @@ -12748,7 +12942,7 @@ loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.0: +loader-utils@^1.0.0, loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== @@ -13185,7 +13379,7 @@ mem@^4.0.0: mimic-fn "^2.0.0" p-is-promise "^2.0.0" -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== @@ -13198,6 +13392,14 @@ memory-fs@^0.4.0, memory-fs@^0.4.1: errno "^0.1.3" readable-stream "^2.0.1" +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha1-MkwBKIuIZSlm0WHbd4OHIIRajjw= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + merge-anything@^2.2.4: version "2.4.4" resolved "https://registry.yarnpkg.com/merge-anything/-/merge-anything-2.4.4.tgz#6226b2ac3d3d3fc5fb9e8d23aa400df25f98fdf0" @@ -13734,6 +13936,17 @@ node-modules-regexp@^1.0.0: resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= +node-notifier@^5.1.2: + version "5.4.3" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha1-y3La+UyTkECY4oucWQ/YZuRkvVA= + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + node-notifier@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-7.0.0.tgz#513bc42f2aa3a49fce1980a7ff375957c71f718a" @@ -14184,6 +14397,14 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" +optimize-css-assets-webpack-plugin@^5.0.1: + version "5.0.4" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.4.tgz#85883c6528aaa02e30bbad9908c92926bb52dc90" + integrity sha1-hYg8ZSiqoC4wu62ZCMkpJrtS3JA= + dependencies: + cssnano "^4.1.10" + last-call-webpack-plugin "^3.0.0" + optimize-css-assets-webpack-plugin@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz#e2f1d4d94ad8c0af8967ebd7cf138dcb1ef14572" @@ -14648,6 +14869,11 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +peer-deps-externals-webpack-plugin@^1.0.4: + version "1.0.4" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/peer-deps-externals-webpack-plugin/-/peer-deps-externals-webpack-plugin-1.0.4.tgz#191e8f116c70401364dbd8e6c44ab3f8844101dc" + integrity sha1-GR6PEWxwQBNk29jmxEqz+IRBAdw= + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -15770,7 +15996,7 @@ querystring-es3@^0.2.0: resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= -querystring@0.2.0: +querystring@0.2.0, querystring@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= @@ -15780,6 +16006,11 @@ querystringify@^2.0.0, querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== +raf-schd@^4.0.2: + version "4.0.2" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0" + integrity sha1-vUTHCBiPLoTIEL9V/OqSMbyu2KA= + raf@3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -15870,6 +16101,49 @@ react-app-polyfill@^0.2.1: raf "3.4.1" whatwg-fetch "3.0.0" +react-beautiful-dnd@^13.0.0: + version "13.0.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" + integrity sha1-9wzI/4K4S8cY+K8VfJ+VdXpsO0A= + dependencies: + "@babel/runtime" "^7.8.4" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" + +react-dev-utils@7.0.3: + version "7.0.3" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/react-dev-utils/-/react-dev-utils-7.0.3.tgz#f1316cfffd792fd41b0c28ad5db86c1d74484d6f" + integrity sha1-8TFs//15L9QbDCitXbhsHXRITW8= + dependencies: + "@babel/code-frame" "7.0.0" + address "1.0.3" + browserslist "4.4.1" + chalk "2.4.2" + cross-spawn "6.0.5" + detect-port-alt "1.1.6" + escape-string-regexp "1.0.5" + filesize "3.6.1" + find-up "3.0.0" + global-modules "2.0.0" + globby "8.0.2" + gzip-size "5.0.0" + immer "1.10.0" + inquirer "6.2.1" + is-root "2.0.0" + loader-utils "1.2.3" + opn "5.4.0" + pkg-up "2.0.0" + react-error-overlay "^5.1.3" + recursive-readdir "2.2.2" + shell-quote "1.6.1" + sockjs-client "1.3.0" + strip-ansi "5.0.0" + text-table "0.2.0" + react-dev-utils@^7.0.3: version "7.0.5" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-7.0.5.tgz#cb95375d01ae71ca27b3c7616006ef7a77d14e8e" @@ -15915,6 +16189,11 @@ react-error-boundary@^1.2.5: resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-1.2.5.tgz#a362cb799d2e58ff8f114f7c4bc25677ce4e4149" integrity sha512-5CPSeLJA2igJNppAgFRwnTL9aK3ojenk65enNzhVyoxYNbHpIJXnChUO7+4vPhkncRA9wvQMXq6Azp2XeXd+iQ== +react-error-overlay@^5.1.3: + version "5.1.6" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/react-error-overlay/-/react-error-overlay-5.1.6.tgz#0cd73407c5d141f9638ae1e0c63e7b2bf7e9929d" + integrity sha1-DNc0B8XRQfljiuHgxj57K/fpkp0= + react-error-overlay@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.4.tgz#88dfb88857c18ceb3b9f95076f850d7121776991" @@ -15925,7 +16204,7 @@ react-frame-component@^4.0.2: resolved "https://registry.yarnpkg.com/react-frame-component/-/react-frame-component-4.1.0.tgz#bef04039c6af687314f27b20ef9893d85eefe3e6" integrity sha512-2HkO0iccSjd+xRA+aOxq7Mm50WUmCjdmhbQhOiG6gQTChaW//Y3mdkGeUfVA3YkXvDVbigRDvJd/VTUlqaZWSw== -react-is@^16.12.0, react-is@^16.6.0, react-is@^16.8.6: +react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -15950,6 +16229,17 @@ react-measure@^2.3.0: prop-types "^15.6.2" resize-observer-polyfill "^1.5.0" +react-redux@^7.1.1: + version "7.2.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985" + integrity sha1-je33hJAQFNsv7KGrYzhk3uaK2YU= + dependencies: + "@babel/runtime" "^7.5.5" + hoist-non-react-statics "^3.3.0" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^16.9.0" + react-test-renderer@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1" @@ -16180,6 +16470,14 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" +redux@^4.0.4: + version "4.0.5" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" + integrity sha1-TbXeWBbheJHeioDEJCMtBvBR2T8= + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + regenerate-unicode-properties@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" @@ -17733,6 +18031,11 @@ strip-json-comments@^3.1.0: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY= + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -17912,7 +18215,7 @@ swagger-parser@^8.0.4: swagger-methods "^2.0.1" z-schema "^4.2.2" -symbol-observable@^1.1.0: +symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== @@ -18106,6 +18409,11 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiny-invariant@^1.0.6: + version "1.1.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" + integrity sha1-Y0xfjv3CdxS384bDXmdgmR0jCHU= + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -18274,6 +18582,17 @@ ts-loader@7.0.4: micromatch "^4.0.0" semver "^6.0.0" +ts-loader@^6.0.1: + version "6.2.2" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/ts-loader/-/ts-loader-6.2.2.tgz#dffa3879b01a1a1e0a4b85e2b8421dc0dfff1c58" + integrity sha1-3/o4ebAaGh4KS4XiuEIdwN//HFg= + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + ts-md5@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/ts-md5/-/ts-md5-1.2.7.tgz#b76471fc2fd38f0502441f6c3b9494ed04537401" @@ -18452,6 +18771,11 @@ typescript@3.9.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== +typescript@^3.5.3: + version "3.9.7" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa" + integrity sha1-mNYApevcOPQMsndSLxLcgA6eJfo= + typescript@^3.8.3: version "3.8.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" @@ -18713,6 +19037,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.1: + version "1.1.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c" + integrity sha1-Oebwj+J+Qip9eyNLX5BWrzE70iw= + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -18804,6 +19133,11 @@ v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== +v8-compile-cache@^2.1.1: + version "2.1.1" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha1-VLw83UMxe8qR413K8wWxpyN950U= + v8-to-istanbul@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-4.1.3.tgz#22fe35709a64955f49a08a7c7c959f6520ad6f20" @@ -19038,6 +19372,13 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha1-mUihhmy71suCTeoTp+1pH2yN3/A= + dependencies: + chokidar "^2.1.8" + watchpack@^1.6.1: version "1.6.1" resolved "https://botbuilder.myget.org/F/botframework-cli/npm/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" @@ -19047,6 +19388,17 @@ watchpack@^1.6.1: graceful-fs "^4.1.2" neo-async "^2.5.0" +watchpack@^1.7.4: + version "1.7.4" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" + integrity sha1-bp2lOzyAuy1lCBiPWyAEEIZs0ws= + dependencies: + graceful-fs "^4.1.2" + neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.0" + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" @@ -19105,7 +19457,24 @@ webpack-cli@^3.3.11: v8-compile-cache "2.0.3" yargs "13.2.4" -webpack-dev-middleware@^3.7.2: +webpack-cli@^3.3.2: + version "3.3.12" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" + integrity sha1-lOmtoIFFPNCqYJyZ5QABL9OtLUo= + dependencies: + chalk "^2.4.2" + cross-spawn "^6.0.5" + enhanced-resolve "^4.1.1" + findup-sync "^3.0.0" + global-modules "^2.0.0" + import-local "^2.0.0" + interpret "^1.4.0" + loader-utils "^1.4.0" + supports-color "^6.1.0" + v8-compile-cache "^2.1.1" + yargs "^13.3.2" + +webpack-dev-middleware@^3.7.0, webpack-dev-middleware@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== @@ -19155,6 +19524,16 @@ webpack-dev-server@^3.11.0: ws "^6.2.1" yargs "^13.3.2" +webpack-hot-middleware@^2.25.0: + version "2.25.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/webpack-hot-middleware/-/webpack-hot-middleware-2.25.0.tgz#4528a0a63ec37f8f8ef565cf9e534d57d09fe706" + integrity sha1-RSigpj7Df4+O9WXPnlNNV9Cf5wY= + dependencies: + ansi-html "0.0.7" + html-entities "^1.2.0" + querystring "^0.2.0" + strip-ansi "^3.0.0" + webpack-log@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" @@ -19179,6 +19558,15 @@ webpack-merge@^4.2.2: dependencies: lodash "^4.17.15" +webpack-notifier@^1.7.0: + version "1.8.0" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/webpack-notifier/-/webpack-notifier-1.8.0.tgz#994bdde0fcefc5f1a92b6d91353c8152ddd9c583" + integrity sha1-mUvd4PzvxfGpK22RNTyBUt3ZxYM= + dependencies: + node-notifier "^5.1.2" + object-assign "^4.1.0" + strip-ansi "^3.0.1" + webpack-sources@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" @@ -19224,6 +19612,35 @@ webpack@4.43.0, webpack@^4.43.0: watchpack "^1.6.1" webpack-sources "^1.4.1" +webpack@^4.32.0: + version "4.44.2" + resolved "https://botbuilder.myget.org/F/botbuilder-v4-js-daily/npm/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" + integrity sha1-a/4rCvBVyLLR6Q7SzZNj+EEma3I= + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.3.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + websocket-driver@0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" @@ -19286,7 +19703,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.1: +which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==