This repository was archived by the owner on Jul 9, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 374
feat: form dialog extension #4296
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
5a9244b
Form dialogs init
sorgh 7501f9e
new design
sorgh a32f5f9
UX comments
sorgh 2104ad0
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
sorgh 5e18fc7
conflicts resolved
sorgh a43c8c0
yarn
sorgh 5fda1cb
click outside to dismiss expanded
sorgh 37de06b
Fix yarn.lock
sorgh ee4889b
PR comments
sorgh 882d8c3
fix demo
sorgh 7934c94
Merge branch 'main' into sorgh/4145b
hatpick 24d7b01
Merge branch 'main' into sorgh/4145b
hatpick File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| node_modules | ||
| dist | ||
| lib | ||
|
|
||
| packages | ||
| bin | ||
| obj | ||
|
|
||
| **/*.log |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
79
Composer/packages/form-dialogs/src/FormDialogSchemaEditor.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
hatpick marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </RecoilRoot> | ||
| ); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
hatpick marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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: '', | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.