diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 4d338cb221..bab7f61df1 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -1,8 +1,11 @@ version: "3" +# optional ports are specified throughout for more advanced use cases. + services: app-service: restart: always + #build: ./build/server image: budibase/budibase-apps ports: - "${APP_PORT}:4002" @@ -20,6 +23,7 @@ services: worker-service: restart: always + #build: ./build/worker image: budibase/budibase-worker ports: - "${WORKER_PORT}:4003" @@ -62,7 +66,7 @@ services: - ./envoy.yaml:/etc/envoy/envoy.yaml ports: - "${MAIN_PORT}:10000" - - "9901:9901" + #- "9901:9901" depends_on: - minio-service - worker-service @@ -77,8 +81,8 @@ services: - COUCHDB_USER=${COUCH_DB_USER} ports: - "${COUCH_DB_PORT}:5984" - - "4369:4369" - - "9100:9100" + #- "4369:4369" + #- "9100:9100" volumes: - couchdb_data:/couchdb diff --git a/hosting/envoy.yaml b/hosting/envoy.yaml index 3c816cb1ca..11f5c81b99 100644 --- a/hosting/envoy.yaml +++ b/hosting/envoy.yaml @@ -20,6 +20,11 @@ static_resources: route: cluster: app-service prefix_rewrite: "/" + + # special case for presenting our static self hosting page + - match: { path: "/" } + route: + cluster: app-service # special case for when API requests are made, can just forward, not to minio - match: { prefix: "/api/" } diff --git a/lerna.json b/lerna.json index 640c262a36..b53f4142a5 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.6.2", + "version": "0.7.4", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/builder/package.json b/packages/builder/package.json index f8a2f6b60d..0dbd0ad240 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.6.2", + "version": "0.7.4", "license": "AGPL-3.0", "private": true, "scripts": { @@ -63,27 +63,26 @@ } }, "dependencies": { - "@budibase/bbui": "^1.55.2", - "@budibase/client": "^0.6.2", - "@budibase/colorpicker": "^1.0.1", - "@budibase/string-templates": "^0.6.2", + "@budibase/bbui": "^1.57.0", + "@budibase/client": "^0.7.4", + "@budibase/colorpicker": "1.0.1", + "@budibase/string-templates": "^0.7.4", "@budibase/svelte-ag-grid": "^0.0.16", "@sentry/browser": "5.19.1", - "@svelteschool/svelte-forms": "^0.7.0", - "britecharts": "^2.16.0", + "@svelteschool/svelte-forms": "0.7.0", "codemirror": "^5.59.0", "d3-selection": "^1.4.1", "deepmerge": "^4.2.2", - "downloadjs": "^1.4.7", + "downloadjs": "1.4.7", "fast-sort": "^2.2.0", - "lodash": "^4.17.13", + "lodash": "4.17.13", "posthog-js": "1.4.5", - "remixicon": "^2.5.0", - "shortid": "^2.2.15", + "remixicon": "2.5.0", + "shortid": "2.2.15", "svelte-loading-spinners": "^0.1.1", - "svelte-portal": "^0.1.0", - "uuid": "^8.3.1", - "yup": "^0.29.2" + "svelte-portal": "0.1.0", + "uuid": "8.3.1", + "yup": "0.29.2" }, "devDependencies": { "@babel/core": "^7.5.5", diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 679a77ca2b..79785f4e8d 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -2,6 +2,7 @@ import { cloneDeep } from "lodash/fp" import { get } from "svelte/store" import { backendUiStore, store } from "builderStore" import { findAllMatchingComponents, findComponentPath } from "./storeUtils" +import { makePropSafe } from "@budibase/string-templates" import { TableNames } from "../constants" // Regex to match all instances of template strings @@ -106,7 +107,9 @@ export const getContextBindings = (rootComponent, componentId) => { contextBindings.push({ type: "context", - runtimeBinding: `${component._id}.${runtimeBoundKey}`, + runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe( + runtimeBoundKey + )}`, readableBinding: `${component._instanceName}.${table.name}.${key}`, fieldSchema, providerId: component._id, @@ -167,7 +170,7 @@ export const getComponentBindings = rootComponent => { return { type: "instance", providerId: component._id, - runtimeBinding: `${component._id}`, + runtimeBinding: `${makePropSafe(component._id)}`, readableBinding: `${component._instanceName}`, } }) @@ -199,43 +202,52 @@ export const getSchemaForDatasource = datasource => { } /** - * Converts a readable data binding into a runtime data binding + * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ -export function readableToRuntimeBinding(bindableProperties, textWithBindings) { +function bindingReplacement(bindableProperties, textWithBindings, convertTo) { + const convertFrom = + convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding" if (typeof textWithBindings !== "string") { return textWithBindings } + const convertFromProps = bindableProperties + .map(el => el[convertFrom]) + .sort((a, b) => { + return b.length - a.length + }) const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || [] let result = textWithBindings - boundValues.forEach(boundValue => { - const binding = bindableProperties.find(({ readableBinding }) => { - return boundValue === `{{ ${readableBinding} }}` - }) - if (binding) { - result = result.replace(boundValue, `{{ ${binding.runtimeBinding} }}`) + for (let boundValue of boundValues) { + let newBoundValue = boundValue + for (let from of convertFromProps) { + if (newBoundValue.includes(from)) { + const binding = bindableProperties.find(el => el[convertFrom] === from) + newBoundValue = newBoundValue.replace(from, binding[convertTo]) + } } - }) + result = result.replace(boundValue, newBoundValue) + } return result } +/** + * Converts a readable data binding into a runtime data binding + */ +export function readableToRuntimeBinding(bindableProperties, textWithBindings) { + return bindingReplacement( + bindableProperties, + textWithBindings, + "runtimeBinding" + ) +} + /** * Converts a runtime data binding into a readable data binding */ export function runtimeToReadableBinding(bindableProperties, textWithBindings) { - if (typeof textWithBindings !== "string") { - return textWithBindings - } - const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || [] - let result = textWithBindings - boundValues.forEach(boundValue => { - const binding = bindableProperties.find(({ runtimeBinding }) => { - return boundValue === `{{ ${runtimeBinding} }}` - }) - // Show invalid bindings as invalid rather than a long ID - result = result.replace( - boundValue, - `{{ ${binding?.readableBinding ?? "Invalid binding"} }}` - ) - }) - return result + return bindingReplacement( + bindableProperties, + textWithBindings, + "readableBinding" + ) } diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index 0a235dd9a0..faf5b460f1 100644 --- a/packages/builder/src/builderStore/store/backend.js +++ b/packages/builder/src/builderStore/store/backend.js @@ -119,6 +119,18 @@ export const getBackendUiStore = () => { return json }, save: async (datasourceId, query) => { + const integrations = get(store).integrations + const dataSource = get(store).datasources.filter( + ds => ds._id === datasourceId + ) + // check if readable attribute is found + if (dataSource.length !== 0) { + const integration = integrations[dataSource[0].source] + const readable = integration.query[query.queryVerb].readable + if (readable) { + query.readable = readable + } + } query.datasourceId = datasourceId const response = await api.post(`/api/queries`, query) const json = await response.json() diff --git a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js index 0a9148eaf8..fd54405875 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js +++ b/packages/builder/src/builderStore/store/screenTemplates/rowDetailScreen.js @@ -2,6 +2,7 @@ import sanitizeUrl from "./utils/sanitizeUrl" import { rowListUrl } from "./rowListScreen" import { Screen } from "./utils/Screen" import { Component } from "./utils/Component" +import { makePropSafe } from "@budibase/string-templates" import { makeMainContainer, makeBreadcrumbContainer, @@ -12,7 +13,7 @@ import { export default function(tables) { return tables.map(table => { const heading = table.primaryDisplay - ? `{{ data.${table.primaryDisplay} }}` + ? `{{ data.${makePropSafe(table.primaryDisplay)} }}` : null return { name: `${table.name} - Detail`, @@ -60,8 +61,8 @@ function generateTitleContainer(table, title, providerId) { onClick: [ { parameters: { - rowId: `{{ ${providerId}._id }}`, - revId: `{{ ${providerId}._rev }}`, + rowId: `{{ ${makePropSafe(providerId)}._id }}`, + revId: `{{ ${makePropSafe(providerId)}._rev }}`, tableId: table._id, }, "##eventHandlerType": "Delete Row", diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 439687d0c2..ed89d4316d 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -64,7 +64,11 @@ {:else if value.customType === 'password'} {:else if value.customType === 'email'} - + {:else if value.customType === 'table'} {:else if value.customType === 'row'} @@ -75,7 +79,7 @@ {:else if value.type === 'string' || value.type === 'number'} diff --git a/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte b/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte index a326f60a08..3956e765ee 100644 --- a/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte +++ b/packages/builder/src/components/automation/SetupPanel/GenericBindingPopover.svelte @@ -3,6 +3,7 @@ import { TextArea, Label, + Input, Heading, Body, Spacer, @@ -10,6 +11,9 @@ Popover, } from "@budibase/bbui" import { createEventDispatcher } from "svelte" + import { isValid } from "@budibase/string-templates" + import { handlebarsCompletions } from "constants/completions" + const dispatch = createEventDispatcher() export let value = "" @@ -18,9 +22,14 @@ export let align export let popover = null + let helpers = handlebarsCompletions() let getCaretPosition + let validity = true + let search = "" $: categories = Object.entries(groupBy("category", bindings)) + $: value && checkValid() + $: searchRgx = new RegExp(search, "ig") function onClickBinding(binding) { const position = getCaretPosition() @@ -34,18 +43,27 @@ value += toAdd } } + + function checkValid() { + validity = isValid(value) + } Available bindings + + + {#each categories as [categoryName, bindings]} {categoryName} - {#each bindings as binding} + {#each bindings.filter(binding => + binding.label.match(searchRgx) + ) as binding} onClickBinding(binding)}> {binding.label} {binding.type} @@ -56,6 +74,18 @@ {/each} {/each} + Helpers + + {#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper} + onClickBinding(helper)}> + {helper.label} + + + {@html helper.description || ''} + + {helper.example || ''} + + {/each} @@ -70,11 +100,20 @@ bind:getCaretPosition bind:value placeholder="Add options from the left, type text, or do both" /> + {#if !validity} + + Current Handlebars syntax is invalid, please check the guide + here + for more details. + + {/if} Learn more about binding - Done + + Done + @@ -128,7 +167,13 @@ .binding__description { color: var(--grey-8); margin-top: 2px; + white-space: normal; } + + pre { + white-space: normal; + } + .binding__type { font-family: monospace; background-color: var(--grey-2); @@ -152,4 +197,14 @@ align-items: center; margin-top: var(--spacing-m); } + + .syntax-error { + color: var(--red); + font-size: 12px; + } + + .syntax-error a { + color: var(--red); + text-decoration: underline; + } diff --git a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte index 981c1297e6..d8f094b3cb 100644 --- a/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/EditRoles.svelte @@ -9,7 +9,7 @@ let permissions = [] let selectedRole = {} let errors = [] - let builtInRoles = ['Admin', 'Power', 'Basic', 'Public'] + let builtInRoles = ["Admin", "Power", "Basic", "Public"] $: selectedRoleId = selectedRole._id $: otherRoles = $backendUiStore.roles.filter( role => role._id !== selectedRoleId @@ -103,7 +103,11 @@ {/each} {#if selectedRole} - + import api from "builderStore/api" import { Button, Select } from "@budibase/bbui" + import download from "downloadjs" const FORMATS = [ { @@ -19,13 +20,12 @@ let exportFormat = FORMATS[0].key async function exportView() { - const response = await api.post( - `/api/views/export?format=${exportFormat}`, - view + download( + `/api/views/export?view=${encodeURIComponent( + view.name + )}&format=${exportFormat}` ) - const downloadInfo = await response.json() onClosed() - window.location = downloadInfo.url } diff --git a/packages/builder/src/components/common/BindableInput.svelte b/packages/builder/src/components/common/BindableInput.svelte index 0e2cc2174f..7b8831902c 100644 --- a/packages/builder/src/components/common/BindableInput.svelte +++ b/packages/builder/src/components/common/BindableInput.svelte @@ -41,7 +41,7 @@ .icon { right: 2px; - top: 2px; + top: 26px; bottom: 2px; position: absolute; align-items: center; diff --git a/packages/builder/src/components/design/PropertiesPanel/BindingPanel.svelte b/packages/builder/src/components/design/PropertiesPanel/BindingPanel.svelte index 8bfae51337..76dbdf229c 100644 --- a/packages/builder/src/components/design/PropertiesPanel/BindingPanel.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/BindingPanel.svelte @@ -1,59 +1,128 @@ + + {#if context} Columns - {#each context as { readableBinding }} + {#each context.filter(context => + context.readableBinding.match(searchRgx) + ) as { readableBinding }} addToText(readableBinding)}> {readableBinding} {/each} {/if} + {#if instance} Components + - {#each instance as { readableBinding }} + {#each instance.filter(instance => + instance.readableBinding.match(searchRgx) + ) as { readableBinding }} addToText(readableBinding)}> {readableBinding} {/each} {/if} + + Helpers + + + {#each helpers.filter(helper => helper.label.match(searchRgx) || helper.description.match(searchRgx)) as helper} + addToText(helper.text)}> + + {helper.displayText} + + {@html helper.description} + + {helper.example || ''} + + + {/each} + + {#if !valid} + + Current Handlebars syntax is invalid, please check the guide + here + for more details. + + {/if} @@ -101,6 +170,11 @@ border-width: 1px 0 1px 0; } + pre, + .description { + white-space: normal; + } + li:hover { color: var(--ink); font-weight: 500; @@ -114,4 +188,27 @@ height: 40vh; overflow-y: auto; } + + .syntax-error { + padding-top: var(--spacing-m); + color: var(--red); + font-size: 12px; + } + + .syntax-error a { + color: var(--red); + text-decoration: underline; + } + + .description :global(p) { + color: var(--grey-7); + } + + .description :global(p:hover) { + color: var(--ink); + } + + .description :global(p a) { + color: var(--grey-7); + } diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte index 9dc7bc2f0a..525a441122 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte @@ -21,6 +21,7 @@ let bindingDrawer let temporaryBindableValue = value let anchor + let valid $: bindableProperties = getBindableProperties( $currentAsset.props, @@ -79,7 +80,7 @@ class="icon" data-cy={`${key}-binding-button`} on:click={bindingDrawer.show}> - + {/if} @@ -90,10 +91,11 @@
{helper.example || ''}
+ Current Handlebars syntax is invalid, please check the guide + here + for more details. +