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 8 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 @@ -24,6 +24,7 @@
"packages/electron-server",
"packages/extension",
"packages/extension-client",
"packages/form-dialogs",
"packages/intellisense",
"packages/lib",
"packages/lib/*",
Expand All @@ -42,7 +43,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": "1.0.0",
"license": "MIT",
"author": "Microsoft",
"description": "Botframework Composer dialog form components.",
"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"
}
}
92 changes: 92 additions & 0 deletions Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// 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 { VisualEditor } from 'src/components/VisualEditor';

export type FormDialogSchemaEditorProps = {
/**
* Unique id for the visual editor.
*/
editorId: string;
/**
* Wether to show or hide the theme picker.
*/
showThemePicker?: boolean;
/**
* 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,
showThemePicker = false,
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 (
<VisualEditor
key={editorId}
schemaExtension={schemaExtension}
showThemePicker={showThemePicker}
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