diff --git a/.vscode/launch.json b/.vscode/launch.json index c9b54c47e0..b341a1c454 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ "request": "launch", "preLaunchTask": "Start web dev server", "url": "http://127.0.0.1:9000", - "webRoot": "${workspaceFolder}", + "webRoot": "${workspaceFolder}/web", "sourceMaps": true, "trace": true, "outFiles": [ @@ -184,4 +184,4 @@ } } ] -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 429c9e24a3..fa37bfd292 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,63 +1,59 @@ { // Angular "vsicons.presets.angular": true, - // CSS "css.validate": false, "scss.validate": false, - // Default Formatting "editor.tabSize": 4, "editor.detectIndentation": false, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": false, - // Per Language Formatting // Keep this up to date with .prettierignore // Run Prettier first, followed by ESLint "[javascript]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": [ - "source.formatDocument", - "source.fixAll.eslint" + "source.formatDocument", + "source.fixAll.eslint" ] }, "[typescript]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": [ - "source.formatDocument", - "source.fixAll.eslint" + "source.formatDocument", + "source.fixAll.eslint" ] }, "[typescriptreact]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": [ - "source.formatDocument", - "source.fixAll.eslint" + "source.formatDocument", + "source.fixAll.eslint" ] }, "[json]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": [ - "source.formatDocument", - "source.fixAll.eslint" + "source.formatDocument", + "source.fixAll.eslint" ] }, "[jsonc]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": [ - "source.formatDocument", - "source.fixAll.eslint" + "source.formatDocument", + "source.fixAll.eslint" ] }, - // Python - "python.autoComplete.extraPaths": ["${workspaceRoot}/python"], + "python.autoComplete.extraPaths": [ + "${workspaceRoot}/python" + ], "python.formatting.provider": "yapf", - // Typescript "typescript.tsdk": "node_modules/typescript/lib", - // General "search.exclude": { "**/node_modules": true, @@ -70,7 +66,14 @@ "desktop/dll/**": true }, "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, "**/__pycache__/**/*": true, - ".awcache/**": true + ".awcache/**": true, + "**/node_modules": true } -} +} \ No newline at end of file diff --git a/packages/playground/src/ui-playground/demo/form/action-form-demo.tsx b/packages/playground/src/ui-playground/demo/form/action-form-demo.tsx index 48ab566409..3b5577433e 100644 --- a/packages/playground/src/ui-playground/demo/form/action-form-demo.tsx +++ b/packages/playground/src/ui-playground/demo/form/action-form-demo.tsx @@ -155,7 +155,11 @@ export const ActionFormDemo: React.FC = () => { > = (props) => { if (props.controllerRef) { controllerRef = props.controllerRef; } + const onChangeRef = React.useRef(props.onChange); + onChangeRef.current = props.onChange; const { value = "" } = props; @@ -121,7 +123,12 @@ export const MonacoEditor: React.FC = (props) => { const model = controllerRef.current.model; - editor = _createEditor(model, props, containerRef.current); + editor = _createEditor( + model, + props, + containerRef.current, + onChangeRef + ); onWindowResize = () => { if (editor) { editor.layout(); @@ -160,7 +167,8 @@ export const MonacoEditor: React.FC = (props) => { function _createEditor( model: monaco.editor.ITextModel, props: MonacoEditorImplProps, - containerEl: HTMLElement + containerEl: HTMLElement, + onChangeRef: React.MutableRefObject<((value: string) => void) | undefined> ): monaco.editor.IStandaloneCodeEditor { const editor = monaco.editor.create(containerEl, { theme: props.theme, @@ -169,11 +177,11 @@ function _createEditor( }); let debouncedOnChange: DebouncedFunction<() => void> | undefined; - if (props.onChange) { + if (onChangeRef.current) { debouncedOnChange = debounce( () => { - if (props.onChange) { - props.onChange(model.getValue()); + if (onChangeRef.current) { + onChangeRef.current(model.getValue()); } }, props.onChangeDelay ?? defaultOnChangeDelay, diff --git a/packages/react/src/ui-react/components/form/form-container.tsx b/packages/react/src/ui-react/components/form/form-container.tsx index 600287978e..f1d9b1aca1 100644 --- a/packages/react/src/ui-react/components/form/form-container.tsx +++ b/packages/react/src/ui-react/components/form/form-container.tsx @@ -70,5 +70,7 @@ export const FormContainer = ( }; }, [form, formValidateHandler]); - return getBrowserEnvironment().getFormLayout(layout).render(form, buttons); + const LayoutComp = getBrowserEnvironment().getFormLayout(layout); + + return ; }; diff --git a/packages/react/src/ui-react/components/form/form-layout-provider.ts b/packages/react/src/ui-react/components/form/form-layout-provider.ts index b1ade4afd6..10a8e0849b 100644 --- a/packages/react/src/ui-react/components/form/form-layout-provider.ts +++ b/packages/react/src/ui-react/components/form/form-layout-provider.ts @@ -1,14 +1,18 @@ -import { FormLayout, FormLayoutProvider, FormLayoutType } from "./form-layout"; -import { ListFormLayout } from "./list-form-layout"; +import { FormValues } from "@batch/ui-common/lib/form"; +import React from "react"; +import { FormLayoutProvider, FormLayoutType, LayoutProps } from "./form-layout"; +import { ListFormLayoutComp } from "./list-form-layout"; export class DefaultFormLayoutProvider implements FormLayoutProvider { - getLayout(layout: FormLayoutType): FormLayout { + getLayout( + layout: FormLayoutType + ): React.FC> { if (layout === "list") { - return new ListFormLayout(); + return ListFormLayoutComp; } else if (layout === "steps") { // TODO: Make a layout where each top-level section is a 'step' // in the form. - return new ListFormLayout(); + return ListFormLayoutComp; } throw new Error(`Invalid form layout type: ${layout}`); } diff --git a/packages/react/src/ui-react/components/form/form-layout.tsx b/packages/react/src/ui-react/components/form/form-layout.tsx index fb028b1265..ccb9d3acea 100644 --- a/packages/react/src/ui-react/components/form/form-layout.tsx +++ b/packages/react/src/ui-react/components/form/form-layout.tsx @@ -5,7 +5,14 @@ import { FormButton } from "./form-container"; export type FormLayoutType = "list" | "steps"; export interface FormLayoutProvider { - getLayout(layout: FormLayoutType): FormLayout; + getLayout( + layout: FormLayoutType + ): React.FC>; +} + +export interface LayoutProps { + form: Form; + button?: FormButton[]; } export interface FormLayout { diff --git a/packages/react/src/ui-react/components/form/list-form-layout.tsx b/packages/react/src/ui-react/components/form/list-form-layout.tsx index 549471395c..2b1ac93d8f 100644 --- a/packages/react/src/ui-react/components/form/list-form-layout.tsx +++ b/packages/react/src/ui-react/components/form/list-form-layout.tsx @@ -9,49 +9,48 @@ import { getBrowserEnvironment } from "../../environment"; import { useUniqueId } from "../../hooks"; import { useAppTheme } from "../../theme"; import { FormButton } from "./form-container"; -import { FormLayout } from "./form-layout"; /** - * Render a form as a flat list. + * React Component to render a form as a flat list. */ -export class ListFormLayout implements FormLayout { - render( - form: Form, - buttons?: FormButton[] - ): JSX.Element { - const rows: JSX.Element[] = []; - this._renderChildEntries(form.childEntries(), rows); - return ( -
-

- {form.title ?? "Untitled form"} -

- {rows} - - - -
- ); - } +export function ListFormLayoutComp(props: { + form: Form; + buttons?: FormButton[]; +}): JSX.Element { + const { form, buttons } = props; + const rows: JSX.Element[] = []; + _renderChildEntries(form.childEntries(), rows); + + return ( +
+

+ {form.title ?? "Untitled form"} +

+ {rows} + + + +
+ ); +} - private _renderChildEntries( - entries: IterableIterator>, - rows: JSX.Element[] - ) { - for (const entry of entries) { - if (entry instanceof Parameter) { - rows.push(); - } else if (entry instanceof Section) { - rows.push(); - if (entry.childEntriesCount > 0) { - this._renderChildEntries(entry.childEntries(), rows); - } - } else if (entry instanceof SubForm) { - rows.push(); - if (entry.childEntriesCount > 0) { - this._renderChildEntries(entry.childEntries(), rows); - } +function _renderChildEntries( + entries: IterableIterator>, + rows: JSX.Element[] +) { + for (const entry of entries) { + if (entry instanceof Parameter) { + rows.push(); + } else if (entry instanceof Section) { + rows.push(); + if (entry.childEntriesCount > 0) { + _renderChildEntries(entry.childEntries(), rows); + } + } else if (entry instanceof SubForm) { + rows.push(); + if (entry.childEntriesCount > 0) { + _renderChildEntries(entry.childEntries(), rows); } } } diff --git a/packages/react/src/ui-react/environment/browser-environment.ts b/packages/react/src/ui-react/environment/browser-environment.ts index 46615422d9..ae7da08465 100644 --- a/packages/react/src/ui-react/environment/browser-environment.ts +++ b/packages/react/src/ui-react/environment/browser-environment.ts @@ -9,10 +9,11 @@ import { } from "@batch/ui-common/lib/environment"; import { FormValues } from "@batch/ui-common/lib/form"; import { StorageAccountService, SubscriptionService } from "@batch/ui-service"; +import React from "react"; import { - FormLayout, FormLayoutProvider, FormLayoutType, + LayoutProps, } from "../components/form/form-layout"; import { FormControlOptions, @@ -35,7 +36,9 @@ export interface BrowserEnvironment opts?: FormControlOptions ): JSX.Element; - getFormLayout(layoutType?: FormLayoutType): FormLayout; + getFormLayout( + layoutType?: FormLayoutType + ): React.FC>; } export interface BrowserEnvironmentConfig extends EnvironmentConfig { @@ -90,7 +93,9 @@ export class DefaultBrowserEnvironment /** * Get the form control for a given parameter */ - getFormLayout(layoutType: FormLayoutType = "list"): FormLayout { + getFormLayout( + layoutType: FormLayoutType = "list" + ): React.FC> { const provider = this.getInjectable( BrowserDependencyName.FormLayoutProvider ); diff --git a/packages/react/src/ui-react/environment/mock-browser-environment.ts b/packages/react/src/ui-react/environment/mock-browser-environment.ts index 20e29a428d..5a400c4957 100644 --- a/packages/react/src/ui-react/environment/mock-browser-environment.ts +++ b/packages/react/src/ui-react/environment/mock-browser-environment.ts @@ -1,11 +1,12 @@ import { Parameter } from "@batch/ui-common"; import { MockEnvironment } from "@batch/ui-common/lib/environment"; import { FormValues } from "@batch/ui-common/lib/form"; +import React from "react"; import { FormControlOptions, ParameterTypeResolver } from "../components/form"; import { - FormLayout, FormLayoutProvider, FormLayoutType, + LayoutProps, } from "../components/form/form-layout"; import { BrowserDependencyName, @@ -53,7 +54,9 @@ export class MockBrowserEnvironment } // TODO: This code shouldn't need to be duplicated from DefaultBrowserEnvironment - getFormLayout(layoutType: FormLayoutType = "list"): FormLayout { + getFormLayout( + layoutType: FormLayoutType = "list" + ): React.FC> { const provider = this.getInjectable( BrowserDependencyName.FormLayoutProvider ); diff --git a/web/package-lock.json b/web/package-lock.json index 3b8d8c95ae..aa6ece9392 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,6 +23,7 @@ "jest": "^27.1.0", "jest-junit": "^12.2.0", "monaco-editor-webpack-plugin": "^4.0.0", + "source-map-loader": "^1.1.3", "style-loader": "^0.23.1", "ts-jest": "^27.0.5", "ts-loader": "^8.1.0", @@ -1254,6 +1255,12 @@ "pretty-format": "^27.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -10455,6 +10462,60 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", + "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "dev": true, + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.6.1", + "whatwg-mimetype": "^2.3.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -14632,6 +14693,12 @@ "pretty-format": "^27.0.0" } }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -21911,6 +21978,42 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-loader": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", + "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.2", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "source-map": "^0.6.1", + "whatwg-mimetype": "^2.3.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", diff --git a/web/package.json b/web/package.json index d7542567e4..f6c979a744 100644 --- a/web/package.json +++ b/web/package.json @@ -87,6 +87,7 @@ "jest": "^27.1.0", "jest-junit": "^12.2.0", "monaco-editor-webpack-plugin": "^4.0.0", + "source-map-loader": "^1.1.3", "style-loader": "^0.23.1", "ts-jest": "^27.0.5", "ts-loader": "^8.1.0", diff --git a/web/webpack.config.js b/web/webpack.config.js index 0a74d6e025..8417eca67f 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -125,6 +125,12 @@ module.exports = (env) => { test: /\.ttf$/, use: ["file-loader"], }, + { + test: /\.js$/, + include: path.resolve(__dirname, "../packages"), + enforce: "pre", + use: ["source-map-loader"], + }, ], },