1
0
Fork 0
mirror of synced 2024-06-30 20:10:54 +12:00

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

This commit is contained in:
mike12345567 2021-11-11 13:50:42 +00:00
commit 5c8670c7f4
61 changed files with 479 additions and 278 deletions

View file

@ -1,5 +1,5 @@
{
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

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

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"license": "AGPL-3.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",

View file

@ -36,5 +36,7 @@
{getOptionLabel}
{getOptionValue}
on:change={onChange}
on:pick
on:type
/>
</Field>

View file

@ -40,8 +40,15 @@
open = false
}
const onChange = e => {
selectOption(e.target.value)
const onType = e => {
const value = e.target.value
dispatch("type", value)
selectOption(value)
}
const onPick = value => {
dispatch("pick", value)
selectOption(value)
}
</script>
@ -62,7 +69,7 @@
type="text"
on:focus={() => (focus = true)}
on:blur={() => (focus = false)}
on:change={onChange}
on:change={onType}
value={value || ""}
placeholder={placeholder || ""}
{disabled}
@ -99,7 +106,7 @@
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(getOptionValue(option))}
on:click={() => onPick(getOptionValue(option))}
>
<span class="spectrum-Menu-itemLabel"
>{getOptionLabel(option)}</span

View file

@ -11,7 +11,8 @@ it("should rename an unpublished application", () => {
renameApp(appRename)
cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
})
cy.deleteApp(appRename)
})
xit("Should rename a published application", () => {
// It is not possible to rename a published application

View file

@ -43,24 +43,26 @@ Cypress.Commands.add("createApp", name => {
})
})
Cypress.Commands.add("deleteApp", () => {
Cypress.Commands.add("deleteApp", appName => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(1000)
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
.its("body")
.then(val => {
console.log(val)
if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
cy.contains("Delete").click()
cy.get(".spectrum-Button--warning").click()
cy.get(".spectrum-Modal").within(() => {
cy.get("input").type(appName)
cy.get(".spectrum-Button--warning").click()
})
}
})
})
Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests"
cy.deleteApp()
cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.")
})

View file

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

View file

@ -12,7 +12,7 @@ export default class PosthogClient {
posthog.init(this.token, {
autocapture: false,
capture_pageview: false,
capture_pageview: true,
api_host: this.url,
})
posthog.set_config({ persistence: "cookie" })

View file

@ -31,11 +31,11 @@ export const getBindableProperties = (asset, componentId) => {
const deviceBindings = getDeviceBindings()
const stateBindings = getStateBindings()
return [
...stateBindings,
...deviceBindings,
...urlBindings,
...contextBindings,
...urlBindings,
...stateBindings,
...userBindings,
...deviceBindings,
]
}
@ -217,18 +217,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
keys.forEach(key => {
const fieldSchema = schema[key]
// Make safe runtime binding and replace certain bindings with a
// new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
const runtimeBinding = `${safeComponentId}.${makePropSafe(
runtimeBoundKey
)}`
// Make safe runtime binding
const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}`
// Optionally use a prefix with readable bindings
let readableBinding = component._instanceName
@ -267,17 +257,9 @@ const getUserBindings = () => {
const safeUser = makePropSafe("user")
keys.forEach(key => {
const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
bindings.push({
type: "context",
runtimeBinding: `${safeUser}.${makePropSafe(runtimeBoundKey)}`,
runtimeBinding: `${safeUser}.${makePropSafe(key)}`,
readableBinding: `Current User.${key}`,
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties

View file

@ -45,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
state: false,
customThemes: false,
devicePreview: false,
messagePassing: false,
},
currentFrontEndType: "none",
selectedScreenId: "",

View file

@ -202,7 +202,7 @@
display: inline-block;
}
.block {
width: 360px;
width: 480px;
font-size: 16px;
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);

View file

@ -234,7 +234,8 @@
<Editor
mode="javascript"
on:change={e => {
onChange(e, key)
// need to pass without the value inside
onChange({ detail: e.detail.value }, key)
inputData[key] = e.detail.value
}}
value={inputData[key]}

View file

@ -18,6 +18,11 @@
FIELDS,
AUTO_COLUMN_SUB_TYPES,
RelationshipTypes,
ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES,
SWITCHABLE_TYPES,
} from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import { notifications } from "@budibase/bbui"
@ -92,6 +97,9 @@
opt.type === table.type &&
table.sourceId === opt.sourceId
)
$: typeEnabled =
!originalName ||
(originalName && SWITCHABLE_TYPES.indexOf(field.type) !== -1)
async function saveColumn() {
if (field.type === AUTO_TYPE) {
@ -204,7 +212,14 @@
}
function getAllowedTypes() {
if (!external) {
if (originalName && ALLOWABLE_STRING_TYPES.indexOf(field.type) !== -1) {
return ALLOWABLE_STRING_OPTIONS
} else if (
originalName &&
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
) {
return ALLOWABLE_NUMBER_OPTIONS
} else if (!external) {
return [
...Object.values(fieldDefinitions),
{ name: "Auto Column", type: AUTO_TYPE },
@ -259,7 +274,7 @@
/>
<Select
disabled={originalName}
disabled={!typeEnabled}
label="Type"
bind:value={field.type}
on:change={handleTypeChange}

View file

@ -8,6 +8,7 @@
export let onOk = undefined
export let onCancel = undefined
export let warning = true
export let disabled
let modal
@ -26,6 +27,7 @@
confirmText={okText}
{cancelText}
{warning}
{disabled}
>
<Body size="S">
{body}

View file

@ -17,6 +17,7 @@
export let disabled = false
export let options
export let allowJS = true
export let appendBindingsAsOptions = true
const dispatch = createEventDispatcher()
let bindingDrawer
@ -24,15 +25,30 @@
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
const onChange = (value, optionPicked) => {
// Add HBS braces if picking binding
if (optionPicked && !options?.includes(value)) {
value = `{{ ${value} }}`
}
dispatch("change", readableToRuntimeBinding(bindings, value))
}
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
if (!appendBindingsAsOptions) {
return options
}
return []
.concat(options || [])
.concat(bindings?.map(binding => binding.readableBinding) || [])
}
</script>
<div class="control">
@ -40,12 +56,17 @@
{label}
{disabled}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
on:type={e => onChange(e.detail, false)}
on:pick={e => onChange(e.detail, true)}
{placeholder}
{options}
options={allOptions}
/>
{#if !disabled}
<div class="icon" on:click={bindingDrawer.show}>
<div
class="icon"
on:click={bindingDrawer.show}
data-cy="text-binding-button"
>
<Icon size="S" name="FlashOn" />
</div>
{/if}

View file

@ -1,9 +1,16 @@
<script>
import { Icon, Modal, notifications, ModalContent } from "@budibase/bbui"
import {
Icon,
Input,
Modal,
notifications,
ModalContent,
} from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
let revertModal
let appName
$: appId = $store.appId
@ -33,10 +40,17 @@
<Icon name="Revert" hoverable on:click={revertModal.show} />
<Modal bind:this={revertModal}>
<ModalContent title="Revert Changes" confirmText="Revert" onConfirm={revert}>
<ModalContent
title="Revert Changes"
confirmText="Revert"
onConfirm={revert}
disabled={appName !== $store.name}
>
<span
>The changes you have made will be deleted and the application reverted
back to its production state.</span
>
<span>Please enter your app name to continue.</span>
<Input bind:value={appName} />
</ModalContent>
</Modal>

View file

@ -69,6 +69,7 @@
theme: $store.theme,
customTheme: $store.customTheme,
previewDevice: $store.previewDevice,
messagePassing: $store.clientFeatures.messagePassing
}
// Saving pages and screens to the DB causes them to have _revs.
@ -94,10 +95,12 @@
const handlers = {
[MessageTypes.READY]: () => {
// Initialise the app when mounted
if ($store.clientFeatures.messagePassing) {
if (!loading) return
}
// Display preview immediately if the intelligent loading feature
// is not supported
if (!loading) return
if (!$store.clientFeatures.intelligentLoading) {
loading = false
}
@ -117,17 +120,34 @@
onMount(() => {
window.addEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.addEventListener("ready", () => {
receiveMessage({ data: { type: MessageTypes.READY }})
}, { once: true })
iframe.contentWindow.addEventListener("error", event => {
receiveMessage({ data: { type: MessageTypes.ERROR, error: event.detail }})
}, { once: true })
// Add listener for events sent by client library in preview
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
}
})
// Remove all iframe event listeners on component destroy
onDestroy(() => {
if (iframe.contentWindow) {
window.removeEventListener("message", receiveMessage) //
window.removeEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.removeEventListener("keydown", handleKeydownEvent)
}
}
})
const handleBudibaseEvent = event => {
const { type, data } = event.data
const { type, data } = event.data || event.detail
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") {
@ -164,7 +184,7 @@
}
const handleKeydownEvent = event => {
const { key } = event.data
const { key } = event.data || event
if (
(key === "Delete" || key === "Backspace") &&
selectedComponentId &&

View file

@ -3,8 +3,8 @@
"name": "Blocks",
"icon": "Article",
"children": [
"tablewithsearch",
"cardlistwithsearch"
"tableblock",
"cardsblock"
]
},
"section",

View file

@ -97,6 +97,7 @@ export default `
window.addEventListener("keydown", evt => {
window.parent.postMessage({ type: "keydown", key: event.key })
})
window.parent.postMessage({ type: "ready" })
</script>
</head>

View file

@ -78,7 +78,6 @@
<DetailSummary name={section.name} collapsible={false}>
{#if idx === 0 && !componentInstance._component.endsWith("/layout")}
<PropertyControl
bindable={false}
control={Input}
label="Name"
key="_instanceName"

View file

@ -13,9 +13,10 @@
import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
import { selectedComponent, store } from "builderStore"
import { selectedComponent } from "builderStore"
import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
import { getComponentSettings } from "builderStore/storeUtils"
export let conditions = []
export let bindings = []
@ -55,15 +56,11 @@
]
let dragDisabled = true
$: definition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: settings = (definition?.settings ?? []).map(setting => {
return {
label: setting.label,
value: setting.key,
}
})
$: settings = getComponentSettings($selectedComponent?._component)
$: settingOptions = settings.map(setting => ({
label: setting.label,
value: setting.key,
}))
$: conditions.forEach(link => {
if (!link.id) {
link.id = generate()
@ -71,9 +68,7 @@
})
const getSettingDefinition = key => {
return definition?.settings?.find(setting => {
return setting.key === key
})
return settings.find(setting => setting.key === key)
}
const getComponentForSetting = key => {
@ -175,7 +170,10 @@
bind:value={condition.action}
/>
{#if condition.action === "update"}
<Select options={settings} bind:value={condition.setting} />
<Select
options={settingOptions}
bind:value={condition.setting}
/>
<div>TO</div>
{#if getSettingDefinition(condition.setting)}
<PropertyControl

View file

@ -1,15 +0,0 @@
<script>
import { Input } from "@budibase/bbui"
import { isJSBinding } from "@budibase/string-templates"
export let value
$: isJS = isJSBinding(value)
</script>
<Input
{...$$props}
value={isJS ? "(JavaScript function)" : value}
readonly={isJS}
on:change
/>

View file

@ -1,11 +1,9 @@
<script>
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
import { Label } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "helpers"
export let label = ""
export let bindable = true
@ -20,10 +18,6 @@
export let componentBindings = []
export let nested = false
let bindingDrawer
let anchor
let valid
$: allBindings = getAllBindings(bindings, componentBindings, nested)
$: safeValue = getSafeValue(value, props.defaultValue, allBindings)
$: tempValue = safeValue
@ -33,12 +27,7 @@
if (!nested) {
return bindings
}
return [...(bindings || []), ...(componentBindings || [])]
}
const handleClose = () => {
handleChange(tempValue)
bindingDrawer.hide()
return [...(componentBindings || []), ...(bindings || [])]
}
// Handle a value change of any type
@ -74,7 +63,7 @@
}
</script>
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
<div class="property-control" data-cy={`setting-${key}`}>
{#if type !== "boolean" && label}
<div class="label">
<Label>{label}</Label>
@ -94,31 +83,6 @@
{type}
{...props}
/>
{#if bindable && !key.startsWith("_") && type === "text"}
<div
class="icon"
data-cy={`${key}-binding-button`}
on:click={bindingDrawer.show}
>
<Icon size="S" name="FlashOn" />
</div>
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}>
Save
</Button>
<BindingPanel
slot="body"
bind:valid
value={safeValue}
on:change={e => (tempValue = e.detail)}
bindableProperties={allBindings}
allowJS
/>
</Drawer>
{/if}
</div>
</div>
@ -130,40 +94,10 @@
justify-content: flex-start;
align-items: stretch;
}
.label {
padding-bottom: var(--spectrum-global-dimension-size-65);
}
.control {
position: relative;
}
.icon {
right: 1px;
top: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
</style>

View file

@ -10,4 +10,10 @@
.filter(x => x != null)
</script>
<DrawerBindableCombobox {value} {bindings} on:change options={urlOptions} />
<DrawerBindableCombobox
{value}
{bindings}
on:change
options={urlOptions}
appendBindingsAsOptions={false}
/>

View file

@ -15,10 +15,10 @@ import URLSelect from "./URLSelect.svelte"
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./FormFieldSelect.svelte"
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
import Input from "./Input.svelte"
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
const componentMap = {
text: Input,
text: DrawerBindableCombobox,
select: Select,
dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,

View file

@ -54,7 +54,6 @@
<DetailSummary name="Screen" collapsible={false}>
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}

View file

@ -30,7 +30,6 @@
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
<div style="grid-column: {prop.column || 'auto'}">
<PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
control={prop.control}
key={prop.key}

View file

@ -9,7 +9,6 @@ export const margin = {
label: "Top",
key: "margin-top",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -30,7 +29,6 @@ export const margin = {
label: "Right",
key: "margin-right",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -51,7 +49,6 @@ export const margin = {
label: "Bottom",
key: "margin-bottom",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -72,7 +69,6 @@ export const margin = {
label: "Left",
key: "margin-left",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -100,7 +96,6 @@ export const padding = {
label: "Top",
key: "padding-top",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -121,7 +116,6 @@ export const padding = {
label: "Right",
key: "padding-right",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -142,7 +136,6 @@ export const padding = {
label: "Bottom",
key: "padding-bottom",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -163,7 +156,6 @@ export const padding = {
label: "Left",
key: "padding-left",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },

View file

@ -157,6 +157,11 @@
}
return title
}
async function onCancel() {
template = null
await auth.setInitInfo({})
}
</script>
{#if showTemplateSelection}
@ -186,7 +191,7 @@
title={getModalTitle()}
confirmText={template?.fromFile ? "Import app" : "Create app"}
onConfirm={createNewApp}
onCancel={inline ? () => (template = null) : null}
onCancel={inline ? onCancel : null}
cancelText={inline ? "Back" : undefined}
showCloseIcon={!inline}
disabled={!valid}

View file

@ -37,33 +37,33 @@
<p class="detail">{template?.category?.toUpperCase()}</p>
</div>
{/each}
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
<div
class="template import"
on:click={() => onSelect(null, { useImport: true })}
>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Import an app</Heading>
<p class="detail">BLANK</p>
</div>
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
<div
class="template import"
on:click={() => onSelect(null, { useImport: true })}
>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Import an app</Heading>
<p class="detail">BLANK</p>
</div>
</Layout>
<style>

View file

@ -138,3 +138,19 @@ export const RelationshipTypes = {
ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one",
}
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
opt => opt.type
)
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
opt => opt.type
)
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
ALLOWABLE_STRING_TYPES
)

View file

@ -28,9 +28,13 @@
}
if (user && user.tenantId) {
// no tenant in the url - send to account portal to fix this
if (!urlTenantId) {
window.location.href = $admin.accountPortalUrl
// redirect to correct tenantId subdomain
if (!window.location.host.includes("localhost")) {
let redirectUrl = window.location.href
redirectUrl = redirectUrl.replace("://", `://${user.tenantId}.`)
window.location.href = redirectUrl
}
return
}

View file

@ -6,6 +6,7 @@
ActionButton,
ActionGroup,
ButtonGroup,
Input,
Select,
Modal,
Page,
@ -36,6 +37,7 @@
let loaded = false
let searchTerm = ""
let cloud = $admin.cloud
let appName = ""
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(app =>
@ -296,8 +298,12 @@
title="Confirm deletion"
okText="Delete app"
onOk={confirmDeleteApp}
disabled={appName !== selectedApp?.name}
>
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
<p>Please enter the app name below to confirm.</p>
<Input bind:value={appName} data-cy="delete-app-confirmation" />
</ConfirmDialog>
<ConfirmDialog
bind:this={unpublishModal}

View file

@ -2,6 +2,7 @@ import { writable, get } from "svelte/store"
import { views, queries, datasources } from "./"
import { cloneDeep } from "lodash/fp"
import api from "builderStore/api"
import { SWITCHABLE_TYPES } from "../../constants/backend"
export function createTablesStore() {
const store = writable({})
@ -47,7 +48,11 @@ export function createTablesStore() {
const field = updatedTable.schema[key]
const oldField = oldTable?.schema[key]
// if the type has changed then revert back to the old field
if (oldField != null && oldField?.type !== field.type) {
if (
oldField != null &&
oldField?.type !== field.type &&
SWITCHABLE_TYPES.indexOf(oldField?.type) === -1
) {
updatedTable.schema[key] = oldField
}
// field has been renamed

View file

@ -57,11 +57,11 @@ export function createAuthStore() {
analytics.showChat({
email: user.email,
created_at: (user.createdAt || Date.now()) / 1000,
name: user.name,
name: user.account?.name,
user_id: user._id,
tenant: user.tenantId,
"Company size": user.size,
"Job role": user.profession,
"Company size": user.account?.size,
"Job role": user.account?.profession,
})
})
}
@ -80,16 +80,30 @@ export function createAuthStore() {
}
}
async function setInitInfo(info) {
await api.post(`/api/global/auth/init`, info)
auth.update(store => {
store.initInfo = info
return store
})
return info
}
async function getInitInfo() {
const response = await api.get(`/api/global/auth/init`)
const json = response.json()
auth.update(store => {
store.initInfo = json
return store
})
return json
}
return {
subscribe: store.subscribe,
setOrganisation: setOrganisation,
getInitInfo: async () => {
const response = await api.get(`/api/global/auth/init`)
return await response.json()
},
setInitInfo: async info => {
await api.post(`/api/global/auth/init`, info)
},
setOrganisation,
getInitInfo,
setInitInfo,
checkQueryString: async () => {
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("tenantId")) {
@ -129,6 +143,7 @@ export function createAuthStore() {
throw "Unable to create logout"
}
await response.json()
await setInitInfo({})
setUser(null)
},
updateSelf: async fields => {

View file

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

View file

@ -5,7 +5,8 @@
"deviceAwareness": true,
"state": true,
"customThemes": true,
"devicePreview": true
"devicePreview": true,
"messagePassing": true
},
"layout": {
"name": "Layout",
@ -2595,6 +2596,11 @@
"key": "linkURL",
"label": "Link URL"
},
{
"type": "boolean",
"key": "linkPeek",
"label": "Open link in modal"
},
{
"type": "boolean",
"key": "horizontal",
@ -2617,9 +2623,9 @@
}
]
},
"tablewithsearch": {
"tableblock": {
"block": true,
"name": "Table with search",
"name": "Table block",
"icon": "Table",
"styles": ["size"],
"info": "Only the first 3 search columns will be used.",
@ -2730,18 +2736,23 @@
{
"type": "boolean",
"key": "showTitleButton",
"label": "Show button",
"label": "Show link button",
"defaultValue": false
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
},
{
"type": "text",
"key": "titleButtonText",
"label": "Button text"
},
{
"type": "event",
"label": "Button action",
"key": "titleButtonOnClick"
"type": "url",
"label": "Button link",
"key": "titleButtonURL"
}
]
},
@ -2759,9 +2770,9 @@
}
]
},
"cardlistwithsearch": {
"cardsblock": {
"block": true,
"name": "Card list with search",
"name": "Cards block",
"icon": "Table",
"styles": ["size"],
"info": "Only the first 3 search columns will be used.",
@ -2838,7 +2849,22 @@
"key": "cardImageURL",
"label": "Image URL",
"nested": true
},
{
"type": "boolean",
"key": "linkCardTitle",
"label": "Link card title"
},
{
"type": "boolean",
"key": "cardPeek",
"label": "Open link in modal"
},
{
"type": "url",
"label": "Link screen",
"key": "cardURL",
"nested": true
},
{
"type": "boolean",
@ -2855,7 +2881,6 @@
"key": "cardButtonText",
"label": "Button text",
"nested": true
},
{
"type": "event",
@ -2872,7 +2897,12 @@
{
"type": "boolean",
"key": "showTitleButton",
"label": "Show button"
"label": "Show link button"
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
},
{
"type": "text",
@ -2880,9 +2910,21 @@
"label": "Button text"
},
{
"type": "event",
"label": "Button action",
"key": "titleButtonOnClick"
"type": "url",
"label": "Button link",
"key": "titleButtonURL"
}
]
},
{
"section": true,
"name": "Advanced",
"settings": [
{
"type": "field",
"label": "ID column for linking (appended to URL)",
"key": "linkColumn",
"placeholder": "Default"
}
]
}

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"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": "^0.9.180-alpha.1",
"@budibase/bbui": "^0.9.184",
"@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.180-alpha.1",
"@budibase/string-templates": "^0.9.184",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"

View file

@ -108,6 +108,8 @@ export const deleteRows = async ({ tableId, rows }) => {
/**
* Enriches rows which contain certain field types so that they can
* be properly displayed.
* The ability to create these bindings has been removed, but they will still
* exist in client apps to support backwards compatibility.
*/
export const enrichRows = async (rows, tableId) => {
if (!Array.isArray(rows)) {

View file

@ -8,15 +8,22 @@
export let description
export let imageURL
export let linkURL
export let linkPeek
export let horizontal
export let showButton
export let buttonText
export let buttonOnClick
const { styleable, linkable } = getContext("sdk")
const { styleable, routeStore } = getContext("sdk")
const component = getContext("component")
$: external = linkURL && !linkURL.startsWith("/")
const handleLink = e => {
if (!linkURL) {
return
}
e.preventDefault()
routeStore.actions.navigate(linkURL, linkPeek)
}
</script>
<div
@ -37,16 +44,10 @@
<div class="spectrum-Card-header">
<div
class="spectrum-Card-title spectrum-Heading spectrum-Heading--sizeXS"
on:click={handleLink}
class:link={linkURL}
>
{#if linkURL}
{#if external}
<a href={linkURL}>{title || "Card Title"}</a>
{:else}
<a use:linkable href={linkURL}>{title || "Card Title"}</a>
{/if}
{:else}
{title || "Card Title"}
{/if}
{title || "Card Title"}
</div>
</div>
{#if subtitle}
@ -88,11 +89,12 @@
.spectrum-Card-container {
padding: var(--spectrum-global-dimension-size-50) 0;
}
.spectrum-Card-title :global(a) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
.spectrum-Card-title.link {
transition: color 130ms ease-in-out;
}
.spectrum-Card-title.link:hover {
cursor: pointer;
color: var(--spectrum-link-primary-m-text-color-hover);
}
.spectrum-Card-subtitle {
text-overflow: ellipsis;
@ -103,14 +105,6 @@
word-wrap: anywhere;
white-space: pre-wrap;
}
a {
transition: color 130ms ease-in-out;
color: var(--spectrum-alias-text-color);
}
a:hover {
color: var(--spectrum-link-primary-m-text-color-hover);
}
.horizontal .spectrum-Card-coverPhoto {
flex: 0 0 160px;
height: auto;

View file

@ -14,15 +14,20 @@
export let limit
export let showTitleButton
export let titleButtonText
export let titleButtonOnClick
export let titleButtonURL
export let titleButtonPeek
export let cardTitle
export let cardSubtitle
export let cardDescription
export let cardImageURL
export let linkCardTitle
export let cardURL
export let cardPeek
export let cardHorizontal
export let showCardButton
export let cardButtonText
export let cardButtonOnClick
export let linkColumn
const { API, styleable } = getContext("sdk")
const context = getContext("context")
@ -37,11 +42,27 @@
let formId
let dataProviderId
let repeaterId
let schema
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
$: cardWidth = cardHorizontal ? 420 : 300
$: fullCardURL = buildFullCardUrl(
linkCardTitle,
cardURL,
repeaterId,
linkColumn
)
$: titleButtonAction = [
{
"##eventHandlerType": "Navigate To",
parameters: {
peek: titleButtonPeek,
url: titleButtonURL,
},
},
]
// Enrich the default filter with the specified search fields
const enrichFilter = (filter, columns, formId) => {
@ -49,7 +70,7 @@
columns?.forEach(column => {
enrichedFilter.push({
field: column.name,
operator: "equal",
operator: column.type === "string" ? "string" : "equal",
type: "string",
valueType: "Binding",
value: `{{ [${formId}].[${column.name}] }}`,
@ -68,12 +89,23 @@
enrichedColumns.push({
name: column,
componentType,
type: schemaType,
})
}
})
return enrichedColumns.slice(0, 3)
}
// Builds a full details page URL for the card title
const buildFullCardUrl = (link, url, repeaterId, linkColumn) => {
if (!link || !url || !repeaterId) {
return null
}
const col = linkColumn || "_id"
const split = url.split("/:")
return `${split[0]}/{{ [${repeaterId}].[${col}] }}`
}
// Load the datasource schema on mount so we can determine column types
onMount(async () => {
if (dataSource) {
@ -113,7 +145,7 @@
<BlockComponent
type="button"
props={{
onClick: titleButtonOnClick,
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
@ -136,6 +168,7 @@
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
props={{
dataProvider: `{{ literal [${dataProviderId}] }}`,
@ -161,6 +194,8 @@
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{
width: "auto",

View file

@ -22,7 +22,8 @@
export let linkPeek
export let showTitleButton
export let titleButtonText
export let titleButtonOnClick
export let titleButtonURL
export let titleButtonPeek
const { API, styleable } = getContext("sdk")
const context = getContext("context")
@ -41,6 +42,15 @@
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
$: titleButtonAction = [
{
"##eventHandlerType": "Navigate To",
parameters: {
peek: titleButtonPeek,
url: titleButtonURL,
},
},
]
// Enrich the default filter with the specified search fields
const enrichFilter = (filter, columns, formId) => {
@ -48,7 +58,7 @@
columns?.forEach(column => {
enrichedFilter.push({
field: column.name,
operator: "equal",
operator: column.type === "string" ? "string" : "equal",
type: "string",
valueType: "Binding",
value: `{{ [${formId}].[${column.name}] }}`,
@ -67,6 +77,7 @@
enrichedColumns.push({
name: column,
componentType,
type: schemaType,
})
}
})
@ -112,7 +123,7 @@
<BlockComponent
type="button"
props={{
onClick: titleButtonOnClick,
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}

View file

@ -1,2 +1,2 @@
export { default as tablewithsearch } from "./TableWithSearch.svelte"
export { default as cardlistwithsearch } from "./CardListWithSearch.svelte"
export { default as tableblock } from "./TableBlock.svelte"
export { default as cardsblock } from "./CardsBlock.svelte"

View file

@ -4,6 +4,9 @@ import { builderStore } from "stores"
export const linkable = (node, href) => {
if (get(builderStore).inBuilder) {
node.onclick = e => {
e.preventDefault()
}
return
}
link(node, href)

View file

@ -21,15 +21,18 @@ module PgMock {
function Pool() {
}
const on = jest.fn()
Pool.prototype.query = query
Pool.prototype.connect = jest.fn(() => {
// @ts-ignore
return new Client()
})
Pool.prototype.on = on
pg.Client = Client
pg.Pool = Pool
pg.queryMock = query
pg.on = on
module.exports = pg
}

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -68,9 +68,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.180-alpha.1",
"@budibase/client": "^0.9.180-alpha.1",
"@budibase/string-templates": "^0.9.180-alpha.1",
"@budibase/auth": "^0.9.184",
"@budibase/client": "^0.9.184",
"@budibase/string-templates": "^0.9.184",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",

View file

@ -198,7 +198,7 @@ exports.fetchAppPackage = async ctx => {
application,
screens,
layouts,
clientLibPath: clientLibraryPath(ctx.params.appId),
clientLibPath: clientLibraryPath(ctx.params.appId, application.version),
}
}
@ -324,7 +324,7 @@ exports.delete = async ctx => {
ctx.body = result
}
exports.sync = async ctx => {
exports.sync = async (ctx, next) => {
const appId = ctx.params.appId
if (!isDevAppID(appId)) {
ctx.throw(400, "This action cannot be performed for production apps")
@ -332,6 +332,20 @@ exports.sync = async ctx => {
// replicate prod to dev
const prodAppId = getDeployedAppID(appId)
try {
const prodDb = new CouchDB(prodAppId, { skip_setup: true })
const info = await prodDb.info()
if (info.error) throw info.error
} catch (err) {
// the database doesn't exist. Don't replicate
ctx.status = 200
ctx.body = {
message: "App sync not required, app not deployed.",
}
return next()
}
const replication = new Replication({
source: prodAppId,
target: appId,

View file

@ -82,6 +82,13 @@ exports.revert = async ctx => {
const db = new CouchDB(productionAppId, { skip_setup: true })
const info = await db.info()
if (info.error) throw info.error
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
if (
!deploymentDoc.history ||
Object.keys(deploymentDoc.history).length === 0
) {
throw new Error("No deployments for app")
}
} catch (err) {
return ctx.throw(400, "App has not yet been deployed")
}

View file

@ -87,7 +87,7 @@ exports.serveApp = async function (ctx) {
title: appInfo.name,
production: env.isProd(),
appId,
clientLibPath: clientLibraryPath(appId),
clientLibPath: clientLibraryPath(appId, appInfo.version),
})
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)

View file

@ -8,6 +8,7 @@ const {
generateForeignKey,
generateJunctionTableName,
foreignKeyStructure,
hasTypeChanged,
} = require("./utils")
const {
DataSourceOperation,
@ -172,6 +173,10 @@ exports.save = async function (ctx) {
oldTable = await getTable(appId, ctx.request.body._id)
}
if (hasTypeChanged(tableToSave, oldTable)) {
ctx.throw(400, "A column type has changed.")
}
const db = new CouchDB(appId)
const datasource = await db.get(datasourceId)
const oldTables = cloneDeep(datasource.entities)

View file

@ -2,7 +2,7 @@ const CouchDB = require("../../../db")
const linkRows = require("../../../db/linkedRows")
const { getRowParams, generateTableID } = require("../../../db/utils")
const { FieldTypes } = require("../../../constants")
const { TableSaveFunctions } = require("./utils")
const { TableSaveFunctions, hasTypeChanged } = require("./utils")
exports.save = async function (ctx) {
const appId = ctx.appId
@ -21,6 +21,10 @@ exports.save = async function (ctx) {
oldTable = await db.get(ctx.request.body._id)
}
if (hasTypeChanged(tableToSave, oldTable)) {
ctx.throw(400, "A column type has changed.")
}
// saving a table is a complex operation, involving many different steps, this
// has been broken out into a utility to make it more obvious/easier to manipulate
const tableSaveFunctions = new TableSaveFunctions({

View file

@ -8,7 +8,7 @@ const {
const { isEqual } = require("lodash/fp")
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants")
const { inputProcessing } = require("../../../utilities/rowProcessor")
const { USERS_TABLE_SCHEMA } = require("../../../constants")
const { USERS_TABLE_SCHEMA, SwitchableTypes } = require("../../../constants")
const {
isExternalTable,
breakExternalTableId,
@ -335,4 +335,21 @@ exports.foreignKeyStructure = (keyName, meta = null) => {
return structure
}
exports.hasTypeChanged = (table, oldTable) => {
if (!oldTable) {
return false
}
for (let [key, field] of Object.entries(oldTable.schema)) {
const oldType = field.type
if (!table.schema[key]) {
continue
}
const newType = table.schema[key].type
if (oldType !== newType && SwitchableTypes.indexOf(oldType) === -1) {
return true
}
}
return false
}
exports.TableSaveFunctions = TableSaveFunctions

View file

@ -45,6 +45,13 @@ exports.FieldTypes = {
INTERNAL: "internal",
}
exports.SwitchableTypes = [
exports.FieldTypes.STRING,
exports.FieldTypes.OPTIONS,
exports.FieldTypes.NUMBER,
exports.FieldTypes.BOOLEAN,
]
exports.RelationshipTypes = {
ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one",

View file

@ -30,7 +30,7 @@ function generateSchema(
// skip things that are already correct
const oldColumn = oldTable ? oldTable.schema[key] : null
if (
(oldColumn && oldColumn.type === column.type) ||
(oldColumn && oldColumn.type) ||
(primaryKey === key && !isJunction)
) {
continue

View file

@ -28,6 +28,7 @@ module PostgresModule {
database: string
user: string
password: string
schema: string
ssl?: boolean
ca?: string
rejectUnauthorized?: boolean
@ -65,6 +66,11 @@ module PostgresModule {
default: "root",
required: true,
},
schema: {
type: DatasourceFieldTypes.STRING,
default: "public",
required: true,
},
ssl: {
type: DatasourceFieldTypes.BOOLEAN,
default: false,
@ -124,8 +130,7 @@ module PostgresModule {
public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {}
COLUMNS_SQL =
"select * from information_schema.columns where not table_schema = 'information_schema' and not table_schema = 'pg_catalog'"
COLUMNS_SQL!: string
PRIMARY_KEYS_SQL = `
select tc.table_schema, tc.table_name, kc.column_name as primary_key
@ -155,6 +160,17 @@ module PostgresModule {
}
this.client = this.pool
this.setSchema()
}
setSchema() {
if (!this.config.schema) {
this.config.schema = 'public'
}
this.client.on('connect', (client: any) => {
client.query(`SET search_path TO ${this.config.schema}`);
});
this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`
}
/**

View file

@ -15,6 +15,10 @@ describe("Postgres Integration", () => {
config = new TestConfiguration()
})
it("calls the connection callback", async () => {
expect(pg.on).toHaveBeenCalledWith('connect', expect.anything())
})
it("calls the create method with the correct params", async () => {
const sql = "insert into users (name, age) values ('Joe', 123);"
await config.integration.create({

View file

@ -51,11 +51,16 @@ exports.objectStoreUrl = () => {
* @return {string} The URL to be inserted into appPackage response or server rendered
* app index file.
*/
exports.clientLibraryPath = appId => {
exports.clientLibraryPath = (appId, version) => {
if (env.isProd()) {
return `${exports.objectStoreUrl()}/${sanitizeKey(
let url = `${exports.objectStoreUrl()}/${sanitizeKey(
appId
)}/budibase-client.js`
// append app version to bust the cache
if (version) {
url += `?v=${version}`
}
return url
} else {
return `/api/assets/client`
}

View file

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

View file

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

View file

@ -27,7 +27,7 @@ exports.syncUserInApps = async userId => {
"POST",
{}
)
if (response.status !== 200) {
if (response && response.status !== 200) {
throw "Unable to sync user."
}
}