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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"packages/electron-server",
"packages/extension",
"packages/extension-client",
"packages/form-dialogs",
"packages/intellisense",
"packages/lib",
"packages/lib/*",
Expand All @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions Composer/packages/form-dialogs/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
extends: ['../../.eslintrc.react.js'],
parserOptions: {
project: './tsconfig.lib.json',
tsconfigRootDir: __dirname,
},
};
9 changes: 9 additions & 0 deletions Composer/packages/form-dialogs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
dist
lib

packages
bin
obj

**/*.log
63 changes: 63 additions & 0 deletions Composer/packages/form-dialogs/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
79 changes: 79 additions & 0 deletions Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<FormDialogPropertiesEditor
key={editorId}
schemaExtension={schemaExtension}
onGenerateDialog={onGenerateDialog}
onReset={startOver}
/>
);
});

export const FormDialogSchemaEditor = (props: FormDialogSchemaEditorProps) => {
return (
<RecoilRoot>
<InternalFormDialogSchemaEditor {...props}></InternalFormDialogSchemaEditor>
</RecoilRoot>
);
};
153 changes: 153 additions & 0 deletions Composer/packages/form-dialogs/src/atoms/appState.ts
Original file line number Diff line number Diff line change
@@ -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<FormDialogSchema>({
key: 'FormDialogSchemaAtom',
default: {
id: '',
name: '',
requiredPropertyIds: [],
optionalPropertyIds: [],
},
});

/**
* This atom family represent a form dialog schema property.
*/
export const formDialogPropertyAtom = atomFamily<FormDialogProperty, string>({
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<string[]>({
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<string[]>({
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<object, string>({
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<boolean, string>({
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<Record<string, object>>((acc, propId, idx) => {
const property = schemaPropertyStores[idx];
acc[property.name] = get(formDialogPropertyJsonSelector(propId));
return acc;
}, <Record<string, object>>{}),
};
}

const required = schemaPropertyStores.filter((property) => property.required).map((property) => property.name);
const examples = schemaPropertyStores.reduce<Record<string, string[]>>((acc, property) => {
if (property.examples?.length) {
acc[property.name] = property.examples;
}
return acc;
}, <Record<string, string[]>>{});

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<string[]>({
key: 'FormDialogTemplatesAtom',
default: [],
});

export const activePropertyIdAtom = atom<string>({
key: 'ActivePropertyIdAtom',
default: '',
});
Loading