diff --git a/docs/DEV-SETUP-DEBIAN.md b/docs/DEV-SETUP-DEBIAN.md index 88a124708c..9edd8286cb 100644 --- a/docs/DEV-SETUP-DEBIAN.md +++ b/docs/DEV-SETUP-DEBIAN.md @@ -1,12 +1,15 @@ ## Dev Environment on Debian 11 -### Install Node +### Install NVM & Node 14 +NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating -Budibase requires a recent version of node (14+): +Install NVM ``` -curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - -apt -y install nodejs -node -v +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +``` +Install Node 14 +``` +nvm install 14 ``` ### Install npm requirements @@ -31,7 +34,7 @@ This setup process was tested on Debian 11 (bullseye) with version numbers show - Docker: 20.10.5 - Docker-Compose: 1.29.2 -- Node: v16.15.1 +- Node: v14.20.1 - Yarn: 1.22.19 - Lerna: 5.1.4 diff --git a/docs/DEV-SETUP-MACOSX.md b/docs/DEV-SETUP-MACOSX.md index c5990e58da..d9e2dcad6a 100644 --- a/docs/DEV-SETUP-MACOSX.md +++ b/docs/DEV-SETUP-MACOSX.md @@ -11,7 +11,7 @@ through brew. ### Install Node -Budibase requires a recent version of node (14+): +Budibase requires a recent version of node 14: ``` brew install node npm node -v @@ -38,7 +38,7 @@ This setup process was tested on Mac OSX 12 (Monterey) with version numbers show - Docker: 20.10.14 - Docker-Compose: 2.6.0 -- Node: 18.3.0 +- Node: 14.20.1 - Yarn: 1.22.19 - Lerna: 5.1.4 @@ -59,4 +59,7 @@ The dev version will be available on port 10000 i.e. http://127.0.0.1:10000/builder/admin | **NOTE**: If you are working on a M1 Apple Silicon, you will need to uncomment `# platform: linux/amd64` line in -[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml) \ No newline at end of file +[hosting/docker-compose-dev.yaml](../hosting/docker-compose.dev.yaml) + +### Troubleshooting +If there are errors with the `yarn setup` command, you can try installing nvm and node 14. This is the same as the instructions for Debian 11. diff --git a/docs/DEV-SETUP-WINDOWS.md b/docs/DEV-SETUP-WINDOWS.md new file mode 100644 index 0000000000..c5608b7567 --- /dev/null +++ b/docs/DEV-SETUP-WINDOWS.md @@ -0,0 +1,81 @@ +## Dev Environment on Windows 10/11 (WSL2) + + +### Install WSL with Ubuntu LTS + +Enable WSL 2 on Windows 10/11 for docker support. +``` +wsl --set-default-version 2 +``` +Install Ubuntu LTS. +``` +wsl --install Ubuntu +``` + +Or follow the instruction here: +https://learn.microsoft.com/en-us/windows/wsl/install + +### Install Docker in windows +Download the installer from docker and install it. + +Check this url for more detailed instructions: +https://docs.docker.com/desktop/install/windows-install/ + +You should follow the next steps from within the Ubuntu terminal. + +### Install NVM & Node 14 +NVM documentation: https://github.com/nvm-sh/nvm#installing-and-updating + +Install NVM +``` +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +``` +Install Node 14 +``` +nvm install 14 +``` + + +### Install npm requirements + +``` +npm install -g yarn jest lerna +``` + +### Clone the repo +``` +git clone https://github.com/Budibase/budibase.git +``` + +### Check Versions + +This setup process was tested on Windows 11 with version numbers show below. Your mileage may vary using anything else. + +- Docker: 20.10.7 +- Docker-Compose: 2.10.2 +- Node: v14.20.1 +- Yarn: 1.22.19 +- Lerna: 5.5.4 + +### Build + +``` +cd budibase +yarn setup +``` +The yarn setup command runs several build steps i.e. +``` +node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev +``` +So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose. + +The dev version will be available on port 10000 i.e. + +http://127.0.0.1:10000/builder/admin + +### Working with the code +Here are the instructions to work on the application from within Visual Studio Code (in Windows) through the WSL. All the commands and files are within the Ubuntu system and it should run as if you were working on a Linux machine. + +https://code.visualstudio.com/docs/remote/wsl + +Note you will be able to run the application from within the WSL terminal and you will be able to access the application from the a browser in Windows. \ No newline at end of file diff --git a/hosting/single/Dockerfile b/hosting/single/Dockerfile index f34290f627..58796f0362 100644 --- a/hosting/single/Dockerfile +++ b/hosting/single/Dockerfile @@ -19,8 +19,8 @@ ADD packages/worker . RUN node /pinVersions.js && yarn && yarn build && /cleanup.sh FROM couchdb:3.2.1 -# TARGETARCH can be amd64 or arm e.g. docker build --build-arg TARGETARCH=amd64 -ARG TARGETARCH=amd64 +ARG TARGETARCH +ENV TARGETARCH $TARGETARCH #TARGETBUILD can be set to single (for single docker image) or aas (for azure app service) # e.g. docker build --build-arg TARGETBUILD=aas .... ARG TARGETBUILD=single diff --git a/lerna.json b/lerna.json index 4e08b98680..a47d8fe604 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 75cd8ff374..8df964912e 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "2.0.14-alpha.2", + "@budibase/types": "2.0.24-alpha.3", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", diff --git a/packages/backend-core/src/redis/index.ts b/packages/backend-core/src/redis/index.ts index 206110366f..62e718d8ad 100644 --- a/packages/backend-core/src/redis/index.ts +++ b/packages/backend-core/src/redis/index.ts @@ -214,6 +214,31 @@ export = class RedisWrapper { } } + async bulkGet(keys: string[]) { + const db = this._db + const prefixedKeys = keys.map(key => addDbPrefix(db, key)) + let response = await this.getClient().mget(prefixedKeys) + if (Array.isArray(response)) { + let final: any = {} + let count = 0 + for (let result of response) { + if (result) { + let parsed + try { + parsed = JSON.parse(result) + } catch (err) { + parsed = result + } + final[keys[count]] = parsed + } + count++ + } + return final + } else { + throw new Error(`Invalid response: ${response}`) + } + } + async store(key: string, value: any, expirySeconds: number | null = null) { const db = this._db if (typeof value === "object") { diff --git a/packages/bbui/package.json b/packages/bbui/package.json index ebbdbdd49a..0583237a45 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "2.0.14-alpha.2", + "@budibase/string-templates": "2.0.24-alpha.3", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/builder/cypress/integration/queryLevelTransformers.spec.js b/packages/builder/cypress/integration/queryLevelTransformers.spec.js index 2b74e0c2e5..d16f8075f9 100644 --- a/packages/builder/cypress/integration/queryLevelTransformers.spec.js +++ b/packages/builder/cypress/integration/queryLevelTransformers.spec.js @@ -1,5 +1,5 @@ import filterTests from "../support/filterTests" -const interact = require('../support/interact') +const interact = require("../support/interact") filterTests(["smoke", "all"], () => { context("Query Level Transformers", () => { diff --git a/packages/builder/package.json b/packages/builder/package.json index 3d05114a82..900b33fb88 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "license": "GPL-3.0", "private": true, "scripts": { @@ -71,10 +71,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.0.14-alpha.2", - "@budibase/client": "2.0.14-alpha.2", - "@budibase/frontend-core": "2.0.14-alpha.2", - "@budibase/string-templates": "2.0.14-alpha.2", + "@budibase/bbui": "2.0.24-alpha.3", + "@budibase/client": "2.0.24-alpha.3", + "@budibase/frontend-core": "2.0.24-alpha.3", + "@budibase/string-templates": "2.0.24-alpha.3", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/builderStore/componentUtils.js b/packages/builder/src/builderStore/componentUtils.js index e7787dea72..54997df38f 100644 --- a/packages/builder/src/builderStore/componentUtils.js +++ b/packages/builder/src/builderStore/componentUtils.js @@ -185,43 +185,42 @@ export const makeComponentUnique = component => { // Replace component ID const oldId = component._id const newId = Helpers.uuid() - component._id = newId + let definition = JSON.stringify(component) - if (component._children?.length) { - let children = JSON.stringify(component._children) + // Replace all instances of this ID in HBS bindings + definition = definition.replace(new RegExp(oldId, "g"), newId) - // Replace all instances of this ID in child HBS bindings - children = children.replace(new RegExp(oldId, "g"), newId) + // Replace all instances of this ID in JS bindings + const bindings = findHBSBlocks(definition) + bindings.forEach(binding => { + // JSON.stringify will have escaped double quotes, so we need + // to account for that + let sanitizedBinding = binding.replace(/\\"/g, '"') - // Replace all instances of this ID in child JS bindings - const bindings = findHBSBlocks(children) - bindings.forEach(binding => { - // JSON.stringify will have escaped double quotes, so we need - // to account for that - let sanitizedBinding = binding.replace(/\\"/g, '"') + // Check if this is a valid JS binding + let js = decodeJSBinding(sanitizedBinding) + if (js != null) { + // Replace ID inside JS binding + js = js.replace(new RegExp(oldId, "g"), newId) - // Check if this is a valid JS binding - let js = decodeJSBinding(sanitizedBinding) - if (js != null) { - // Replace ID inside JS binding - js = js.replace(new RegExp(oldId, "g"), newId) + // Create new valid JS binding + let newBinding = encodeJSBinding(js) - // Create new valid JS binding - let newBinding = encodeJSBinding(js) + // Replace escaped double quotes + newBinding = newBinding.replace(/"/g, '\\"') - // Replace escaped double quotes - newBinding = newBinding.replace(/"/g, '\\"') + // Insert new JS back into binding. + // A single string replace here is better than a regex as + // the binding contains special characters, and we only need + // to replace a single instance. + definition = definition.replace(binding, newBinding) + } + }) - // Insert new JS back into binding. - // A single string replace here is better than a regex as - // the binding contains special characters, and we only need - // to replace a single instance. - children = children.replace(binding, newBinding) - } - }) - - // Recurse on all children - component._children = JSON.parse(children) - component._children.forEach(makeComponentUnique) + // Recurse on all children + component = JSON.parse(definition) + return { + ...component, + _children: component._children?.map(makeComponentUnique), } } diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 7456ec5691..536692eecc 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -169,7 +169,12 @@ export const getComponentBindableProperties = (asset, componentId) => { /** * Gets all data provider components above a component. */ -export const getContextProviderComponents = (asset, componentId, type) => { +export const getContextProviderComponents = ( + asset, + componentId, + type, + options = { includeSelf: false } +) => { if (!asset || !componentId) { return [] } @@ -177,7 +182,9 @@ export const getContextProviderComponents = (asset, componentId, type) => { // Get the component tree leading up to this component, ignoring the component // itself const path = findComponentPath(asset.props, componentId) - path.pop() + if (!options?.includeSelf) { + path.pop() + } // Filter by only data provider components return path.filter(component => { @@ -396,19 +403,17 @@ export const getUserBindings = () => { bindings = keys.reduce((acc, key) => { const fieldSchema = schema[key] - if (fieldSchema.type !== "link") { - acc.push({ - type: "context", - runtimeBinding: `${safeUser}.${makePropSafe(key)}`, - readableBinding: `Current User.${key}`, - // Field schema and provider are required to construct relationship - // datasource options, based on bindable properties - fieldSchema, - providerId: "user", - category: "Current User", - icon: "User", - }) - } + acc.push({ + type: "context", + runtimeBinding: `${safeUser}.${makePropSafe(key)}`, + readableBinding: `Current User.${key}`, + // Field schema and provider are required to construct relationship + // datasource options, based on bindable properties + fieldSchema, + providerId: "user", + category: "Current User", + icon: "User", + }) return acc }, []) @@ -800,6 +805,17 @@ export const buildFormSchema = component => { if (!component) { return schema } + + // If this is a form block, simply use the fields setting + if (component._component.endsWith("formblock")) { + let schema = {} + component.fields?.forEach(field => { + schema[field] = { type: "string" } + }) + return schema + } + + // Otherwise find all field component children const settings = getComponentSettings(component._component) const fieldSetting = settings.find( setting => setting.key === "field" && setting.type.startsWith("field/") diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index aefdba9fb2..c90ab10c9a 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -330,6 +330,16 @@ export const getFrontendStore = () => { return state }) }, + sendEvent: (name, payload) => { + const { previewEventHandler } = get(store) + previewEventHandler?.(name, payload) + }, + registerEventHandler: handler => { + store.update(state => { + state.previewEventHandler = handler + return state + }) + }, }, layouts: { select: layoutId => { @@ -611,7 +621,7 @@ export const getFrontendStore = () => { // Make new component unique if copying if (!cut) { - makeComponentUnique(componentToPaste) + componentToPaste = makeComponentUnique(componentToPaste) } newComponentId = componentToPaste._id @@ -891,6 +901,50 @@ export const getFrontendStore = () => { component[name] = value }) }, + requestEjectBlock: componentId => { + store.actions.preview.sendEvent("eject-block", componentId) + }, + handleEjectBlock: async (componentId, ejectedDefinition) => { + let nextSelectedComponentId + + await store.actions.screens.patch(screen => { + const block = findComponent(screen.props, componentId) + const parent = findComponentParent(screen.props, componentId) + + // Sanity check + if (!block || !parent?._children?.length) { + return false + } + + // Attach block children back into ejected definition, using the + // _containsSlot flag to know where to insert them + const slotContainer = findAllMatchingComponents( + ejectedDefinition, + x => x._containsSlot + )[0] + if (slotContainer) { + delete slotContainer._containsSlot + slotContainer._children = [ + ...(slotContainer._children || []), + ...(block._children || []), + ] + } + + // Replace block with ejected definition + ejectedDefinition = makeComponentUnique(ejectedDefinition) + const index = parent._children.findIndex(x => x._id === componentId) + parent._children[index] = ejectedDefinition + nextSelectedComponentId = ejectedDefinition._id + }) + + // Select new root component + if (nextSelectedComponentId) { + store.update(state => { + state.selectedComponentId = nextSelectedComponentId + return state + }) + } + }, }, links: { save: async (url, title) => { diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 21059b32dd..3fd38bddeb 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -314,7 +314,7 @@ const relatedTable = $tables.list.find( tbl => tbl._id === fieldInfo.tableId ) - if (inUse(relatedTable, fieldInfo.fieldName)) { + if (inUse(relatedTable, fieldInfo.fieldName) && !originalName) { newError.relatedName = `Column name already in use in table ${relatedTable.name}` } } diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte index 69d5fe60b4..ef7c81233b 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionDrawer.svelte @@ -21,6 +21,7 @@ export let key export let actions export let bindings = [] + export let nested $: showAvailableActions = !actions?.length @@ -187,6 +188,7 @@ this={selectedActionComponent} parameters={selectedAction.parameters} bindings={allBindings} + {nested} /> {/key} diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte index f8fb385eb3..6a23ba8cbd 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/ButtonActionEditor.svelte @@ -12,6 +12,7 @@ export let value = [] export let name export let bindings + export let nested let drawer let tmpValue @@ -90,6 +91,7 @@ eventType={name} {bindings} {key} + {nested} /> diff --git a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte index 174962d824..433f4bb3c2 100644 --- a/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte +++ b/packages/builder/src/components/design/settings/controls/ButtonActionEditor/actions/SaveRow.svelte @@ -10,11 +10,13 @@ export let parameters export let bindings = [] + export let nested $: formComponents = getContextProviderComponents( $currentAsset, $store.selectedComponentId, - "form" + "form", + { includeSelf: nested } ) $: schemaComponents = getContextProviderComponents( $currentAsset, diff --git a/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte b/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte new file mode 100644 index 0000000000..e19d4b584b --- /dev/null +++ b/packages/builder/src/components/design/settings/controls/EjectBlockButton.svelte @@ -0,0 +1,13 @@ + + +
+ Eject block +
diff --git a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte index 3927e0b3a5..70b88c41e5 100644 --- a/packages/builder/src/components/design/settings/controls/PropertyControl.svelte +++ b/packages/builder/src/components/design/settings/controls/PropertyControl.svelte @@ -20,6 +20,7 @@ export let componentBindings = [] export let nested = false export let highlighted = false + export let info = null $: nullishValue = value == null || value === "" $: allBindings = getAllBindings(bindings, componentBindings, nested) @@ -94,11 +95,15 @@ bindings={allBindings} name={key} text={label} + {nested} {key} {type} {...props} /> + {#if info} +
{@html info}
+ {/if} diff --git a/packages/builder/src/components/design/settings/controls/URLSelect.svelte b/packages/builder/src/components/design/settings/controls/URLSelect.svelte index dc2fa7ad89..a07c2190da 100644 --- a/packages/builder/src/components/design/settings/controls/URLSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/URLSelect.svelte @@ -4,6 +4,7 @@ export let value export let bindings + export let placeholder $: urlOptions = $store.screens .map(screen => screen.routing?.route) @@ -13,6 +14,7 @@ { + iframe?.contentWindow.postMessage( + JSON.stringify({ + name, + payload, + isBudibaseEvent: true, + runtimeEvent: true, + }) + ) + }) + // Update the iframe with the builder info to render the correct preview const refreshContent = message => { - if (iframe) { - iframe.contentWindow.postMessage(message) - } + iframe?.contentWindow.postMessage(message) } const receiveMessage = message => { @@ -198,6 +208,9 @@ block: "center", }) } + } else if (type === "eject-block") { + const { id, definition } = data + await store.actions.components.handleEjectBlock(id, definition) } else if (type === "reload-plugin") { await store.actions.components.refreshDefinitions() } else { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte index c19cba1aac..aeaa577455 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentDropdownMenu.svelte @@ -4,7 +4,9 @@ export let component + $: definition = store.actions.components.getDefinition(component?._component) $: noPaste = !$store.componentToPaste + $: isBlock = definition?.block === true const keyboardEvent = (key, ctrlKey = false) => { document.dispatchEvent( @@ -30,6 +32,15 @@ > Delete + {#if isBlock} + keyboardEvent("e", true)} + > + Eject block + + {/if} { @@ -29,6 +31,10 @@ store.actions.components.copy(component) await store.actions.components.paste(component, "below") }, + ["^e"]: component => { + componentToEject = component + confirmEjectDialog.show() + }, ["^Enter"]: () => { $goto("./new") }, @@ -124,3 +130,10 @@ okText="Delete Component" onOk={() => store.actions.components.delete(componentToDelete)} /> + store.actions.components.requestEjectBlock(componentToEject?._id)} + okText="Eject block" +/> diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte index b4c8e7abad..efd3db8ff7 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte @@ -4,6 +4,7 @@ import { store } from "builderStore" import PropertyControl from "components/design/settings/controls/PropertyControl.svelte" import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte" + import EjectBlockButton from "components/design/settings/controls/EjectBlockButton.svelte" import { getComponentForSetting } from "components/design/settings/componentSettings" export let componentDefinition @@ -12,20 +13,29 @@ export let componentBindings export let isScreen = false - $: sections = getSections(componentDefinition) + $: sections = getSections(componentInstance, componentDefinition, isScreen) - const getSections = definition => { + const getSections = (instance, definition, isScreen) => { const settings = definition?.settings ?? [] const generalSettings = settings.filter(setting => !setting.section) const customSections = settings.filter(setting => setting.section) - return [ + let sections = [ { name: "General", - info: componentDefinition?.info, settings: generalSettings, }, ...(customSections || []), ] + + // Filter out settings which shouldn't be rendered + sections.forEach(section => { + section.settings.forEach(setting => { + setting.visible = canRenderControl(instance, setting, isScreen) + }) + section.visible = section.settings.some(setting => setting.visible) + }) + + return sections } const updateSetting = async (key, value) => { @@ -36,7 +46,7 @@ } } - const canRenderControl = (setting, isScreen) => { + const canRenderControl = (instance, setting, isScreen) => { // Prevent rendering on click setting for screens if (setting?.type === "event" && isScreen) { return false @@ -51,6 +61,7 @@ if (setting.dependsOn) { let dependantSetting = setting.dependsOn let dependantValue = null + let invert = !!setting.dependsOn.invert if (typeof setting.dependsOn === "object") { dependantSetting = setting.dependsOn.setting dependantValue = setting.dependsOn.value @@ -62,7 +73,7 @@ // If no specific value is depended upon, check if a value exists at all // for the dependent setting if (dependantValue == null) { - const currentValue = componentInstance[dependantSetting] + const currentValue = instance[dependantSetting] if (currentValue === false) { return false } @@ -73,7 +84,11 @@ } // Otherwise check the value matches - return componentInstance[dependantSetting] === dependantValue + if (invert) { + return instance[dependantSetting] !== dependantValue + } else { + return instance[dependantSetting] === dependantValue + } } return true @@ -81,60 +96,54 @@ {#each sections as section, idx (section.name)} - - {#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen} - updateSetting("_instanceName", val)} - /> - {/if} - {#each section.settings as setting (setting.key)} - {#if canRenderControl(setting, isScreen)} + {#if section.visible} + + {#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen} updateSetting(setting.key, val)} - highlighted={$store.highlightedSettingKey === setting.key} - props={{ - // Generic settings - placeholder: setting.placeholder || null, - - // Select settings - options: setting.options || [], - - // Number fields - min: setting.min || null, - max: setting.max || null, - }} - {bindings} - {componentBindings} - {componentInstance} - {componentDefinition} + control={Input} + label="Name" + key="_instanceName" + value={componentInstance._instanceName} + onChange={val => updateSetting("_instanceName", val)} /> {/if} - {/each} - {#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")} - - {/if} - {#if section?.info} -
- {@html section.info} -
- {/if} -
-{/each} + {#each section.settings as setting (setting.key)} + {#if setting.visible} + updateSetting(setting.key, val)} + highlighted={$store.highlightedSettingKey === setting.key} + info={setting.info} + props={{ + // Generic settings + placeholder: setting.placeholder || null, - + // Select settings + options: setting.options || [], + + // Number fields + min: setting.min || null, + max: setting.max || null, + }} + {bindings} + {componentBindings} + {componentInstance} + {componentDefinition} + /> + {/if} + {/each} + {#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")} + + {/if} + {#if idx === 0 && componentDefinition?.block} + + {/if} +
+ {/if} +{/each} diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json index 671637f381..088f0c0989 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/componentStructure.json @@ -5,7 +5,8 @@ "children": [ "tableblock", "cardsblock", - "repeaterblock" + "repeaterblock", + "formblock" ] }, { diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte index 0c35fa391e..ec965ed659 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/screens/_components/ScreenDropdownMenu.svelte @@ -38,7 +38,7 @@ let duplicateScreen = Helpers.cloneDeep(screen) delete duplicateScreen._id delete duplicateScreen._rev - makeComponentUnique(duplicateScreen.props) + duplicateScreen.props = makeComponentUnique(duplicateScreen.props) // Attach the new name and URL duplicateScreen.routing.route = sanitizeUrl(screenUrl) diff --git a/packages/cli/package.json b/packages/cli/package.json index 4921e9a3e9..18eff4ce2a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.0.14-alpha.2", - "@budibase/string-templates": "2.0.14-alpha.2", - "@budibase/types": "2.0.14-alpha.2", + "@budibase/backend-core": "2.0.24-alpha.3", + "@budibase/string-templates": "2.0.24-alpha.3", + "@budibase/types": "2.0.24-alpha.3", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/cli/src/exec.js b/packages/cli/src/exec.js index 72fd8e00eb..4df486aed6 100644 --- a/packages/cli/src/exec.js +++ b/packages/cli/src/exec.js @@ -22,6 +22,6 @@ exports.runPkgCommand = async (command, dir = "./") => { throw new Error("Must have yarn or npm installed to run build.") } const npmCmd = command === "install" ? `npm ${command}` : `npm run ${command}` - const cmd = yarn ? `yarn ${command}` : npmCmd + const cmd = yarn ? `yarn ${command} --ignore-engines` : npmCmd await exports.exec(cmd, dir) } diff --git a/packages/client/manifest.json b/packages/client/manifest.json index b6d4941e4c..bd5854dc9f 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -3442,7 +3442,6 @@ }, "s3upload": { "name": "S3 File Upload", - "info": "This component can't be used with S3 datasources that use custom endpoints.", "icon": "UploadToCloud", "styles": [ "size" @@ -3463,7 +3462,8 @@ { "type": "dataSource/s3", "label": "S3 Datasource", - "key": "datasourceId" + "key": "datasourceId", + "info": "This component can't be used with S3 datasources that use custom endpoints" }, { "type": "text", @@ -3501,7 +3501,6 @@ }, "dataprovider": { "name": "Data Provider", - "info": "Pagination is only available for data stored in tables.", "icon": "Data", "illegalChildren": [ "section" @@ -3547,7 +3546,8 @@ "type": "boolean", "label": "Paginate", "key": "paginate", - "defaultValue": true + "defaultValue": true, + "info": "Pagination is only available for data stored in tables" } ], "context": { @@ -3589,7 +3589,6 @@ ], "hasChildren": true, "showEmptyState": false, - "info": "Row selection is only compatible with internal or SQL tables", "settings": [ { "type": "dataProvider", @@ -3646,7 +3645,8 @@ "type": "boolean", "label": "Allow row selection", "key": "allowSelectRows", - "defaultValue": false + "defaultValue": false, + "info": "Row selection is only compatible with internal or SQL tables" }, { "type": "boolean", @@ -3687,13 +3687,13 @@ "size" ], "hasChildren": false, - "info": "Your data provider will be automatically filtered to the given date range.", "settings": [ { "type": "dataProvider", "label": "Provider", "key": "dataProvider", - "required": true + "required": true, + "info": "Your data provider will be automatically filtered to the given date range." }, { "type": "field", @@ -3828,7 +3828,6 @@ "styles": [ "size" ], - "info": "Only the first 3 search columns will be used.", "settings": [ { "type": "text", @@ -3845,7 +3844,8 @@ "type": "searchfield", "label": "Search Columns", "key": "searchColumns", - "placeholder": "Choose search columns" + "placeholder": "Choose search columns", + "info": "Only the first 5 search columns will be used" }, { "type": "filter", @@ -3892,7 +3892,6 @@ { "section": true, "name": "Table", - "info": "Row selection is only compatible with internal or SQL tables", "settings": [ { "type": "number", @@ -3926,7 +3925,8 @@ { "type": "boolean", "label": "Allow row selection", - "key": "allowSelectRows" + "key": "allowSelectRows", + "info": "Row selection is only compatible with internal or SQL tables" }, { "type": "boolean", @@ -3993,7 +3993,6 @@ "styles": [ "size" ], - "info": "Only the first 3 search columns will be used.", "settings": [ { "type": "text", @@ -4010,7 +4009,8 @@ "type": "searchfield", "label": "Search Columns", "key": "searchColumns", - "placeholder": "Choose search columns" + "placeholder": "Choose search columns", + "info": "Only the first 5 search columns will be used" }, { "type": "filter", @@ -4157,6 +4157,7 @@ } }, "repeaterblock": { + "block": true, "name": "Repeater block", "icon": "ViewList", "illegalChildren": [ @@ -4394,5 +4395,145 @@ "required": true } ] + }, + "formblock": { + "name": "Form Block", + "icon": "Form", + "styles": ["size"], + "block": true, + "info": "Form blocks are only compatible with internal or SQL tables", + "settings": [ + { + "type": "select", + "label": "Type", + "key": "actionType", + "options": ["Create", "Update", "View"], + "defaultValue": "Create" + }, + { + "type": "table", + "label": "Table", + "key": "dataSource" + }, + { + "type": "text", + "label": "Row ID", + "key": "rowId", + "nested": true, + "dependsOn": { + "setting": "actionType", + "value": "Create", + "invert": true + } + }, + { + "type": "text", + "label": "Title", + "key": "title", + "nested": true + }, + { + "type": "select", + "label": "Size", + "key": "size", + "options": [ + { + "label": "Medium", + "value": "spectrum--medium" + }, + { + "label": "Large", + "value": "spectrum--large" + } + ], + "defaultValue": "spectrum--medium" + }, + { + "section": true, + "name": "Fields", + "settings": [ + { + "type": "multifield", + "label": "Fields", + "key": "fields" + }, + { + "type": "select", + "label": "Field labels", + "key": "labelPosition", + "defaultValue": "left", + "options": [ + { + "label": "Left", + "value": "left" + }, + { + "label": "Above", + "value": "above" + } + ] + }, + { + "type": "boolean", + "label": "Disabled", + "key": "disabled", + "defaultValue": false, + "dependsOn": { + "setting": "actionType", + "value": "View", + "invert": true + } + } + ] + }, + { + "section": true, + "name": "Buttons", + "settings": [ + { + "type": "boolean", + "label": "Show save button", + "key": "showSaveButton", + "defaultValue": true, + "dependsOn": { + "setting": "actionType", + "value": "View", + "invert": true + } + }, + { + "type": "boolean", + "label": "Show delete button", + "key": "showDeleteButton", + "defaultValue": false, + "dependsOn": { + "setting": "actionType", + "value": "Update" + } + }, + { + "type": "url", + "label": "Navigate after button press", + "key": "actionUrl", + "placeholder": "Choose a screen", + "dependsOn": { + "setting": "actionType", + "value": "View", + "invert": true + } + } + ] + } + ], + "context": [ + { + "type": "form", + "suffix": "form" + }, + { + "type": "schema", + "suffix": "repeater" + } + ] } } \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json index 1e2049ac27..1676a722ba 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.0.14-alpha.2", - "@budibase/frontend-core": "2.0.14-alpha.2", - "@budibase/string-templates": "2.0.14-alpha.2", + "@budibase/bbui": "2.0.24-alpha.3", + "@budibase/frontend-core": "2.0.24-alpha.3", + "@budibase/string-templates": "2.0.24-alpha.3", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/client/src/api/api.js b/packages/client/src/api/api.js index 563126fdd0..8d29d37bd6 100644 --- a/packages/client/src/api/api.js +++ b/packages/client/src/api/api.js @@ -1,5 +1,7 @@ import { createAPIClient } from "@budibase/frontend-core" -import { notificationStore, authStore, devToolsStore } from "../stores" +import { notificationStore } from "../stores/notification.js" +import { authStore } from "../stores/auth.js" +import { devToolsStore } from "../stores/devTools.js" import { get } from "svelte/store" export const API = createAPIClient({ diff --git a/packages/client/src/components/Block.svelte b/packages/client/src/components/Block.svelte index b5e610c1bb..05d92f208c 100644 --- a/packages/client/src/components/Block.svelte +++ b/packages/client/src/components/Block.svelte @@ -1,12 +1,92 @@ - +
+ +
diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte index c23f18f55c..2f756ce296 100644 --- a/packages/client/src/components/BlockComponent.svelte +++ b/packages/client/src/components/BlockComponent.svelte @@ -1,17 +1,21 @@ diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index ab19e91038..537e963ff3 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -16,7 +16,6 @@ themeStore, appStore, devToolsStore, - environmentStore, } from "stores" import NotificationDisplay from "components/overlay/NotificationDisplay.svelte" import ConfirmationDisplay from "components/overlay/ConfirmationDisplay.svelte" @@ -48,8 +47,6 @@ !$builderStore.inBuilder && $devToolsStore.enabled && !$routeStore.queryParams?.peek - $: objectStoreUrl = $environmentStore.cloud ? "https://cdn.budi.live" : "" - $: pluginsUrl = `${objectStoreUrl}/plugins` // Handle no matching route $: { @@ -95,8 +92,7 @@ {#if $builderStore.usedPlugins?.length} {#each $builderStore.usedPlugins as plugin (plugin.hash)} - + {/each} {/if} diff --git a/packages/client/src/components/app/blocks/CardsBlock.svelte b/packages/client/src/components/app/blocks/CardsBlock.svelte index a13364833a..9c110d7097 100644 --- a/packages/client/src/components/app/blocks/CardsBlock.svelte +++ b/packages/client/src/components/app/blocks/CardsBlock.svelte @@ -2,7 +2,6 @@ import { getContext } from "svelte" import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" - import { Heading } from "@budibase/bbui" import { makePropSafe as safe } from "@budibase/string-templates" import { enrichSearchColumns, enrichFilter } from "utils/blocks.js" @@ -31,9 +30,7 @@ export let cardButtonOnClick export let linkColumn - const { fetchDatasourceSchema, styleable } = getContext("sdk") - const context = getContext("context") - const component = getContext("component") + const { fetchDatasourceSchema } = getContext("sdk") let formId let dataProviderId @@ -84,163 +81,132 @@ {#if schemaLoaded} -
- - {#if title || enrichedSearchColumns?.length || showTitleButton} -
-
- {title || ""} -
-
- {#if enrichedSearchColumns?.length} - - {/if} - {#if showTitleButton} - - {/if} -
-
- {/if} + + {#if title || enrichedSearchColumns?.length || showTitleButton} + - + {#if enrichedSearchColumns?.length} + {#each enrichedSearchColumns as column, idx} + + {/each} + {/if} + {#if showTitleButton} + + {/if} + {/if} + + + + -
+
{/if} - - diff --git a/packages/client/src/components/app/blocks/FormBlock.svelte b/packages/client/src/components/app/blocks/FormBlock.svelte new file mode 100644 index 0000000000..5994360bbb --- /dev/null +++ b/packages/client/src/components/app/blocks/FormBlock.svelte @@ -0,0 +1,239 @@ + + + + {#if fields?.length} + + + + + {#if renderHeader} + + + {#if renderButtons} + + {#if renderDeleteButton} + + {/if} + {#if renderSaveButton} + + {/if} + + {/if} + + {/if} + + {#each fields as field, idx} + {#if getComponentForField(field)} + + {/if} + {/each} + + + + + + {:else} + + {/if} + diff --git a/packages/client/src/components/app/blocks/RepeaterBlock.svelte b/packages/client/src/components/app/blocks/RepeaterBlock.svelte index 247a8b0d51..30fbdddcdc 100644 --- a/packages/client/src/components/app/blocks/RepeaterBlock.svelte +++ b/packages/client/src/components/app/blocks/RepeaterBlock.svelte @@ -17,45 +17,43 @@ export let vAlign export let gap - let providerId - const component = getContext("component") - const { styleable } = getContext("sdk") + + let providerId -
- - {#if $component.empty} - - {:else} - - - - {/if} - -
+ + {#if $component.empty} + + {:else} + + + + {/if} +
diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte index e67124fc4f..f75a71a3ee 100644 --- a/packages/client/src/components/app/blocks/TableBlock.svelte +++ b/packages/client/src/components/app/blocks/TableBlock.svelte @@ -2,7 +2,6 @@ import { getContext } from "svelte" import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" - import { Heading } from "@budibase/bbui" import { makePropSafe as safe } from "@budibase/string-templates" import { enrichSearchColumns, enrichFilter } from "utils/blocks.js" @@ -29,9 +28,7 @@ export let titleButtonURL export let titleButtonPeek - const { fetchDatasourceSchema, styleable } = getContext("sdk") - const context = getContext("context") - const component = getContext("component") + const { fetchDatasourceSchema } = getContext("sdk") let formId let dataProviderId @@ -64,145 +61,116 @@ {#if schemaLoaded} -
- - {#if title || enrichedSearchColumns?.length || showTitleButton} -
-
- {title || ""} -
-
- {#if enrichedSearchColumns?.length} - - {/if} - {#if showTitleButton} - - {/if} -
-
- {/if} + + {#if title || enrichedSearchColumns?.length || showTitleButton} + + {#if enrichedSearchColumns?.length} + {#each enrichedSearchColumns as column, idx} + + {/each} + {/if} + {#if showTitleButton} + + {/if} + + {/if} + + -
+
{/if} - - diff --git a/packages/client/src/components/app/blocks/index.js b/packages/client/src/components/app/blocks/index.js index db4de8fc13..32b2b98c06 100644 --- a/packages/client/src/components/app/blocks/index.js +++ b/packages/client/src/components/app/blocks/index.js @@ -1,3 +1,4 @@ export { default as tableblock } from "./TableBlock.svelte" export { default as cardsblock } from "./CardsBlock.svelte" export { default as repeaterblock } from "./RepeaterBlock.svelte" +export { default as formblock } from "./FormBlock.svelte" diff --git a/packages/client/src/components/app/charts/ApexOptionsBuilder.js b/packages/client/src/components/app/charts/ApexOptionsBuilder.js index 31c5a820f7..6b3e3a4440 100644 --- a/packages/client/src/components/app/charts/ApexOptionsBuilder.js +++ b/packages/client/src/components/app/charts/ApexOptionsBuilder.js @@ -1,37 +1,39 @@ export class ApexOptionsBuilder { - formatters = { - ["Default"]: val => (isNaN(val) ? val : Math.round(val * 100) / 100), - ["Thousands"]: val => `${Math.round(val / 1000)}K`, - ["Millions"]: val => `${Math.round(val / 1000000)}M`, - } - options = { - series: [], - legend: { - show: false, - position: "top", - horizontalAlign: "right", - showForSingleSeries: true, - showForNullSeries: true, - showForZeroSeries: true, - }, - chart: { - toolbar: { + constructor() { + this.formatters = { + ["Default"]: val => (isNaN(val) ? val : Math.round(val * 100) / 100), + ["Thousands"]: val => `${Math.round(val / 1000)}K`, + ["Millions"]: val => `${Math.round(val / 1000000)}M`, + } + this.options = { + series: [], + legend: { show: false, + position: "top", + horizontalAlign: "right", + showForSingleSeries: true, + showForNullSeries: true, + showForZeroSeries: true, }, - zoom: { - enabled: false, + chart: { + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, }, - }, - xaxis: { - labels: { - formatter: this.formatters.Default, + xaxis: { + labels: { + formatter: this.formatters.Default, + }, }, - }, - yaxis: { - labels: { - formatter: this.formatters.Default, + yaxis: { + labels: { + formatter: this.formatters.Default, + }, }, - }, + } } setOption(path, value) { diff --git a/packages/client/src/components/app/forms/Form.svelte b/packages/client/src/components/app/forms/Form.svelte index 26922ff312..8eddc11fa5 100644 --- a/packages/client/src/components/app/forms/Form.svelte +++ b/packages/client/src/components/app/forms/Form.svelte @@ -48,36 +48,7 @@ // Fetches the form schema from this form's dataSource const fetchSchema = async dataSource => { - if (!dataSource) { - schema = {} - } - - // If the datasource is a query, then we instead use a schema of the query - // parameters rather than the output schema - else if ( - dataSource.type === "query" && - dataSource._id && - actionType === "Create" - ) { - try { - const query = await API.fetchQueryDefinition(dataSource._id) - let paramSchema = {} - const params = query.parameters || [] - params.forEach(param => { - paramSchema[param.name] = { ...param, type: "string" } - }) - schema = paramSchema - } catch (error) { - schema = {} - } - } - - // For all other cases, just grab the normal schema - else { - const dataSourceSchema = await fetchDatasourceSchema(dataSource) - schema = dataSourceSchema || {} - } - + schema = (await fetchDatasourceSchema(dataSource)) || {} if (!loaded) { loaded = true } @@ -95,7 +66,7 @@ $: initialValues = getInitialValues(actionType, dataSource, $context) $: resetKey = Helpers.hashString( - JSON.stringify(initialValues) + JSON.stringify(schema) + JSON.stringify(initialValues) + JSON.stringify(schema) + disabled ) diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 0e8ab8c258..b671d5554a 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -1,9 +1,10 @@ import ClientApp from "./components/ClientApp.svelte" import { - componentStore, builderStore, appStore, devToolsStore, + blockStore, + componentStore, environmentStore, } from "./stores" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js" @@ -50,6 +51,17 @@ const loadBudibase = async () => { const enableDevTools = !get(builderStore).inBuilder && get(appStore).isDevApp devToolsStore.actions.setEnabled(enableDevTools) + // Register handler for runtime events from the builder + window.handleBuilderRuntimeEvent = (name, payload) => { + if (!window["##BUDIBASE_IN_BUILDER##"]) { + return + } + if (name === "eject-block") { + const block = blockStore.actions.getBlock(payload) + block?.eject() + } + } + // Register any custom components if (window["##BUDIBASE_CUSTOM_COMPONENTS##"]) { window["##BUDIBASE_CUSTOM_COMPONENTS##"].forEach(component => { diff --git a/packages/client/src/stores/blocks.js b/packages/client/src/stores/blocks.js new file mode 100644 index 0000000000..98381ec79b --- /dev/null +++ b/packages/client/src/stores/blocks.js @@ -0,0 +1,34 @@ +import { get, writable } from "svelte/store" + +const createBlockStore = () => { + const store = writable({}) + + const registerBlock = (id, instance) => { + store.update(state => ({ + ...state, + [id]: instance, + })) + } + + const unregisterBlock = id => { + store.update(state => { + delete state[id] + return state + }) + } + + const getBlock = id => { + return get(store)[id] + } + + return { + subscribe: store.subscribe, + actions: { + registerBlock, + unregisterBlock, + getBlock, + }, + } +} + +export const blockStore = createBlockStore() diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index fea070c27c..5aaea2bdb0 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -85,6 +85,9 @@ const createBuilderStore = () => { highlightSetting: setting => { dispatchEvent("highlight-setting", { setting }) }, + ejectBlock: (id, definition) => { + dispatchEvent("eject-block", { id, definition }) + }, updateUsedPlugin: (name, hash) => { // Check if we used this plugin const used = get(store)?.usedPlugins?.find(x => x.name === name) diff --git a/packages/client/src/stores/index.js b/packages/client/src/stores/index.js index 378d3febd2..5b77762223 100644 --- a/packages/client/src/stores/index.js +++ b/packages/client/src/stores/index.js @@ -17,6 +17,7 @@ export { devToolsStore } from "./devTools" export { componentStore } from "./components" export { uploadStore } from "./uploads.js" export { rowSelectionStore } from "./rowSelection.js" +export { blockStore } from "./blocks.js" export { environmentStore } from "./environment" // Context stores are layered and duplicated, so it is not a singleton diff --git a/packages/client/src/utils/schema.js b/packages/client/src/utils/schema.js index 46c352a29f..433a1c5fee 100644 --- a/packages/client/src/utils/schema.js +++ b/packages/client/src/utils/schema.js @@ -16,7 +16,7 @@ import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js" */ export const fetchDatasourceSchema = async ( datasource, - options = { enrichRelationships: false } + options = { enrichRelationships: false, formSchema: false } ) => { const handler = { table: TableFetch, @@ -34,7 +34,17 @@ export const fetchDatasourceSchema = async ( // Get the datasource definition and then schema const definition = await instance.getDefinition(datasource) - let schema = instance.getSchema(datasource, definition) + + // Get the normal schema as long as we aren't wanting a form schema + let schema + if (datasource?.type !== "query" || !options?.formSchema) { + schema = instance.getSchema(datasource, definition) + } else if (definition.parameters?.length) { + schema = {} + definition.parameters.forEach(param => { + schema[param.name] = { ...param, type: "string" } + }) + } if (!schema) { return null } diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 0fbbb1c471..7d3355f34e 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.0.14-alpha.2", + "@budibase/bbui": "2.0.24-alpha.3", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/frontend-core/src/fetch/DataFetch.js b/packages/frontend-core/src/fetch/DataFetch.js index a3cc1c231c..31007121f1 100644 --- a/packages/frontend-core/src/fetch/DataFetch.js +++ b/packages/frontend-core/src/fetch/DataFetch.js @@ -14,52 +14,52 @@ import { convertJSONSchemaToTableSchema } from "../utils/json" * For other types of datasource, this class is overridden and extended. */ export default class DataFetch { - // API client - API = null - - // Feature flags - featureStore = writable({ - supportsSearch: false, - supportsSort: false, - supportsPagination: false, - }) - - // Config - options = { - datasource: null, - limit: 10, - - // Search config - filter: null, - query: null, - - // Sorting config - sortColumn: null, - sortOrder: "ascending", - sortType: null, - - // Pagination config - paginate: true, - } - - // State of the fetch - store = writable({ - rows: [], - info: null, - schema: null, - loading: false, - loaded: false, - query: null, - pageNumber: 0, - cursor: null, - cursors: [], - }) - /** * Constructs a new DataFetch instance. * @param opts the fetch options */ constructor(opts) { + // API client + this.API = null + + // Feature flags + this.featureStore = writable({ + supportsSearch: false, + supportsSort: false, + supportsPagination: false, + }) + + // Config + this.options = { + datasource: null, + limit: 10, + + // Search config + filter: null, + query: null, + + // Sorting config + sortColumn: null, + sortOrder: "ascending", + sortType: null, + + // Pagination config + paginate: true, + } + + // State of the fetch + this.store = writable({ + rows: [], + info: null, + schema: null, + loading: false, + loaded: false, + query: null, + pageNumber: 0, + cursor: null, + cursors: [], + }) + // Merge options with their default values this.API = opts?.API this.options = { diff --git a/packages/frontend-core/src/utils/lucene.js b/packages/frontend-core/src/utils/lucene.js index 1221e20664..774ddbd834 100644 --- a/packages/frontend-core/src/utils/lucene.js +++ b/packages/frontend-core/src/utils/lucene.js @@ -121,7 +121,12 @@ export const buildLuceneQuery = filter => { query.allOr = true return } - if (type === "datetime" && !isHbs) { + if ( + type === "datetime" && + !isHbs && + operator !== "empty" && + operator !== "notEmpty" + ) { // Ensure date value is a valid date and parse into correct format if (!value) { return diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 4a3e0080f5..53e89077a7 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 759cc502b0..2f2409850e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.0.14-alpha.2", + "version": "2.0.24-alpha.3", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -77,11 +77,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.0.14-alpha.2", - "@budibase/client": "2.0.14-alpha.2", - "@budibase/pro": "2.0.14-alpha.2", - "@budibase/string-templates": "2.0.14-alpha.2", - "@budibase/types": "2.0.14-alpha.2", + "@budibase/backend-core": "2.0.24-alpha.3", + "@budibase/client": "2.0.24-alpha.3", + "@budibase/pro": "2.0.24-alpha.3", + "@budibase/string-templates": "2.0.24-alpha.3", + "@budibase/types": "2.0.24-alpha.3", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index d7e2a8f0bd..a7caf85e94 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -32,7 +32,7 @@ const { import { USERS_TABLE_SCHEMA } from "../../constants" import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { clientLibraryPath, stringToReadStream } from "../../utilities" -import { getAllLocks } from "../../utilities/redis" +import { getLocksById } from "../../utilities/redis" import { updateClientLibrary, backupClientLibrary, @@ -45,11 +45,11 @@ import { cleanupAutomations } from "../../automations/utils" import { context } from "@budibase/backend-core" import { checkAppMetadata } from "../../automations/logging" import { getUniqueRows } from "../../utilities/usageQuota/rows" -import { quotas } from "@budibase/pro" +import { quotas, groups } from "@budibase/pro" import { errors, events, migrations } from "@budibase/backend-core" import { App, Layout, Screen, MigrationType } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" -import { groups } from "@budibase/pro" +import { enrichPluginURLs } from "../../utilities/plugins" const URL_REGEX_SLASH = /\/|\\/g @@ -171,16 +171,16 @@ export const fetch = async (ctx: any) => { const all = ctx.query && ctx.query.status === AppStatus.ALL const apps = await getAllApps({ dev, all }) + const appIds = apps + .filter((app: any) => app.status === "development") + .map((app: any) => app.appId) // get the locks for all the dev apps if (dev || all) { - const locks = await getAllLocks() + const locks = await getLocksById(appIds) for (let app of apps) { - if (app.status !== "development") { - continue - } - const lock = locks.find((lock: any) => lock.appId === app.appId) + const lock = locks[app.appId] if (lock) { - app.lockedBy = lock.user + app.lockedBy = lock } else { // make sure its definitely not present delete app.lockedBy @@ -208,10 +208,13 @@ export const fetchAppDefinition = async (ctx: any) => { export const fetchAppPackage = async (ctx: any) => { const db = context.getAppDB() - const application = await db.get(DocumentType.APP_METADATA) + let application = await db.get(DocumentType.APP_METADATA) const layouts = await getLayouts() let screens = await getScreens() + // Enrich plugin URLs + application.usedPlugins = enrichPluginURLs(application.usedPlugins) + // Only filter screens if the user is not a builder if (!(ctx.user.builder && ctx.user.builder.global)) { const userRoleId = getUserRoleId(ctx) diff --git a/packages/server/src/api/controllers/datasource.js b/packages/server/src/api/controllers/datasource.js index 4fafaa546c..af52be8e26 100644 --- a/packages/server/src/api/controllers/datasource.js +++ b/packages/server/src/api/controllers/datasource.js @@ -68,6 +68,7 @@ exports.buildSchemaFromDb = async function (ctx) { datasource.entities = tables } + setDefaultDisplayColumns(datasource) const dbResp = await db.put(datasource) datasource._rev = dbResp.rev @@ -78,6 +79,24 @@ exports.buildSchemaFromDb = async function (ctx) { ctx.body = response } +/** + * Make sure all datasource entities have a display name selected + */ +const setDefaultDisplayColumns = datasource => { + // + for (let entity of Object.values(datasource.entities)) { + if (entity.primaryDisplay) { + continue + } + const notAutoColumn = Object.values(entity.schema).find( + schema => !schema.autocolumn + ) + if (notAutoColumn) { + entity.primaryDisplay = notAutoColumn.name + } + } +} + /** * Check for variables that have been updated or removed and invalidate them. */ @@ -155,6 +174,7 @@ exports.save = async function (ctx) { const { tables, error } = await buildSchemaHelper(datasource) schemaError = error datasource.entities = tables + setDefaultDisplayColumns(datasource) } const dbResp = await db.put(datasource) @@ -238,19 +258,6 @@ const buildSchemaHelper = async datasource => { const connector = new Connector(datasource.config) await connector.buildSchema(datasource._id, datasource.entities) - // make sure they all have a display name selected - for (let entity of Object.values(datasource.entities ?? {})) { - if (entity.primaryDisplay) { - continue - } - const notAutoColumn = Object.values(entity.schema).find( - schema => !schema.autocolumn - ) - if (notAutoColumn) { - entity.primaryDisplay = notAutoColumn.name - } - } - const errors = connector.schemaErrors let error = null if (errors && Object.keys(errors).length > 0) { diff --git a/packages/server/src/api/controllers/dev.js b/packages/server/src/api/controllers/dev.js index 8438175ca8..e5582be5b3 100644 --- a/packages/server/src/api/controllers/dev.js +++ b/packages/server/src/api/controllers/dev.js @@ -103,7 +103,7 @@ exports.revert = async ctx => { target: appId, }) try { - if (!env.isTest()) { + if (!env.isCypress()) { // in-memory db stalls on rollback await replication.rollback() } diff --git a/packages/server/src/api/controllers/public/rows.ts b/packages/server/src/api/controllers/public/rows.ts index 4daccd9542..67059ec2f5 100644 --- a/packages/server/src/api/controllers/public/rows.ts +++ b/packages/server/src/api/controllers/public/rows.ts @@ -52,14 +52,19 @@ export async function read(ctx: any, next: any) { } export async function update(ctx: any, next: any) { - ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params)) + const { tableId } = ctx.params + ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params), tableId) await rowController.save(ctx) await next() } export async function destroy(ctx: any, next: any) { + const { tableId } = ctx.params // set the body as expected, with the _id and _rev fields - ctx.request.body = await addRev(fixRow({ _id: ctx.params.rowId }, ctx.params)) + ctx.request.body = await addRev( + fixRow({ _id: ctx.params.rowId }, ctx.params), + tableId + ) await rowController.destroy(ctx) // destroy controller doesn't currently return the row as the body, need to adjust this // in the public API to be correct diff --git a/packages/server/src/api/controllers/public/utils.ts b/packages/server/src/api/controllers/public/utils.ts index d86eced9ba..6909db9628 100644 --- a/packages/server/src/api/controllers/public/utils.ts +++ b/packages/server/src/api/controllers/public/utils.ts @@ -22,7 +22,7 @@ export async function addRev( } /** - * Performs a case insensitive search on the provided documents, using the + * Performs a case in-sensitive search on the provided documents, using the * provided key and value. This will be a string based search, using the * startsWith function. */ diff --git a/packages/server/src/api/controllers/query/index.ts b/packages/server/src/api/controllers/query/index.ts index f69653b720..5c09a2f3b6 100644 --- a/packages/server/src/api/controllers/query/index.ts +++ b/packages/server/src/api/controllers/query/index.ts @@ -240,6 +240,10 @@ async function execute( const { rows, pagination, extra } = await quotas.addQuery(runFn, { datasourceId: datasource._id, }) + // remove the raw from execution incase transformer being used to hide data + if (extra?.raw) { + delete extra.raw + } if (opts && opts.rowsOnly) { ctx.body = rows } else { diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js index 3cf60fbcc0..051a55aa9f 100644 --- a/packages/server/src/api/controllers/row/internalSearch.js +++ b/packages/server/src/api/controllers/row/internalSearch.js @@ -145,7 +145,7 @@ class QueryBuilder { * @param options The preprocess options * @returns {string|*} */ - preprocess(value, { escape, lowercase, wrap } = {}) { + preprocess(value, { escape, lowercase, wrap, type } = {}) { const hasVersion = !!this.version // Determine if type needs wrapped const originalType = typeof value @@ -157,8 +157,11 @@ class QueryBuilder { if (escape && originalType === "string") { value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") } + // Wrap in quotes - if (hasVersion && wrap) { + if (originalType === "string" && !isNaN(value) && !type) { + value = `"${value}"` + } else if (hasVersion && wrap) { value = originalType === "number" ? value : `"${value}"` } return value @@ -253,6 +256,7 @@ class QueryBuilder { value = builder.preprocess(value, { escape: true, lowercase: true, + type: "string", }) return `${key}:${value}*` }) @@ -281,6 +285,7 @@ class QueryBuilder { value = builder.preprocess(value, { escape: true, lowercase: true, + type: "fuzzy", }) return `${key}:${value}~` }) diff --git a/packages/server/src/api/controllers/static/index.ts b/packages/server/src/api/controllers/static/index.ts index 80116a21f5..08213c2cf8 100644 --- a/packages/server/src/api/controllers/static/index.ts +++ b/packages/server/src/api/controllers/static/index.ts @@ -1,3 +1,5 @@ +import { enrichPluginURLs } from "../../../utilities/plugins" + require("svelte/register") const send = require("koa-send") @@ -107,12 +109,13 @@ export const serveApp = async function (ctx: any) { if (!env.isJest()) { const App = require("./templates/BudibaseApp.svelte").default + const plugins = enrichPluginURLs(appInfo.usedPlugins) const { head, html, css } = App.render({ title: appInfo.name, production: env.isProd(), appId, clientLibPath: clientLibraryPath(appId, appInfo.version, ctx), - usedPlugins: appInfo.usedPlugins, + usedPlugins: plugins, }) const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`) diff --git a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte index 4bf54f2c91..227f980896 100644 --- a/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte +++ b/packages/server/src/api/controllers/static/templates/BudibaseApp.svelte @@ -88,9 +88,7 @@ {#if usedPlugins?.length} {#each usedPlugins as plugin} - + {/each} {/if}