Skip to content

Commit

Permalink
Merge branch 'main' into zxyanliu/title-text/20200904
Browse files Browse the repository at this point in the history
  • Loading branch information
a-b-r-o-w-n committed Sep 14, 2020
2 parents 9c6b517 + f2a365a commit 8bc7709
Show file tree
Hide file tree
Showing 521 changed files with 2,401 additions and 1,417 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

/Composer/ @cwhitten @boydc2014 @a-b-r-o-w-n @beyackle @srinaath @tonyanziano @geoffcoxmsft @hatpick

/Composer/packages/adaptive-flow @yeze322 @cwhitten @boydc2014 @a-b-r-o-w-n

/docs/ @cwhitten @boydc2014 @benbrown @geoffcoxmsft
6 changes: 3 additions & 3 deletions .github/actions/conventional-pr/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Composer/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
**/server/tmp.zip
# not ignore all lib folder because packages/lib, so probably we should rename that to libs
packages/lib/*/lib
packages/extensions/*/lib

Dockerfile
.dockerignore
7 changes: 7 additions & 0 deletions Composer/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ cypress/results
cypress/videos

TestBots/

l10ntemp/

packages/server/schemas/*.schema
packages/server/schemas/*.uischema
!packages/server/schemas/sdk.schema
!packages/server/schemas/sdk.uischema
6 changes: 5 additions & 1 deletion Composer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ WORKDIR /app/Composer
COPY --from=build /src/Composer/yarn.lock .
COPY --from=build /src/Composer/package.json .
COPY --from=build /src/Composer/packages/client ./packages/client
COPY --from=build /src/Composer/packages/extensions ./packages/extensions
COPY --from=build /src/Composer/packages/adaptive-flow ./packages/adaptive-flow
COPY --from=build /src/Composer/packages/adaptive-form ./packages/adaptive-form
COPY --from=build /src/Composer/packages/extension ./packages/extension
COPY --from=build /src/Composer/packages/extension-client ./packages/extension-client
COPY --from=build /src/Composer/packages/intellisense ./packages/intellisense
COPY --from=build /src/Composer/packages/server ./packages/server
COPY --from=build /src/Composer/packages/lib ./packages/lib
COPY --from=build /src/Composer/packages/tools ./packages/tools
Expand Down
163 changes: 0 additions & 163 deletions Composer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,166 +30,3 @@ If you run into the issue of `There appears to be trouble with your network conn

## Documentation
The documentation for Composer [can be found here](/toc.md).

## Extension Framework
Composer is built on top of an extension framework, which allows anyone to provide an extension as the editor of certain type of bot assets.

All editors, 1st party or 3rd party, are loaded in the same way by the extension framework.

Non-editor extensions are not supported at this time, though the mechanisms for providing extensions will scale outside the dialog editor's.

### What's an extension? what's in it
Each extension is a standalone React component package ([why React component](#why-react-component)) under Composer's `/packages/extensions` folder, which implements the extension `interface`.

Composer is managed via yarn workspaces, producing such a folder layout.
```
|- Composer
|- package.json // define the workspaces
|- packages
|- client // composer main app
|- server // composer api server
|- extensions
|- package.json // put all extension as one package
|- adaptive-form // dialog property editor
|- lib
|- shared // shared code
```

All extensions under `/extensions` folder will be eventually packed into one `extensions` package, then the `client` package will depends on this `extensions` package.

### Extension Interface
The extension interface defines the way how an extension comminutes to the host.

In React world, interface means the props passed into a component. An extension will be passed ino 3 props:
- data:any. which is the data to be edited by this editor.
- onChange: (newData) => void. which is the callback enables an editor to save the edited data.
- [shellApi](#shell-api). which is a set of apis providing other capabilities than data in\out.

The rendering code of an extension will be sth like this:
```
import SomeEditor from 'someplace'
<SomeEditor data={..}, onChange={...}, shellApi={...}>
```

#### data in\out story

With this interface, it's pretty clear how data is in\out extension. Data is passed in through `data` prop, and been saved with `onChange` prop.

#### shell api

Shell api is a set of apis that provides other important functionalities that empower an extension to provide a more powerful and smooth editing experience. including

* OpenSubEditor
```
openSubEditor(location:string, data:any, onChange:(newData:any) => void)
```
This is the most important api that support a [multiple editors](#multiple-editors) scenario, which allows an editor to delegate some editing task to another editor, and listen the changes.

Note that, this api doesn't allow you to specify the type or the name of the sub editor. You only get to specify a data, the shell will use a centralized way to manage how editors are registered and picked up. See registration section in the below for more details.

We may suppport other forms of openSubEditor later, but we expect this is the mosted used one.

* ObjectGraph

TBD, this will be an api that enable each extension have the knowledge of a global object graph.
* Alert
* ReadFile
* Prompt

### Why React component

There are many options to choose when picking an abstraction for an extension. Different level abstractions have different impacts on many aspects, like developing, testing, debuging and running the extension.

A low-level abstraction like HTML page will give us perfect isolation, great flexibilty (use whatever language you want to build that), but usually result in a relatively high amount of effort to develop a robust api between host and extension because it's using the low-level messaging primitives, and also not good for performance because of the extreme isolation.

A high-level abstraction like React component, will cost a little bit on isolation, but gain the best support from a mature and powerful framework, in every cycle of the development of extensions. It will help most of boosting the productivity, simplifying the architecure, and gain the best performance.

Based on our scenario, we will use React as a start point, and host the extension in an `<IFrame>` to gain a certain level of isolation, for two major reasons:

* we favor produtivity, simplicity, performance at this point over extreme isolation, more choices when developing extensions. We don't expect there are so many extension developers like VS Code . :)
* we can always go low-level whenever we see fit, all extensions will still work then, not but visa-verse


### How an extension is discovered, registered, loaded, & hosted

#### registration

Inside the client package, there is an `EditorMap` module to manage the mapping from `dialog asset type` => `registered editor`.

The extension registration is done by modifying `/Composer/packages/client/src/extension-container/EditorMap.js` to add an entry in the registration table

```
ExtensionMap.js
const EditorRegistration = [
{
when: (data) => getSuffix(data.name) === ".dialog",
pick: FormEditor
},
{
when: (data) => getSuffix(data.name) === ".lu",
pick: JsonEditor
},
{
when: (data) => true,
pick: JsonEditor
}
]
```

As you can see in above, editors are registered in such an registration table with each entry includes two properties:
* when. A lamdba that tests whether this kind of data is interested or not
* pick. The target extenion type to be picked up.

In this design, we treat file the same way as any json object, by wrapping it into an data object, such as
```
{
name: "xx.lu",
content: "file content"
}
```

With this unification into a data object and a lambda on top of that, it's simple, powerful and flexible to define extension at any level of content.

#### discovery

In the runtime, each time an data is to be edited, the shell will go through the table, run the condition of each entry, and pick a proper editor for that.

#### loading

The loading of the extension is totally controlled by composer, common loading patterns (lazy, prefetch) can be utilized but is not a concern of the extension.

Here in this protoype, we showed how a typical lazy-loading process looks like:

When the composer starts up, composer will try to read all bot assets, starting from the .bot file, then resolve all it's dependencies. And list all files into the sidebar on the left.

When user click a ".lu" file, the composer knows which extension can handle this ".lu" file, based on the mapping produced by `ExtensionMap`. The Composer sends a signal to the mounted Extension and an appropriate payload to give it the data and interface it needs for Composer to edit the current Dialog.

Loading an editor in the json-schema sense is the following:

1. Edit sends a signal to the Extension that it is time to render, and a payload representing the current `formData`
2. Extension loads its schemas `schema`, `uiSchema` (descriptions on what and how the form is the be rendered) with the given `formData`
3. On change of a value in a form control, we construct a not-yet-saved Dialog that when saved persists it to the Bot asset. Saving conventions are not yet defined, but could be a debounced auto-save, or save on demand, etc.

#### hosting

Extensions are gauranteed to be hosted in an isolated (from the main composer window) container, like `<iframe />`.

Communication between the Composer and the extension is using the React conventions: props, since we assume an extension is an React component.


### Dialog editing

If the field being edited is part of a parent Dialog, we may need to provide an alert allowing the user to let us know if the intention is to edit the base Dialog or create a new dialog with this edited override. (business requirement needed). If the user chooses to modify the base dialog, this will update all Dialogs that currently inhereit from it.

### extending an extension

As designed, the Extension is sealed to its current React Component implementation. If during a design session one wanted to add a property on a Dialog configured to a particular Extension that was not supported by the Editors current schema, this would require a new schema, which would map to a new Editor type.

## multiple editors

This section will explain how multiple editors are supported in details, including how editors are organized in a hierachy way, how data is bubbled up from children to parent, then to shell, etc.
2 changes: 1 addition & 1 deletion Composer/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Tests inside of the Composer project are run via Jest ([documentation](https://jestjs.io/docs/en/24.0/getting-started.html)), with coverage reporting from Jest's built-in reporting tool istanbul.

Tests are scoped to each of the packages within the Composer project, and can be found in the `__tests__` directory inside of each package directory. For example, **client** tests can be found under the `<root>/Composer/packages/client/__tests__/` directory, **Adaptive Flow** tests can be found under the `<root>/Composer/packages/extensions/adaptive-flow/__tests__` directory, etc.
Tests are scoped to each of the packages within the Composer project, and can be found in the `__tests__` directory inside of each package directory. For example, **client** tests can be found under the `<root>/Composer/packages/client/__tests__/` directory, **Adaptive Flow** tests can be found under the `<root>/Composer/packages/adaptive-flow/__tests__` directory, etc.

## Running Tests

Expand Down
2 changes: 1 addition & 1 deletion Composer/cypress/integration/LuisDeploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ context('Luis Deploy', () => {
status: 200,
response: 'fixture:luPublish/success',
});
cy.findByText('Start Bot').click();
cy.findByText(/^(Start|Restart) Bot$/).click();

// clear its settings before
cy.enterTextAndSubmit('ProjectNameInput', 'MyProject');
Expand Down
8 changes: 4 additions & 4 deletions Composer/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ module.exports = {
testPathIgnorePatterns: ['/node_modules/', '/scripts/', '/jestMocks/', '__tests__/setup.(j|t)s', '/cypress/'],
projects: [
'<rootDir>/packages/client',
'<rootDir>/packages/extensions/adaptive-form',
'<rootDir>/packages/extensions/adaptive-flow',
'<rootDir>/packages/extensions/extension',
'<rootDir>/packages/extensions/intellisense',
'<rootDir>/packages/adaptive-form',
'<rootDir>/packages/adaptive-flow',
'<rootDir>/packages/extension-client',
'<rootDir>/packages/intellisense',
'<rootDir>/packages/lib/code-editor',
'<rootDir>/packages/lib/shared',
'<rootDir>/packages/server',
Expand Down
22 changes: 16 additions & 6 deletions Composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@
"set-value": "^3.0.2",
"kind-of": "^6.0.3",
"elliptic": "^6.5.3",
"bl": "^2.2.1"
"@babel/parser": "^7.11.3",
"bl": "^2.2.1",
"node-forge": "^0.10.0"
},
"engines": {
"node": ">=12"
},
"workspaces": [
"packages/adaptive-flow",
"packages/adaptive-form",
"packages/client",
"packages/electron-server",
"packages/extensions",
"packages/extensions/*",
"packages/extension",
"packages/extension-client",
"packages/intellisense",
"packages/lib",
"packages/lib/*",
"packages/server",
Expand All @@ -33,11 +38,11 @@
"scripts": {
"build": "node scripts/begin.js && yarn build:prod",
"build:prod": "yarn build:dev && yarn build:server && yarn build:client && yarn build:electron",
"build:dev": "yarn build:test && yarn build:lib && yarn build:tools && yarn build:extensions && yarn build:plugins",
"build:dev": "yarn build:test && yarn build:lib && yarn build:tools && yarn build:extensions && yarn build:plugins && yarn l10n",
"build:test": "yarn workspace @bfc/test-utils build",
"build:lib": "yarn workspace @bfc/libs build:all",
"build:electron": "yarn workspace @bfc/electron-server build",
"build:extensions": "wsrun -lt -p @bfc/plugin-loader @bfc/intellisense @bfc/extension @bfc/adaptive-form @bfc/adaptive-flow @bfc/ui-plugin-* @bfc/client-plugin-lib -c build",
"build:extensions": "wsrun -lt -p @bfc/extension @bfc/intellisense @bfc/extension-client @bfc/adaptive-form @bfc/adaptive-flow @bfc/ui-plugin-* -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 Expand Up @@ -65,7 +70,12 @@
"typecheck": "concurrently --kill-others-on-fail \"npm:typecheck:*\"",
"typecheck:server": "yarn workspace @bfc/server typecheck",
"typecheck:client": "yarn workspace @bfc/client typecheck",
"tableflip": "rimraf node_modules/ **/node_modules && yarn && yarn build"
"tableflip": "rimraf node_modules/ **/node_modules && yarn && yarn build",
"l10n:extract": "cross-env NODE_ENV=production format-message extract -g underscored_crc32 -o packages/server/src/locales/en-US.json l10ntemp/**/*.js",
"l10n:extractJson": "node scripts/l10n-extractJson.js",
"l10n:transform": "node scripts/l10n-transform.js",
"l10n:babel": "babel ./packages --extensions \".ts,.tsx,.jsx,.js\" --out-dir l10ntemp --presets=@babel/react,@babel/typescript --plugins=@babel/plugin-proposal-class-properties --ignore \"packages/**/__tests__\",\"packages/**/node_modules\",\"packages/**/build/**/*.js\"",
"l10n": "yarn l10n:babel && yarn l10n:extract && yarn l10n:transform packages/server/src/locales/en-US.json && yarn l10n:extractJson packages/server/schemas"
},
"husky": {
"hooks": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
extends: ['../../../.eslintrc.react.js'],
extends: ['../../.eslintrc.react.js'],
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import React from 'react';
import { render } from '@bfc/test-utils';
import { ExtensionContext } from '@bfc/extension';
import { EditorExtensionContext } from '@bfc/extension-client';

import AdaptiveFlowEditor from '../../src/adaptive-flow-editor/AdaptiveFlowEditor';

Expand All @@ -12,15 +12,15 @@ import { ShellApiStub } from './stubs/ShellApiStub';
describe('<VisualDesigner/>', () => {
it('can render.', () => {
const visualDesigner = render(
<ExtensionContext.Provider
<EditorExtensionContext.Provider
value={{
shellApi: ShellApiStub,
shellData: {} as any,
plugins: [],
}}
>
<AdaptiveFlowEditor />
</ExtensionContext.Provider>
</EditorExtensionContext.Provider>
);
expect(visualDesigner).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import React from 'react';
import { render, fireEvent } from '@bfc/test-utils';
import ExtensionContext from '@bfc/extension/lib/extensionContext';
import { EditorExtensionContext } from '@bfc/extension-client';

import { ActionNodeWrapper } from '../../../src/adaptive-flow-editor/renderers/NodeWrapper';
import { ShellApiStub } from '../stubs/ShellApiStub';
Expand All @@ -12,15 +12,15 @@ describe('<ActionNodeWrapper>', () => {
it('can render.', () => {
const mockOnEvent = jest.fn();
const ele = render(
<ExtensionContext.Provider
<EditorExtensionContext.Provider
value={{
shellApi: ShellApiStub,
shellData: {} as any,
plugins: [],
}}
>
<ActionNodeWrapper data={{}} id="test" onEvent={mockOnEvent} />
</ExtensionContext.Provider>
</EditorExtensionContext.Provider>
);
expect(ele.getByTestId('ActionNodeWrapper')).toBeTruthy();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import React from 'react';
import { render } from '@bfc/test-utils';
import { WidgetComponent, FlowEditorWidgetMap } from '@bfc/extension';
import { WidgetComponent, FlowEditorWidgetMap } from '@bfc/extension-client';

import { renderUIWidget, UIWidgetContext } from '../../../src/adaptive-flow-renderer/utils/visual/widgetRenderer';
import { FlowWidget } from '../../../src/adaptive-flow-renderer/types/flowRenderer.types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"lint:fix": "yarn lint --fix"
},
"dependencies": {
"@bfc/extension": "*",
"@bfc/extension-client": "*",
"@bfc/shared": "*",
"@bfc/ui-shared": "*",
"@emotion/core": "^10.0.27",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import React, { useRef, useMemo, useEffect } from 'react';
import isEqual from 'lodash/isEqual';
import formatMessage from 'format-message';
import { DialogFactory } from '@bfc/shared';
import { useShellApi, JSONSchema7 } from '@bfc/extension';
import { useShellApi, JSONSchema7 } from '@bfc/extension-client';
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';

import { FlowSchema, FlowWidget } from '../adaptive-flow-renderer/types/flowRenderer.types';
Expand Down
Loading

0 comments on commit 8bc7709

Please sign in to comment.