Merge branch 'develop' of github.com:Budibase/budibase into feature/query-variables

This commit is contained in:
mike12345567 2021-12-15 16:35:30 +00:00
commit 85858ff6b1
44 changed files with 706 additions and 266 deletions

View File

@ -12,6 +12,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-node@v1
with:
node-version: 14.x
- run: yarn
- run: yarn bootstrap
- name: 'Get Previous tag' - name: 'Get Previous tag'
id: previoustag id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1" uses: "WyriHaximus/github-action-get-previous-tag@v1"

View File

@ -11,8 +11,8 @@ sources:
- https://github.com/Budibase/budibase - https://github.com/Budibase/budibase
- https://budibase.com - https://budibase.com
type: application type: application
version: 0.2.6 version: 1.0.0
appVersion: 1.0.10 appVersion: 1.0.20
dependencies: dependencies:
- name: couchdb - name: couchdb
version: 3.3.4 version: 3.3.4

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.19-alpha.2", "version": "1.0.22",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"description": "Authentication middlewares for budibase builder and apps", "description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -7,7 +7,7 @@
export let disabled = false export let disabled = false
export let id = null export let id = null
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focus = false let focus = false
@ -41,6 +41,7 @@
<div class="spectrum-Search" class:is-disabled={disabled}> <div class="spectrum-Search" class:is-disabled={disabled}>
<div <div
class="spectrum-Textfield" class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet}
class:is-focused={focus} class:is-focused={focus}
class:is-disabled={disabled} class:is-disabled={disabled}
> >

View File

@ -9,6 +9,7 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,6 +24,7 @@
{disabled} {disabled}
{value} {value}
{placeholder} {placeholder}
{quiet}
on:change={onChange} on:change={onChange}
on:click on:click
on:input on:input

View File

@ -10,7 +10,7 @@ it("should rename an unpublished application", () => {
cy.get(".home-logo").click() cy.get(".home-logo").click()
renameApp(appRename) renameApp(appRename)
cy.searchForApplication(appRename) cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
cy.deleteApp(appRename) cy.deleteApp(appRename)
}) })
@ -29,7 +29,7 @@ xit("Should rename a published application", () => {
cy.get(".home-logo").click() cy.get(".home-logo").click()
renameApp(appRename, true) renameApp(appRename, true)
cy.searchForApplication(appRename) cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
}) })
it("Should try to rename an application to have no name", () => { it("Should try to rename an application to have no name", () => {
@ -38,7 +38,7 @@ it("Should try to rename an application to have no name", () => {
// Close modal and confirm name has not been changed // Close modal and confirm name has not been changed
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
cy.searchForApplication("Cypress Tests") cy.searchForApplication("Cypress Tests")
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
}) })
xit("Should create two applications with the same name", () => { xit("Should create two applications with the same name", () => {
@ -64,7 +64,7 @@ it("should validate application names", () => {
cy.get(".home-logo").click() cy.get(".home-logo").click()
renameApp(numberName) renameApp(numberName)
cy.searchForApplication(numberName) cy.searchForApplication(numberName)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appTable").find(".title").should("have.length", 1)
renameApp(specialCharName) renameApp(specialCharName)
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
}) })
@ -74,14 +74,14 @@ it("should validate application names", () => {
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
// Check for when an app is published // Check for when an app is published
if (published == true){ if (published == true){
// Should not have Edit as option, will unpublish app // Should not have Edit as option, will unpublish app
cy.should("not.have.value", "Edit") cy.should("not.have.value", "Edit")
cy.get(".spectrum-Menu").contains("Unpublish").click() cy.get(".spectrum-Menu").contains("Unpublish").click()
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
} }
cy.contains("Edit").click() cy.contains("Edit").click()
cy.get(".spectrum-Modal") cy.get(".spectrum-Modal")

View File

@ -50,7 +50,9 @@ Cypress.Commands.add("deleteApp", appName => {
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length > 0) { if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(
".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon"
).click()
cy.contains("Delete").click() cy.contains("Delete").click()
cy.get(".spectrum-Modal").within(() => { cy.get(".spectrum-Modal").within(() => {
cy.get("input").type(appName) cy.get("input").type(appName)

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.19-alpha.2", "@budibase/bbui": "^1.0.22",
"@budibase/client": "^1.0.19-alpha.2", "@budibase/client": "^1.0.22",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^1.0.19-alpha.2", "@budibase/string-templates": "^1.0.22",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -8,6 +8,9 @@
name: "javascript", name: "javascript",
json: true, json: true,
}, },
XML: {
name: "xml",
},
SQL: { SQL: {
name: "sql", name: "sql",
}, },
@ -40,11 +43,12 @@
let editor let editor
// Keep editor up to date with value // Keep editor up to date with value
$: editor?.setOption("mode", mode)
$: editor?.setValue(value || "") $: editor?.setValue(value || "")
// Creates an instance of a code mirror editor // Creates an instance of a code mirror editor
async function createEditor(mode, value) { async function createEditor(mode, value) {
if (!CodeMirror || !textarea || editor) { if (!CodeMirror || !textarea) {
return return
} }

View File

@ -4,6 +4,7 @@ import "codemirror/lib/codemirror.css"
// Modes // Modes
import "codemirror/mode/javascript/javascript" import "codemirror/mode/javascript/javascript"
import "codemirror/mode/sql/sql" import "codemirror/mode/sql/sql"
import "codemirror/mode/xml/xml"
import "codemirror/mode/css/css" import "codemirror/mode/css/css"
import "codemirror/mode/handlebars/handlebars" import "codemirror/mode/handlebars/handlebars"

View File

@ -1,5 +1,4 @@
<script> <script>
import { gradient } from "actions"
import { import {
Heading, Heading,
Button, Button,
@ -18,15 +17,20 @@
export let deleteApp export let deleteApp
export let unpublishApp export let unpublishApp
export let releaseLock export let releaseLock
export let editIcon
</script> </script>
<div class="title"> <div class="title">
<div class="preview" use:gradient={{ seed: app.name }} /> <div style="display: flex;">
<div style="color: {app.icon?.color || ''}">
<Icon size="XL" name={app.icon?.name || "Apps"} />
</div>
<div class="name" on:click={() => editApp(app)}> <div class="name" on:click={() => editApp(app)}>
<Heading size="XS"> <Heading size="XS">
{app.name} {app.name}
</Heading> </Heading>
</div> </div>
</div>
</div> </div>
<div class="desktop"> <div class="desktop">
{#if app.updatedAt} {#if app.updatedAt}
@ -62,6 +66,7 @@
disabled={app.lockedOther} disabled={app.lockedOther}
on:click={() => editApp(app)} on:click={() => editApp(app)}
size="S" size="S"
quiet
secondary>Open</Button secondary>Open</Button
> >
<ActionMenu align="right"> <ActionMenu align="right">
@ -86,15 +91,11 @@
<MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem> <MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem> <MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
{/if} {/if}
<MenuItem on:click={() => editIcon(app)} icon="Brush">Edit Icon</MenuItem>
</ActionMenu> </ActionMenu>
</div> </div>
<style> <style>
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.name { .name {
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
@ -103,6 +104,7 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-left: calc(1.5 * var(--spacing-xl));
} }
.title :global(h1:hover) { .title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600); color: var(--spectrum-global-color-blue-600);

View File

@ -0,0 +1,127 @@
<script>
import { ModalContent, Modal, Icon, ColorPicker, Label } from "@budibase/bbui"
import { apps } from "stores/portal"
export let app
let modal
$: selectedIcon = app?.icon?.name
$: selectedColor = app?.icon?.color
let iconsList = [
"Actions",
"ConversionFunnel",
"App",
"Briefcase",
"Money",
"ShoppingCart",
"Form",
"Help",
"Monitoring",
"Sandbox",
"Project",
"Organisations",
"Magnify",
"Launch",
"Car",
"Camera",
"Bug",
"Channel",
"Calculator",
"Calendar",
"GraphDonut",
"GraphBarHorizontal",
"Demographic",
"Apps",
]
export const show = () => {
modal.show()
}
export const hide = () => {
modal.hide()
}
const onCancel = () => {
selectedIcon = ""
selectedColor = ""
hide()
}
const changeColor = val => {
selectedColor = val
}
const save = async () => {
await apps.update(app.instance._id, {
icon: {
name: selectedIcon,
color: selectedColor,
},
})
}
</script>
<Modal bind:this={modal} on:hide={onCancel}>
<ModalContent
title={"Edit Icon"}
confirmText={"Save"}
onConfirm={() => save()}
>
<div class="scrollable-icons">
<div class="title-spacing">
<Label>Select an Icon</Label>
</div>
<div class="grid">
{#each iconsList as item}
<div
class="icon-item"
style="color: {item === selectedIcon ? selectedColor : ''}"
on:click={() => (selectedIcon = item)}
>
<Icon name={item} />
</div>
{/each}
</div>
</div>
<div class="color-selection">
<div>
<Label>Select a Color</Label>
</div>
<div class="color-selection-item">
<ColorPicker
bind:value={selectedColor}
on:change={e => changeColor(e.detail)}
/>
</div>
</div>
</ModalContent>
</Modal>
<style>
.scrollable-icons {
overflow-y: auto;
height: 230px;
}
.grid {
display: grid;
grid-gap: 20px;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
.color-selection {
display: flex;
align-items: center;
}
.color-selection-item {
margin-left: 20px;
}
.title-spacing {
margin-bottom: 20px;
}
.icon-item {
cursor: pointer;
}
</style>

View File

@ -1,13 +1,7 @@
<script> <script>
import { writable, get as svelteGet } from "svelte/store" import { writable, get as svelteGet } from "svelte/store"
import {
notifications, import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
Input,
ModalContent,
Dropzone,
Body,
Checkbox,
} from "@budibase/bbui"
import { store, automationStore, hostingStore } from "builderStore" import { store, automationStore, hostingStore } from "builderStore"
import { admin, auth } from "stores/portal" import { admin, auth } from "stores/portal"
import { string, mixed, object } from "yup" import { string, mixed, object } from "yup"
@ -147,16 +141,6 @@
} }
} }
function getModalTitle() {
let title = "Create App"
if (template.fromFile) {
title = "Import App"
} else if (template.key) {
title = "Create app from template"
}
return title
}
async function onCancel() { async function onCancel() {
template = null template = null
await auth.setInitInfo({}) await auth.setInitInfo({})
@ -187,7 +171,7 @@
</ModalContent> </ModalContent>
{:else} {:else}
<ModalContent <ModalContent
title={getModalTitle()} title={"Name your app"}
confirmText={template?.fromFile ? "Import app" : "Create app"} confirmText={template?.fromFile ? "Import app" : "Create app"}
onConfirm={createNewApp} onConfirm={createNewApp}
onCancel={inline ? onCancel : null} onCancel={inline ? onCancel : null}
@ -207,16 +191,14 @@
}} }}
/> />
{/if} {/if}
<Body size="S">
Give your new app a name, and choose which groups have access (paid plans
only).
</Body>
<Input <Input
bind:value={$values.name} bind:value={$values.name}
error={$touched.name && $errors.name} error={$touched.name && $errors.name}
on:blur={() => ($touched.name = true)} on:blur={() => ($touched.name = true)}
label="Name" label="Name"
placeholder={$auth.user.firstName
? `${$auth.user.firstName}'s app`
: "My app"}
/> />
<Checkbox label="Group access" disabled value={true} text="All users" />
</ModalContent> </ModalContent>
{/if} {/if}

View File

@ -1,46 +1,10 @@
<script> <script>
import { Heading, Layout, Icon, Body } from "@budibase/bbui" import { Heading, Layout, Icon } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import api from "builderStore/api"
export let onSelect export let onSelect
async function fetchTemplates() {
const response = await api.get("/api/templates?type=app")
return await response.json()
}
let templatesPromise = fetchTemplates()
</script> </script>
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
{#await templatesPromise}
<div class="spinner-container">
<Spinner size="30" />
</div>
{:then templates}
{#if templates?.length > 0}
<Body size="M">Select a template below, or start from scratch.</Body>
{:else}
<Body size="M">Start your app from scratch below.</Body>
{/if}
<div class="templates">
{#each templates as template}
<div class="template" on:click={() => onSelect(template)}>
<div
class="background-icon"
style={`background: ${template.background};`}
>
<Icon name={template.icon} />
</div>
<Heading size="XS">{template.name}</Heading>
<p class="detail">{template?.category?.toUpperCase()}</p>
</div>
{/each}
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<div class="template start-from-scratch" on:click={() => onSelect(null)}> <div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div <div
class="background-icon" class="background-icon"
@ -67,15 +31,6 @@
</Layout> </Layout>
<style> <style>
.templates {
display: grid;
width: 100%;
grid-gap: var(--spacing-m);
grid-template-columns: 1fr;
justify-content: start;
margin-top: 15px;
}
.background-icon { .background-icon {
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;

View File

@ -77,7 +77,7 @@
async function updateApp() { async function updateApp() {
try { try {
// Update App // Update App
await apps.update(app.instance._id, $values.name.trim()) await apps.update(app.instance._id, { name: $values.name.trim() })
hide() hide()
} catch (error) { } catch (error) {
console.error(error) console.error(error)

View File

@ -200,6 +200,7 @@ export const RawRestBodyTypes = {
ENCODED: "encoded", ENCODED: "encoded",
JSON: "json", JSON: "json",
TEXT: "text", TEXT: "text",
XML: "xml",
} }
export const RestBodyTypes = [ export const RestBodyTypes = [
@ -207,5 +208,6 @@ export const RestBodyTypes = [
{ name: "form-data", value: "form" }, { name: "form-data", value: "form" },
{ name: "x-www-form-encoded", value: "encoded" }, { name: "x-www-form-encoded", value: "encoded" },
{ name: "raw (JSON)", value: "json" }, { name: "raw (JSON)", value: "json" },
{ name: "raw (XML)", value: "xml" },
{ name: "raw (Text)", value: "text" }, { name: "raw (Text)", value: "text" },
] ]

View File

@ -7,7 +7,11 @@
} from "components/common/CodeMirrorEditor.svelte" } from "components/common/CodeMirrorEditor.svelte"
const objectTypes = [RawRestBodyTypes.FORM, RawRestBodyTypes.ENCODED] const objectTypes = [RawRestBodyTypes.FORM, RawRestBodyTypes.ENCODED]
const textTypes = [RawRestBodyTypes.JSON, RawRestBodyTypes.TEXT] const textTypes = [
RawRestBodyTypes.JSON,
RawRestBodyTypes.XML,
RawRestBodyTypes.TEXT,
]
export let query export let query
export let bodyType export let bodyType
@ -25,6 +29,18 @@
query.fields.requestBody = "" query.fields.requestBody = ""
} }
} }
function editorMode(type) {
switch (type) {
case RawRestBodyTypes.JSON:
return EditorModes.JSON
case RawRestBodyTypes.XML:
return EditorModes.XML
default:
case RawRestBodyTypes.TEXT:
return EditorModes.Text
}
}
</script> </script>
<div class="margin"> <div class="margin">
@ -41,9 +57,7 @@
{:else if textTypes.includes(bodyType)} {:else if textTypes.includes(bodyType)}
<CodeMirrorEditor <CodeMirrorEditor
height={200} height={200}
mode={bodyType === RawRestBodyTypes.JSON mode={editorMode(bodyType)}
? EditorModes.JSON
: EditorModes.Text}
value={query.fields.requestBody} value={query.fields.requestBody}
resize="vertical" resize="vertical"
on:change={e => (query.fields.requestBody = e.detail)} on:change={e => (query.fields.requestBody = e.detail)}

View File

@ -48,7 +48,7 @@
let breakQs = {}, let breakQs = {},
bindings = {} bindings = {}
let url = "" let url = ""
let saveId let saveId, isGet
let response, schema, enabledHeaders let response, schema, enabledHeaders
let datasourceType, integrationInfo, queryConfig, responseSuccess let datasourceType, integrationInfo, queryConfig, responseSuccess
let authConfigId let authConfigId
@ -60,6 +60,7 @@
$: url = buildUrl(url, breakQs) $: url = buildUrl(url, breakQs)
$: checkQueryName(url) $: checkQueryName(url)
$: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400 $: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400
$: isGet = query?.queryVerb === "read"
$: authConfigs = buildAuthConfigs(datasource) $: authConfigs = buildAuthConfigs(datasource)
$: schemaReadOnly = !responseSuccess $: schemaReadOnly = !responseSuccess
$: variablesReadOnly = !responseSuccess $: variablesReadOnly = !responseSuccess
@ -327,7 +328,7 @@
<Tab title="Body"> <Tab title="Body">
<RadioGroup <RadioGroup
bind:value={query.fields.bodyType} bind:value={query.fields.bodyType}
options={bodyTypes} options={isGet ? [bodyTypes[0]] : bodyTypes}
direction="horizontal" direction="horizontal"
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
getOptionValue={option => option.value} getOptionValue={option => option.value}

View File

@ -2,33 +2,33 @@
import { import {
Heading, Heading,
Layout, Layout,
Detail,
Button, Button,
ActionButton,
ActionGroup,
ButtonGroup, ButtonGroup,
Input, Input,
Select, Select,
Modal, Modal,
Page, Page,
notifications, notifications,
Body,
Search, Search,
} from "@budibase/bbui" } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte" import CreateAppModal from "components/start/CreateAppModal.svelte"
import UpdateAppModal from "components/start/UpdateAppModal.svelte" import UpdateAppModal from "components/start/UpdateAppModal.svelte"
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
import { store, automationStore } from "builderStore" import { store, automationStore } from "builderStore"
import api, { del, post, get } from "builderStore/api" import api, { del, post, get } from "builderStore/api"
import { onMount } from "svelte" import { onMount } from "svelte"
import { apps, auth, admin } from "stores/portal" import { apps, auth, admin, templates } from "stores/portal"
import download from "downloadjs" import download from "downloadjs"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AppCard from "components/start/AppCard.svelte"
import AppRow from "components/start/AppRow.svelte" import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants" import { AppStatus } from "constants"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
let layout = "grid"
let sortBy = "name" let sortBy = "name"
let template let template
let selectedApp let selectedApp
@ -36,13 +36,13 @@
let updatingModal let updatingModal
let deletionModal let deletionModal
let unpublishModal let unpublishModal
let iconModal
let creatingApp = false let creatingApp = false
let loaded = false let loaded = false
let searchTerm = "" let searchTerm = ""
let cloud = $admin.cloud let cloud = $admin.cloud
let appName = "" let appName = ""
let creatingFromTemplate = false let creatingFromTemplate = false
$: enrichedApps = enrichApps($apps, $auth.user, sortBy) $: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(app => $: filteredApps = enrichedApps.filter(app =>
app?.name?.toLowerCase().includes(searchTerm.toLowerCase()) app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
@ -172,6 +172,11 @@
$goto(`../../app/${app.devId}`) $goto(`../../app/${app.devId}`)
} }
const editIcon = app => {
selectedApp = app
iconModal.show()
}
const exportApp = app => { const exportApp = app => {
const id = app.deployed ? app.prodId : app.devId const id = app.deployed ? app.prodId : app.devId
const appName = encodeURIComponent(app.name) const appName = encodeURIComponent(app.name)
@ -262,6 +267,7 @@
onMount(async () => { onMount(async () => {
await apps.load() await apps.load()
await templates.load()
// if the portal is loaded from an external URL with a template param // if the portal is loaded from an external URL with a template param
const initInfo = await auth.getInitInfo() const initInfo = await auth.getInitInfo()
if (initInfo?.init_template) { if (initInfo?.init_template) {
@ -274,21 +280,66 @@
</script> </script>
<Page wide> <Page wide>
{#if loaded && enrichedApps.length}
<Layout noPadding> <Layout noPadding>
<div class="title"> <div class="title">
<Heading>Apps</Heading> <Heading size="S">Welcome to Budibase</Heading>
<ButtonGroup> <ButtonGroup>
{#if cloud} {#if cloud}
<Button secondary on:click={initiateAppsExport}>Export apps</Button> <Button secondary on:click={initiateAppsExport}>Export apps</Button>
{/if} {/if}
<Button secondary on:click={initiateAppImport}>Import app</Button> <Button icon="Import" quiet secondary on:click={initiateAppImport}
<Button cta on:click={initiateAppCreation}>Create app</Button> >Import app</Button
>
<Button icon="Add" cta on:click={initiateAppCreation}>Create app</Button
>
</ButtonGroup> </ButtonGroup>
</div> </div>
<div class="title-text">
<Body size="S">Manage your apps and get a head start with templates</Body>
</div>
<Detail>Quick Start Templates</Detail>
<div class="grid">
{#each $templates as item}
<div
on:click={() => {
template = item
creationModal.show()
creatingApp = true
}}
class="template-card"
>
<div class="card-body">
<div style="color: {item.background}" class="iconAlign">
<svg
width="26px"
height="26px"
class="spectrum-Icon"
style="color:{item.background};"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-{item.icon}" />
</svg>
</div>
<div class="iconAlign">
<Body weight="900" size="S">{item.name}</Body>
<div style="font-size: 10px;">
<Body size="S">{item.category.toUpperCase()}</Body>
</div>
</div>
</div>
</div>
{/each}
</div>
{#if loaded && enrichedApps.length}
<div class="title">
<Detail>My Apps</Detail>
</div>
<div class="filter"> <div class="filter">
<div class="select"> <div class="select">
<Select <Select
quiet
autoWidth autoWidth
bind:value={sortBy} bind:value={sortBy}
placeholder={null} placeholder={null}
@ -299,35 +350,18 @@
]} ]}
/> />
<div class="desktop-search"> <div class="desktop-search">
<Search placeholder="Search" bind:value={searchTerm} /> <Search quiet placeholder="Search" bind:value={searchTerm} />
</div> </div>
</div> </div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div> </div>
<div class="mobile-search"> <div class="mobile-search">
<Search placeholder="Search" bind:value={searchTerm} /> <Search placeholder="Search" bind:value={searchTerm} />
</div> </div>
<div <div class="appTable">
class:appGrid={layout === "grid"}
class:appTable={layout === "table"}
>
{#each filteredApps as app (app.appId)} {#each filteredApps as app (app.appId)}
<svelte:component <AppRow
this={layout === "grid" ? AppCard : AppRow}
{releaseLock} {releaseLock}
{editIcon}
{app} {app}
{unpublishApp} {unpublishApp}
{viewApp} {viewApp}
@ -338,7 +372,6 @@
/> />
{/each} {/each}
</div> </div>
</Layout>
{/if} {/if}
{#if !enrichedApps.length && !creatingApp && loaded} {#if !enrichedApps.length && !creatingApp && loaded}
<div class="empty-wrapper"> <div class="empty-wrapper">
@ -353,7 +386,9 @@
<Spinner size="10" /> <Spinner size="10" />
</div> </div>
{/if} {/if}
</Layout>
</Page> </Page>
<Modal <Modal
bind:this={creationModal} bind:this={creationModal}
padding={false} padding={false}
@ -389,6 +424,7 @@
</ConfirmDialog> </ConfirmDialog>
<UpdateAppModal app={selectedApp} bind:this={updatingModal} /> <UpdateAppModal app={selectedApp} bind:this={updatingModal} />
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
<style> <style>
.title, .title,
@ -397,7 +433,7 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 10px; gap: 5px;
} }
@media only screen and (max-width: 560px) { @media only screen and (max-width: 560px) {
@ -405,12 +441,48 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
.iconAlign {
padding: 0 0 0 var(--spacing-m);
display: inline-block;
}
.template-card {
height: 80px;
width: 270px;
border-radius: var(--border-radius-s);
margin-bottom: var(--spacing-m);
border: 1px solid var(--spectrum-global-color-gray-300);
cursor: pointer;
display: flex;
}
.title-text {
margin-top: calc(var(--spacing-xl) * -1);
}
.card-body {
display: flex;
align-items: center;
padding: 12px;
}
.grid {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
}
@media (min-width: 200px) {
} }
.select { .select {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: auto auto;
grid-gap: 10px; grid-gap: 30px;
} }
.filter :global(.spectrum-ActionGroup) { .filter :global(.spectrum-ActionGroup) {
flex-wrap: nowrap; flex-wrap: nowrap;
@ -419,11 +491,6 @@
display: none; display: none;
} }
.appGrid {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.appTable { .appTable {
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
@ -464,4 +531,8 @@
display: block; display: block;
} }
} }
.template-card:hover {
background: var(--spectrum-alias-background-color-tertiary);
}
</style> </style>

View File

@ -65,16 +65,17 @@ export function createAppStore() {
} }
} }
async function update(appId, name) { async function update(appId, value) {
const response = await api.put(`/api/applications/${appId}`, { name }) console.log({ value })
const response = await api.put(`/api/applications/${appId}`, { ...value })
if (response.status === 200) { if (response.status === 200) {
store.update(state => { store.update(state => {
const updatedAppIndex = state.findIndex( const updatedAppIndex = state.findIndex(
app => app.instance._id === appId app => app.instance._id === appId
) )
if (updatedAppIndex !== -1) { if (updatedAppIndex !== -1) {
const updatedApp = state[updatedAppIndex] let updatedApp = state[updatedAppIndex]
updatedApp.name = name updatedApp = { ...updatedApp, ...value }
state.apps = state.splice(updatedAppIndex, 1, updatedApp) state.apps = state.splice(updatedAppIndex, 1, updatedApp)
} }
return state return state

View File

@ -5,3 +5,4 @@ export { apps } from "./apps"
export { email } from "./email" export { email } from "./email"
export { auth } from "./auth" export { auth } from "./auth"
export { oidc } from "./oidc" export { oidc } from "./oidc"
export { templates } from "./templates"

View File

@ -0,0 +1,19 @@
import { writable } from "svelte/store"
import api from "builderStore/api"
export function templatesStore() {
const { subscribe, set } = writable([])
async function load() {
const response = await api.get("/api/templates?type=app")
const json = await response.json()
set(json)
}
return {
subscribe,
load,
}
}
export const templates = templatesStore()

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.19-alpha.2", "@budibase/bbui": "^1.0.22",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^1.0.19-alpha.2", "@budibase/string-templates": "^1.0.22",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -1,8 +1,9 @@
<script> <script>
import { setContext, getContext } from "svelte" import { setContext, getContext, onMount } from "svelte"
import Router, { querystring } from "svelte-spa-router" import Router, { querystring } from "svelte-spa-router"
import { routeStore } from "stores" import { routeStore, stateStore } from "stores"
import Screen from "./Screen.svelte" import Screen from "./Screen.svelte"
import { get } from "svelte/store"
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
@ -17,15 +18,17 @@
} }
// Keep query params up to date // Keep query params up to date
$: { $: routeStore.actions.setQueryParams(parseQueryString($querystring))
const parseQueryString = query => {
let queryParams = {} let queryParams = {}
if ($querystring) { if (query) {
const urlSearchParams = new URLSearchParams($querystring) const urlSearchParams = new URLSearchParams(query)
for (const [key, value] of urlSearchParams) { for (const [key, value] of urlSearchParams) {
queryParams[key] = value queryParams[key] = value
} }
} }
routeStore.actions.setQueryParams(queryParams) return queryParams
} }
const getRouterConfig = routes => { const getRouterConfig = routes => {
@ -42,6 +45,19 @@
const onRouteLoading = ({ detail }) => { const onRouteLoading = ({ detail }) => {
routeStore.actions.setActiveRoute(detail.route) routeStore.actions.setActiveRoute(detail.route)
} }
// Initialise state store from query string on initial load
onMount(() => {
const queryParams = parseQueryString(get(querystring))
if (queryParams.state) {
try {
const state = JSON.parse(atob(queryParams.state))
stateStore.actions.initialise(state)
} catch (error) {
// Swallow error and do nothing
}
}
})
</script> </script>
{#key config.id} {#key config.id}

View File

@ -4,6 +4,7 @@
dataSourceStore, dataSourceStore,
notificationStore, notificationStore,
routeStore, routeStore,
stateStore,
} from "stores" } from "stores"
import { Modal, ModalContent, ActionButton } from "@budibase/bbui" import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
@ -12,12 +13,13 @@
NOTIFICATION: "notification", NOTIFICATION: "notification",
CLOSE_SCREEN_MODAL: "close-screen-modal", CLOSE_SCREEN_MODAL: "close-screen-modal",
INVALIDATE_DATASOURCE: "invalidate-datasource", INVALIDATE_DATASOURCE: "invalidate-datasource",
UPDATE_STATE: "update-state",
} }
let iframe let iframe
let listenersAttached = false let listenersAttached = false
const invalidateDataSource = event => { const proxyInvalidation = event => {
const { dataSourceId } = event.detail const { dataSourceId } = event.detail
dataSourceStore.actions.invalidateDataSource(dataSourceId) dataSourceStore.actions.invalidateDataSource(dataSourceId)
} }
@ -27,14 +29,28 @@
notificationStore.actions.send(message, type, icon) notificationStore.actions.send(message, type, icon)
} }
const proxyStateUpdate = event => {
const { type, key, value, persist } = event.detail
if (type === "set") {
stateStore.actions.setValue(key, value, persist)
} else if (type === "delete") {
stateStore.actions.deleteValue(key)
}
}
function receiveMessage(message) { function receiveMessage(message) {
const handlers = { const handlers = {
[MessageTypes.NOTIFICATION]: () => { [MessageTypes.NOTIFICATION]: () => {
proxyNotification(message.data) proxyNotification(message.data)
}, },
[MessageTypes.CLOSE_SCREEN_MODAL]: peekStore.actions.hidePeek, [MessageTypes.CLOSE_SCREEN_MODAL]: () => {
peekStore.actions.hidePeek()
},
[MessageTypes.INVALIDATE_DATASOURCE]: () => { [MessageTypes.INVALIDATE_DATASOURCE]: () => {
invalidateDataSource(message.data) proxyInvalidation(message.data)
},
[MessageTypes.UPDATE_STATE]: () => {
proxyStateUpdate(message.data)
}, },
} }

View File

@ -1,6 +1,7 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { fetchTableDefinition } from "../api" import { fetchTableDefinition } from "../api"
import { FieldTypes } from "../constants" import { FieldTypes } from "../constants"
import { routeStore } from "./routes"
export const createDataSourceStore = () => { export const createDataSourceStore = () => {
const store = writable([]) const store = writable([])
@ -60,10 +61,12 @@ export const createDataSourceStore = () => {
// Emit this as a window event, so parent screens which are iframing us in // Emit this as a window event, so parent screens which are iframing us in
// can also invalidate the same datasource // can also invalidate the same datasource
if (get(routeStore).queryParams?.peek) {
window.parent.postMessage({ window.parent.postMessage({
type: "close-screen-modal", type: "invalidate-datasource",
detail: { dataSourceId }, detail: { dataSourceId },
}) })
}
let invalidations = [dataSourceId] let invalidations = [dataSourceId]

View File

@ -1,4 +1,5 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
import { stateStore } from "./state.js"
const initialState = { const initialState = {
showPeek: false, showPeek: false,
@ -14,7 +15,10 @@ const createPeekStore = () => {
let href = url let href = url
let external = !url.startsWith("/") let external = !url.startsWith("/")
if (!external) { if (!external) {
href = `${window.location.href.split("#")[0]}#${url}?peek=true` const state = get(stateStore)
const serialised = encodeURIComponent(btoa(JSON.stringify(state)))
const query = `peek=true&state=${serialised}`
href = `${window.location.href.split("#")[0]}#${url}?${query}`
} }
store.set({ store.set({
showPeek: true, showPeek: true,

View File

@ -1,9 +1,9 @@
import { writable, get, derived } from "svelte/store" import { writable, get, derived } from "svelte/store"
import { localStorageStore } from "builder/src/builderStore/store/localStorage" import { localStorageStore } from "builder/src/builderStore/store/localStorage"
import { appStore } from "./app"
const createStateStore = () => { const createStateStore = () => {
const localStorageKey = `${get(appStore).appId}.state` const appId = window["##BUDIBASE_APP_ID##"] || "app"
const localStorageKey = `${appId}.state`
const persistentStore = localStorageStore(localStorageKey, {}) const persistentStore = localStorageStore(localStorageKey, {})
// Initialise the temp store to mirror the persistent store // Initialise the temp store to mirror the persistent store
@ -34,6 +34,9 @@ const createStateStore = () => {
}) })
} }
// Initialises the temporary state store with a certain value
const initialise = tempStore.set
// Derive the combination of both persisted and non persisted stores // Derive the combination of both persisted and non persisted stores
const store = derived( const store = derived(
[tempStore, persistentStore], [tempStore, persistentStore],
@ -47,7 +50,7 @@ const createStateStore = () => {
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { setValue, deleteValue }, actions: { setValue, deleteValue, initialise },
} }
} }

View File

@ -116,6 +116,15 @@ const updateStateHandler = action => {
} else if (type === "delete") { } else if (type === "delete") {
stateStore.actions.deleteValue(key) stateStore.actions.deleteValue(key)
} }
// Emit this as an event so that parent windows which are iframing us in
// can also update their state
if (get(routeStore).queryParams?.peek) {
window.parent.postMessage({
type: "update-state",
detail: { type, key, value, persist },
})
}
} }
const handlerMap = { const handlerMap = {

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -70,9 +70,9 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/auth": "^1.0.19-alpha.2", "@budibase/auth": "^1.0.22",
"@budibase/client": "^1.0.19-alpha.2", "@budibase/client": "^1.0.22",
"@budibase/string-templates": "^1.0.19-alpha.2", "@budibase/string-templates": "^1.0.22",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",
@ -90,6 +90,7 @@
"dotenv": "8.2.0", "dotenv": "8.2.0",
"download": "8.0.0", "download": "8.0.0",
"fix-path": "3.0.0", "fix-path": "3.0.0",
"form-data": "^4.0.0",
"fs-extra": "8.1.0", "fs-extra": "8.1.0",
"jimp": "0.16.1", "jimp": "0.16.1",
"joi": "17.2.1", "joi": "17.2.1",
@ -126,6 +127,7 @@
"validate.js": "0.13.1", "validate.js": "0.13.1",
"vm2": "^3.9.3", "vm2": "^3.9.3",
"worker-farm": "^1.7.0", "worker-farm": "^1.7.0",
"xml2js": "^0.4.23",
"yargs": "13.2.4", "yargs": "13.2.4",
"zlib": "1.0.5" "zlib": "1.0.5"
}, },

View File

@ -5,11 +5,15 @@ const { getAutomationParams, generateAutomationID } = require("../../db/utils")
const { const {
checkForWebhooks, checkForWebhooks,
updateTestHistory, updateTestHistory,
removeDeprecated,
} = require("../../automations/utils") } = require("../../automations/utils")
const { deleteEntityMetadata } = require("../../utilities") const { deleteEntityMetadata } = require("../../utilities")
const { MetadataTypes } = require("../../constants") const { MetadataTypes } = require("../../constants")
const { setTestFlag, clearTestFlag } = require("../../utilities/redis") const { setTestFlag, clearTestFlag } = require("../../utilities/redis")
const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS)
const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS)
/************************* /*************************
* * * *
* BUILDER FUNCTIONS * * BUILDER FUNCTIONS *
@ -155,17 +159,17 @@ exports.destroy = async function (ctx) {
} }
exports.getActionList = async function (ctx) { exports.getActionList = async function (ctx) {
ctx.body = actions.ACTION_DEFINITIONS ctx.body = ACTION_DEFS
} }
exports.getTriggerList = async function (ctx) { exports.getTriggerList = async function (ctx) {
ctx.body = triggers.TRIGGER_DEFINITIONS ctx.body = TRIGGER_DEFS
} }
module.exports.getDefinitionList = async function (ctx) { module.exports.getDefinitionList = async function (ctx) {
ctx.body = { ctx.body = {
trigger: triggers.TRIGGER_DEFINITIONS, trigger: TRIGGER_DEFS,
action: actions.ACTION_DEFINITIONS, action: ACTION_DEFS,
} }
} }

View File

@ -13,12 +13,11 @@ const RequestType = {
const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH] const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH]
/** /**
* Note, there is some functionality in this that is not currently exposed as it * NOTE: this functionality is deprecated - it no longer should be used.
* is complex and maybe better to be opinionated here.
* GET/DELETE requests cannot handle body elements so they will not be sent if configured.
*/ */
exports.definition = { exports.definition = {
deprecated: true,
name: "Outgoing webhook", name: "Outgoing webhook",
tagline: "Send a {{inputs.requestMethod}} request", tagline: "Send a {{inputs.requestMethod}} request",
icon: "Send", icon: "Send",

View File

@ -7,6 +7,7 @@ const newid = require("../db/newid")
const { updateEntityMetadata } = require("../utilities") const { updateEntityMetadata } = require("../utilities")
const { MetadataTypes } = require("../constants") const { MetadataTypes } = require("../constants")
const { getDeployedAppID } = require("@budibase/auth/db") const { getDeployedAppID } = require("@budibase/auth/db")
const { cloneDeep } = require("lodash/fp")
const WH_STEP_ID = definitions.WEBHOOK.stepId const WH_STEP_ID = definitions.WEBHOOK.stepId
const CRON_STEP_ID = definitions.CRON.stepId const CRON_STEP_ID = definitions.CRON.stepId
@ -42,6 +43,16 @@ exports.updateTestHistory = async (appId, automation, history) => {
) )
} }
exports.removeDeprecated = definitions => {
const base = cloneDeep(definitions)
for (let key of Object.keys(base)) {
if (base[key].deprecated) {
delete base[key]
}
}
return base
}
// end the repetition and the job itself // end the repetition and the job itself
exports.disableAllCrons = async appId => { exports.disableAllCrons = async appId => {
const promises = [] const promises = []

View File

@ -6,13 +6,14 @@ import {
RestQueryFields as RestQuery, RestQueryFields as RestQuery,
AuthType, AuthType,
BasicAuthConfig, BasicAuthConfig,
BearerAuthConfig BearerAuthConfig,
} from "../definitions/datasource" } from "../definitions/datasource"
import { IntegrationBase } from "./base/IntegrationBase" import { IntegrationBase } from "./base/IntegrationBase"
const BodyTypes = { const BodyTypes = {
NONE: "none", NONE: "none",
FORM_DATA: "form", FORM_DATA: "form",
XML: "xml",
ENCODED: "encoded", ENCODED: "encoded",
JSON: "json", JSON: "json",
TEXT: "text", TEXT: "text",
@ -45,6 +46,9 @@ module RestModule {
const fetch = require("node-fetch") const fetch = require("node-fetch")
const { formatBytes } = require("../utilities") const { formatBytes } = require("../utilities")
const { performance } = require("perf_hooks") const { performance } = require("perf_hooks")
const FormData = require("form-data")
const { URLSearchParams } = require("url")
const { parseStringPromise: xmlParser, Builder: XmlBuilder } = require("xml2js")
const SCHEMA: Integration = { const SCHEMA: Integration = {
docs: "https://github.com/node-fetch/node-fetch", docs: "https://github.com/node-fetch/node-fetch",
@ -110,15 +114,38 @@ module RestModule {
async parseResponse(response: any) { async parseResponse(response: any) {
let data, raw, headers let data, raw, headers
const contentType = response.headers.get("content-type") const contentType = response.headers.get("content-type") || ""
if (contentType && contentType.indexOf("application/json") !== -1) { try {
if (contentType.includes("application/json")) {
data = await response.json() data = await response.json()
raw = JSON.stringify(data) raw = JSON.stringify(data)
} else if (
contentType.includes("text/xml") ||
contentType.includes("application/xml")
) {
const rawXml = await response.text()
data =
(await xmlParser(rawXml, {
explicitArray: false,
trim: true,
explicitRoot: false,
})) || {}
// there is only one structure, its an array, return the array so it appears as rows
const keys = Object.keys(data)
if (keys.length === 1 && Array.isArray(data[keys[0]])) {
data = data[keys[0]]
}
raw = rawXml
} else { } else {
data = await response.text() data = await response.text()
raw = data raw = data
} }
const size = formatBytes(response.headers.get("content-length") || Buffer.byteLength(raw, "utf8")) } catch (err) {
throw "Failed to parse response body."
}
const size = formatBytes(
response.headers.get("content-length") || Buffer.byteLength(raw, "utf8")
)
const time = `${Math.round(performance.now() - this.startTimeMs)}ms` const time = `${Math.round(performance.now() - this.startTimeMs)}ms`
headers = response.headers.raw() headers = response.headers.raw()
for (let [key, value] of Object.entries(headers)) { for (let [key, value] of Object.entries(headers)) {
@ -150,7 +177,59 @@ module RestModule {
return complete return complete
} }
getAuthHeaders(authConfigId: string): { [key: string]: any }{ addBody(bodyType: string, body: string | any, input: any) {
let error, object, string
try {
string = typeof body !== "string" ? JSON.stringify(body) : body
object = typeof body === "object" ? body : JSON.parse(body)
} catch (err) {
error = err
}
if (!input.headers) {
input.headers = {}
}
switch (bodyType) {
case BodyTypes.NONE:
break
case BodyTypes.TEXT:
// content type defaults to plaintext
input.body = string
break
case BodyTypes.ENCODED:
const params = new URLSearchParams()
for (let [key, value] of Object.entries(object)) {
params.append(key, value)
}
input.body = params
break
case BodyTypes.FORM_DATA:
const form = new FormData()
for (let [key, value] of Object.entries(object)) {
form.append(key, value)
}
input.body = form
break
case BodyTypes.XML:
if (object != null) {
string = (new XmlBuilder()).buildObject(object)
}
input.body = string
input.headers["Content-Type"] = "application/xml"
break
default:
case BodyTypes.JSON:
// if JSON error, throw it
if (error) {
throw "Invalid JSON for request body"
}
input.body = string
input.headers["Content-Type"] = "application/json"
break
}
return input
}
getAuthHeaders(authConfigId: string): { [key: string]: any } {
let headers: any = {} let headers: any = {}
if (this.config.authConfigs && authConfigId) { if (this.config.authConfigs && authConfigId) {
@ -180,7 +259,16 @@ module RestModule {
} }
async _req(query: RestQuery) { async _req(query: RestQuery) {
const { path = "", queryString = "", headers = {}, method = "GET", disabledHeaders, bodyType, requestBody, authConfigId } = query const {
path = "",
queryString = "",
headers = {},
method = "GET",
disabledHeaders,
bodyType,
requestBody,
authConfigId,
} = query
const authHeaders = this.getAuthHeaders(authConfigId) const authHeaders = this.getAuthHeaders(authConfigId)
this.headers = { this.headers = {
@ -197,18 +285,9 @@ module RestModule {
} }
} }
let json let input: any = { method, headers: this.headers }
if (bodyType === BodyTypes.JSON && requestBody) { if (requestBody) {
try { input = this.addBody(bodyType, requestBody, input)
json = JSON.parse(requestBody)
} catch (err) {
throw "Invalid JSON for request body"
}
}
const input: any = { method, headers: this.headers }
if (json && typeof json === "object" && Object.keys(json).length > 0) {
input.body = JSON.stringify(json)
} }
this.startTimeMs = performance.now() this.startTimeMs = performance.now()

View File

@ -14,6 +14,11 @@ const fetch = require("node-fetch")
const RestIntegration = require("../rest") const RestIntegration = require("../rest")
const { AuthType } = require("../rest") const { AuthType } = require("../rest")
const HEADERS = {
"Accept": "application/json",
"Content-Type": "application/json"
}
class TestConfiguration { class TestConfiguration {
constructor(config = {}) { constructor(config = {}) {
this.integration = new RestIntegration.integration(config) this.integration = new RestIntegration.integration(config)
@ -35,9 +40,7 @@ describe("REST Integration", () => {
const query = { const query = {
path: "api", path: "api",
queryString: "test=1", queryString: "test=1",
headers: { headers: HEADERS,
Accept: "application/json",
},
bodyType: "json", bodyType: "json",
requestBody: JSON.stringify({ requestBody: JSON.stringify({
name: "test", name: "test",
@ -47,9 +50,7 @@ describe("REST Integration", () => {
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
method: "POST", method: "POST",
body: '{"name":"test"}', body: '{"name":"test"}',
headers: { headers: HEADERS,
Accept: "application/json",
},
}) })
}) })
@ -86,9 +87,7 @@ describe("REST Integration", () => {
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
method: "PUT", method: "PUT",
body: '{"name":"test"}', body: '{"name":"test"}',
headers: { headers: HEADERS,
Accept: "application/json",
},
}) })
}) })
@ -107,13 +106,98 @@ describe("REST Integration", () => {
const response = await config.integration.delete(query) const response = await config.integration.delete(query)
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, { expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
method: "DELETE", method: "DELETE",
headers: { headers: HEADERS,
Accept: "application/json",
},
body: '{"name":"test"}', body: '{"name":"test"}',
}) })
}) })
describe("request body", () => {
const input = { a: 1, b: 2 }
it("should allow no body", () => {
const output = config.integration.addBody("none", null, {})
expect(output.body).toBeUndefined()
expect(Object.keys(output.headers).length).toEqual(0)
})
it("should allow text body", () => {
const output = config.integration.addBody("text", "hello world", {})
expect(output.body).toEqual("hello world")
// gets added by fetch
expect(Object.keys(output.headers).length).toEqual(0)
})
it("should allow form data", () => {
const FormData = require("form-data")
const output = config.integration.addBody("form", input, {})
expect(output.body instanceof FormData).toEqual(true)
expect(output.body._valueLength).toEqual(2)
// gets added by fetch
expect(Object.keys(output.headers).length).toEqual(0)
})
it("should allow encoded form data", () => {
const { URLSearchParams } = require("url")
const output = config.integration.addBody("encoded", input, {})
expect(output.body instanceof URLSearchParams).toEqual(true)
expect(output.body.toString()).toEqual("a=1&b=2")
// gets added by fetch
expect(Object.keys(output.headers).length).toEqual(0)
})
it("should allow JSON", () => {
const output = config.integration.addBody("json", input, {})
expect(output.body).toEqual(JSON.stringify(input))
expect(output.headers["Content-Type"]).toEqual("application/json")
})
it("should allow XML", () => {
const output = config.integration.addBody("xml", input, {})
expect(output.body.includes("<a>1</a>")).toEqual(true)
expect(output.body.includes("<b>2</b>")).toEqual(true)
expect(output.headers["Content-Type"]).toEqual("application/xml")
})
})
describe("response", () => {
function buildInput(json, text, header) {
return {
status: 200,
json: json ? async () => json : undefined,
text: text ? async () => text : undefined,
headers: { get: key => key === "content-length" ? 100 : header, raw: () => ({ "content-type": header }) }
}
}
it("should be able to parse JSON response", async () => {
const input = buildInput({a: 1}, null, "application/json")
const output = await config.integration.parseResponse(input)
expect(output.data).toEqual({a: 1})
expect(output.info.code).toEqual(200)
expect(output.info.size).toEqual("100B")
expect(output.extra.raw).toEqual(JSON.stringify({a: 1}))
expect(output.extra.headers["content-type"]).toEqual("application/json")
})
it("should be able to parse text response", async () => {
const text = "hello world"
const input = buildInput(null, text, "text/plain")
const output = await config.integration.parseResponse(input)
expect(output.data).toEqual(text)
expect(output.extra.raw).toEqual(text)
expect(output.extra.headers["content-type"]).toEqual("text/plain")
})
it("should be able to parse XML response", async () => {
const text = "<root><a>1</a><b>2</b></root>"
const input = buildInput(null, text, "application/xml")
const output = await config.integration.parseResponse(input)
expect(output.data).toEqual({a: "1", b: "2"})
expect(output.extra.raw).toEqual(text)
expect(output.extra.headers["content-type"]).toEqual("application/xml")
})
})
describe("authentication", () => { describe("authentication", () => {
const basicAuth = { const basicAuth = {
_id: "c59c14bd1898a43baa08da68959b24686", _id: "c59c14bd1898a43baa08da68959b24686",

View File

@ -1,6 +1,5 @@
// when thread starts, make sure it is recorded require("./utils").threadSetup()
const env = require("../environment") const env = require("../environment")
env.setInThread()
const actions = require("../automations/actions") const actions = require("../automations/actions")
const automationUtils = require("../automations/automationUtils") const automationUtils = require("../automations/automationUtils")
const AutomationEmitter = require("../events/AutomationEmitter") const AutomationEmitter = require("../events/AutomationEmitter")

View File

@ -1,6 +1,4 @@
// when thread starts, make sure it is recorded require("./utils").threadSetup()
const env = require("../environment")
env.setInThread()
const ScriptRunner = require("../utilities/scriptRunner") const ScriptRunner = require("../utilities/scriptRunner")
const { integrations } = require("../integrations") const { integrations } = require("../integrations")

View File

@ -0,0 +1,13 @@
const env = require("../environment")
const CouchDB = require("../db")
const { init } = require("@budibase/auth")
exports.threadSetup = () => {
// don't run this if not threading
if (env.isTest() || env.DISABLE_THREADING) {
return
}
// when thread starts, make sure it is recorded
env.setInThread()
init(CouchDB)
}

View File

@ -983,10 +983,10 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@budibase/auth@^1.0.18": "@budibase/auth@^1.0.19-alpha.1":
version "1.0.19" version "1.0.22"
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.19.tgz#b5a8ad51170443d2136d244f51cfe7dbcc0db116" resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.22.tgz#a93ea2fea46e00138ad3fa129c9ea19b056654e2"
integrity sha512-6H1K80KX8RUseLXD307tKRc+b0B7/b2SZmAYYGq5qrUSdUotydZaZ90pt5pXVdE754duxyc8DlrwmRfri5xu+A== integrity sha512-eHCNEzGl6HxYlMpfRTXBokq2ALTK5f+CDSgJmGaL/jfPc2NlzCI5NoigZUkSrdwDiYZnnWLfDDR4dArYyLlFuA==
dependencies: dependencies:
"@techpass/passport-openidconnect" "^0.3.0" "@techpass/passport-openidconnect" "^0.3.0"
aws-sdk "^2.901.0" aws-sdk "^2.901.0"
@ -1056,10 +1056,10 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/bbui@^1.0.19": "@budibase/bbui@^1.0.22":
version "1.0.19" version "1.0.22"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.19.tgz#d79c99e8c0adcf24d9b83f00a15eb262ad73a7e2" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.22.tgz#ac3bd3a8699bd0be84aac3c5dff9d093e5b08462"
integrity sha512-GhsyqkDjHMvU1MCr7oXKIZi6NOhmkunJ6eAoob8obCLDm+LXC/1Q8ymSuJicctQpDpraaFS7zqQ6vYY9v7kpiQ== integrity sha512-8/5rXEOwkr0OcQD1fn5GpmI3d5dS1cIJBAODjTVtlZrTdacwlz5W2j3zIh+CBG0X7zhVxEze3zs2b1vDNTvK6A==
dependencies: dependencies:
"@adobe/spectrum-css-workflow-icons" "^1.2.1" "@adobe/spectrum-css-workflow-icons" "^1.2.1"
"@spectrum-css/actionbutton" "^1.0.1" "@spectrum-css/actionbutton" "^1.0.1"
@ -1106,14 +1106,14 @@
svelte-flatpickr "^3.2.3" svelte-flatpickr "^3.2.3"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/client@^1.0.18": "@budibase/client@^1.0.19-alpha.1":
version "1.0.19" version "1.0.22"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.19.tgz#50ba2ad91ac2fd57c51306b80fbab24a26ba1403" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.22.tgz#80d6c3fb2b57a050199dde4a4b3e82b221601c25"
integrity sha512-8vAsD7VkLfq9ZrD+QPXGUcj/2D3vGO++IPr0zIKGNVG5FlOLFceQ9b7itExSFWutyVAjK/e/yq56tugnf0S+Fg== integrity sha512-Cpao7l2lIWyJZJs8+zq1wFnQGaWRTDiRG+HkkjvqQZDkZexlo89zWPkY56NBbMT1qAXd6K3zAdRNNKVCBCtOaA==
dependencies: dependencies:
"@budibase/bbui" "^1.0.19" "@budibase/bbui" "^1.0.22"
"@budibase/standard-components" "^0.9.139" "@budibase/standard-components" "^0.9.139"
"@budibase/string-templates" "^1.0.19" "@budibase/string-templates" "^1.0.22"
regexparam "^1.3.0" regexparam "^1.3.0"
shortid "^2.2.15" shortid "^2.2.15"
svelte-spa-router "^3.0.5" svelte-spa-router "^3.0.5"
@ -1163,10 +1163,10 @@
svelte-apexcharts "^1.0.2" svelte-apexcharts "^1.0.2"
svelte-flatpickr "^3.1.0" svelte-flatpickr "^3.1.0"
"@budibase/string-templates@^1.0.18", "@budibase/string-templates@^1.0.19": "@budibase/string-templates@^1.0.19-alpha.1", "@budibase/string-templates@^1.0.22":
version "1.0.19" version "1.0.22"
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.19.tgz#4b476dfc5d317e56d84a24dffd34715cc74c7b37" resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.22.tgz#b795c61e53d541c0aa346a90d04b50dcca6ae117"
integrity sha512-MmSHF2HK3JS3goyNr3mUQi3azt5vSWlmSGlYFyw473jplRVYkmI8wXrP8gVy9mNJ4vksn3bkgFPI8Hi9RoNSbA== integrity sha512-1ZhxzL75kVhP44fJlCWwqmGIPjZol1eB/xi3O11xJPYQ7lfzeJcGUpksvlgbLgBlw+MKkgppK7gEoMP247E0Qw==
dependencies: dependencies:
"@budibase/handlebars-helpers" "^0.11.7" "@budibase/handlebars-helpers" "^0.11.7"
dayjs "^1.10.4" dayjs "^1.10.4"
@ -5829,6 +5829,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8" combined-stream "^1.0.8"
mime-types "^2.1.12" mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
formidable@^1.1.1, formidable@^1.2.0: formidable@^1.1.1, formidable@^1.2.0:
version "1.2.6" version "1.2.6"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
@ -13081,7 +13090,7 @@ xml2js@0.4.19:
sax ">=0.6.0" sax ">=0.6.0"
xmlbuilder "~9.0.1" xmlbuilder "~9.0.1"
xml2js@^0.4.19, xml2js@^0.4.5: xml2js@^0.4.19, xml2js@^0.4.23, xml2js@^0.4.5:
version "0.4.23" version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.19-alpha.2", "version": "1.0.22",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -29,8 +29,8 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/auth": "^1.0.19-alpha.2", "@budibase/auth": "^1.0.22",
"@budibase/string-templates": "^1.0.19-alpha.2", "@budibase/string-templates": "^1.0.22",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0", "@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",