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 ad68b895e1
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", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "0.9.180-alpha.1", "version": "0.9.184",
"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": "0.9.180-alpha.1", "version": "0.9.184",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

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

View File

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

View File

@ -11,7 +11,8 @@ it("should rename an unpublished application", () => {
renameApp(appRename) renameApp(appRename)
cy.searchForApplication(appRename) cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1) cy.get(".appGrid").find(".wrapper").should("have.length", 1)
}) cy.deleteApp(appRename)
})
xit("Should rename a published application", () => { xit("Should rename a published application", () => {
// It is not possible to 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.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(1000) cy.wait(1000)
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`) cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
console.log(val)
if (val.length > 0) { if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click() cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
cy.contains("Delete").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", () => { Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests" const appName = "Cypress Tests"
cy.deleteApp() cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.") cy.createApp(appName, "This app is used for Cypress testing.")
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.9.180-alpha.1", "version": "0.9.184",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.180-alpha.1", "@budibase/bbui": "^0.9.184",
"@budibase/client": "^0.9.180-alpha.1", "@budibase/client": "^0.9.184",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.180-alpha.1", "@budibase/string-templates": "^0.9.184",
"@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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@
export let disabled = false export let disabled = false
export let options export let options
export let allowJS = true export let allowJS = true
export let appendBindingsAsOptions = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
@ -24,15 +25,30 @@
$: readableValue = runtimeToReadableBinding(bindings, value) $: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue $: tempValue = readableValue
$: isJS = isJSBinding(value) $: isJS = isJSBinding(value)
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
const handleClose = () => { const handleClose = () => {
onChange(tempValue) onChange(tempValue)
bindingDrawer.hide() 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)) dispatch("change", readableToRuntimeBinding(bindings, value))
} }
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
if (!appendBindingsAsOptions) {
return options
}
return []
.concat(options || [])
.concat(bindings?.map(binding => binding.readableBinding) || [])
}
</script> </script>
<div class="control"> <div class="control">
@ -40,12 +56,17 @@
{label} {label}
{disabled} {disabled}
value={isJS ? "(JavaScript function)" : readableValue} 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} {placeholder}
{options} options={allOptions}
/> />
{#if !disabled} {#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" /> <Icon size="S" name="FlashOn" />
</div> </div>
{/if} {/if}

View File

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

View File

@ -69,6 +69,7 @@
theme: $store.theme, theme: $store.theme,
customTheme: $store.customTheme, customTheme: $store.customTheme,
previewDevice: $store.previewDevice, previewDevice: $store.previewDevice,
messagePassing: $store.clientFeatures.messagePassing
} }
// Saving pages and screens to the DB causes them to have _revs. // Saving pages and screens to the DB causes them to have _revs.
@ -94,10 +95,12 @@
const handlers = { const handlers = {
[MessageTypes.READY]: () => { [MessageTypes.READY]: () => {
// Initialise the app when mounted // Initialise the app when mounted
if ($store.clientFeatures.messagePassing) {
if (!loading) return
}
// Display preview immediately if the intelligent loading feature // Display preview immediately if the intelligent loading feature
// is not supported // is not supported
if (!loading) return
if (!$store.clientFeatures.intelligentLoading) { if (!$store.clientFeatures.intelligentLoading) {
loading = false loading = false
} }
@ -117,17 +120,34 @@
onMount(() => { onMount(() => {
window.addEventListener("message", receiveMessage) 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 // Remove all iframe event listeners on component destroy
onDestroy(() => { onDestroy(() => {
if (iframe.contentWindow) { 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 handleBudibaseEvent = event => {
const { type, data } = event.data const { type, data } = event.data || event.detail
if (type === "select-component" && data.id) { if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id }) store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") { } else if (type === "update-prop") {
@ -164,7 +184,7 @@
} }
const handleKeydownEvent = event => { const handleKeydownEvent = event => {
const { key } = event.data const { key } = event.data || event
if ( if (
(key === "Delete" || key === "Backspace") && (key === "Delete" || key === "Backspace") &&
selectedComponentId && selectedComponentId &&

View File

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

View File

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

View File

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

View File

@ -13,9 +13,10 @@
import { generate } from "shortid" import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene" import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
import { selectedComponent, store } from "builderStore" import { selectedComponent } from "builderStore"
import { getComponentForSettingType } from "./componentSettings" import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
import { getComponentSettings } from "builderStore/storeUtils"
export let conditions = [] export let conditions = []
export let bindings = [] export let bindings = []
@ -55,15 +56,11 @@
] ]
let dragDisabled = true let dragDisabled = true
$: definition = store.actions.components.getDefinition( $: settings = getComponentSettings($selectedComponent?._component)
$selectedComponent?._component $: settingOptions = settings.map(setting => ({
) label: setting.label,
$: settings = (definition?.settings ?? []).map(setting => { value: setting.key,
return { }))
label: setting.label,
value: setting.key,
}
})
$: conditions.forEach(link => { $: conditions.forEach(link => {
if (!link.id) { if (!link.id) {
link.id = generate() link.id = generate()
@ -71,9 +68,7 @@
}) })
const getSettingDefinition = key => { const getSettingDefinition = key => {
return definition?.settings?.find(setting => { return settings.find(setting => setting.key === key)
return setting.key === key
})
} }
const getComponentForSetting = key => { const getComponentForSetting = key => {
@ -175,7 +170,10 @@
bind:value={condition.action} bind:value={condition.action}
/> />
{#if condition.action === "update"} {#if condition.action === "update"}
<Select options={settings} bind:value={condition.setting} /> <Select
options={settingOptions}
bind:value={condition.setting}
/>
<div>TO</div> <div>TO</div>
{#if getSettingDefinition(condition.setting)} {#if getSettingDefinition(condition.setting)}
<PropertyControl <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> <script>
import { Button, Icon, Drawer, Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "helpers"
export let label = "" export let label = ""
export let bindable = true export let bindable = true
@ -20,10 +18,6 @@
export let componentBindings = [] export let componentBindings = []
export let nested = false export let nested = false
let bindingDrawer
let anchor
let valid
$: allBindings = getAllBindings(bindings, componentBindings, nested) $: allBindings = getAllBindings(bindings, componentBindings, nested)
$: safeValue = getSafeValue(value, props.defaultValue, allBindings) $: safeValue = getSafeValue(value, props.defaultValue, allBindings)
$: tempValue = safeValue $: tempValue = safeValue
@ -33,12 +27,7 @@
if (!nested) { if (!nested) {
return bindings return bindings
} }
return [...(bindings || []), ...(componentBindings || [])] return [...(componentBindings || []), ...(bindings || [])]
}
const handleClose = () => {
handleChange(tempValue)
bindingDrawer.hide()
} }
// Handle a value change of any type // Handle a value change of any type
@ -74,7 +63,7 @@
} }
</script> </script>
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}> <div class="property-control" data-cy={`setting-${key}`}>
{#if type !== "boolean" && label} {#if type !== "boolean" && label}
<div class="label"> <div class="label">
<Label>{label}</Label> <Label>{label}</Label>
@ -94,31 +83,6 @@
{type} {type}
{...props} {...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>
</div> </div>
@ -130,40 +94,10 @@
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
} }
.label { .label {
padding-bottom: var(--spectrum-global-dimension-size-65); padding-bottom: var(--spectrum-global-dimension-size-65);
} }
.control { .control {
position: relative; 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> </style>

View File

@ -10,4 +10,10 @@
.filter(x => x != null) .filter(x => x != null)
</script> </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 OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./FormFieldSelect.svelte" import FormFieldSelect from "./FormFieldSelect.svelte"
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte" import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
import Input from "./Input.svelte" import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
const componentMap = { const componentMap = {
text: Input, text: DrawerBindableCombobox,
select: Select, select: Select,
dataSource: DataSourceSelect, dataSource: DataSourceSelect,
dataProvider: DataProviderSelect, dataProvider: DataProviderSelect,

View File

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

View File

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

View File

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

View File

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

View File

@ -37,33 +37,33 @@
<p class="detail">{template?.category?.toUpperCase()}</p> <p class="detail">{template?.category?.toUpperCase()}</p>
</div> </div>
{/each} {/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> </div>
{:catch err} {:catch err}
<h1 style="color:red">{err}</h1> <h1 style="color:red">{err}</h1>
{/await} {/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> </Layout>
<style> <style>

View File

@ -138,3 +138,19 @@ export const RelationshipTypes = {
ONE_TO_MANY: "one-to-many", ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one", 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) { if (user && user.tenantId) {
// no tenant in the url - send to account portal to fix this
if (!urlTenantId) { 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 return
} }

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.9.180-alpha.1", "version": "0.9.184",
"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

@ -5,7 +5,8 @@
"deviceAwareness": true, "deviceAwareness": true,
"state": true, "state": true,
"customThemes": true, "customThemes": true,
"devicePreview": true "devicePreview": true,
"messagePassing": true
}, },
"layout": { "layout": {
"name": "Layout", "name": "Layout",
@ -2595,6 +2596,11 @@
"key": "linkURL", "key": "linkURL",
"label": "Link URL" "label": "Link URL"
}, },
{
"type": "boolean",
"key": "linkPeek",
"label": "Open link in modal"
},
{ {
"type": "boolean", "type": "boolean",
"key": "horizontal", "key": "horizontal",
@ -2617,9 +2623,9 @@
} }
] ]
}, },
"tablewithsearch": { "tableblock": {
"block": true, "block": true,
"name": "Table with search", "name": "Table block",
"icon": "Table", "icon": "Table",
"styles": ["size"], "styles": ["size"],
"info": "Only the first 3 search columns will be used.", "info": "Only the first 3 search columns will be used.",
@ -2730,18 +2736,23 @@
{ {
"type": "boolean", "type": "boolean",
"key": "showTitleButton", "key": "showTitleButton",
"label": "Show button", "label": "Show link button",
"defaultValue": false "defaultValue": false
}, },
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
},
{ {
"type": "text", "type": "text",
"key": "titleButtonText", "key": "titleButtonText",
"label": "Button text" "label": "Button text"
}, },
{ {
"type": "event", "type": "url",
"label": "Button action", "label": "Button link",
"key": "titleButtonOnClick" "key": "titleButtonURL"
} }
] ]
}, },
@ -2759,9 +2770,9 @@
} }
] ]
}, },
"cardlistwithsearch": { "cardsblock": {
"block": true, "block": true,
"name": "Card list with search", "name": "Cards block",
"icon": "Table", "icon": "Table",
"styles": ["size"], "styles": ["size"],
"info": "Only the first 3 search columns will be used.", "info": "Only the first 3 search columns will be used.",
@ -2838,7 +2849,22 @@
"key": "cardImageURL", "key": "cardImageURL",
"label": "Image URL", "label": "Image URL",
"nested": true "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", "type": "boolean",
@ -2855,7 +2881,6 @@
"key": "cardButtonText", "key": "cardButtonText",
"label": "Button text", "label": "Button text",
"nested": true "nested": true
}, },
{ {
"type": "event", "type": "event",
@ -2872,7 +2897,12 @@
{ {
"type": "boolean", "type": "boolean",
"key": "showTitleButton", "key": "showTitleButton",
"label": "Show button" "label": "Show link button"
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
}, },
{ {
"type": "text", "type": "text",
@ -2880,9 +2910,21 @@
"label": "Button text" "label": "Button text"
}, },
{ {
"type": "event", "type": "url",
"label": "Button action", "label": "Button link",
"key": "titleButtonOnClick" "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", "name": "@budibase/client",
"version": "0.9.180-alpha.1", "version": "0.9.184",
"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": "^0.9.180-alpha.1", "@budibase/bbui": "^0.9.184",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.180-alpha.1", "@budibase/string-templates": "^0.9.184",
"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

@ -108,6 +108,8 @@ export const deleteRows = async ({ tableId, rows }) => {
/** /**
* Enriches rows which contain certain field types so that they can * Enriches rows which contain certain field types so that they can
* be properly displayed. * 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) => { export const enrichRows = async (rows, tableId) => {
if (!Array.isArray(rows)) { if (!Array.isArray(rows)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -198,7 +198,7 @@ exports.fetchAppPackage = async ctx => {
application, application,
screens, screens,
layouts, layouts,
clientLibPath: clientLibraryPath(ctx.params.appId), clientLibPath: clientLibraryPath(ctx.params.appId, application.version),
} }
} }
@ -324,7 +324,7 @@ exports.delete = async ctx => {
ctx.body = result ctx.body = result
} }
exports.sync = async ctx => { exports.sync = async (ctx, next) => {
const appId = ctx.params.appId const appId = ctx.params.appId
if (!isDevAppID(appId)) { if (!isDevAppID(appId)) {
ctx.throw(400, "This action cannot be performed for production apps") ctx.throw(400, "This action cannot be performed for production apps")
@ -332,6 +332,20 @@ exports.sync = async ctx => {
// replicate prod to dev // replicate prod to dev
const prodAppId = getDeployedAppID(appId) 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({ const replication = new Replication({
source: prodAppId, source: prodAppId,
target: appId, target: appId,

View File

@ -82,6 +82,13 @@ exports.revert = async ctx => {
const db = new CouchDB(productionAppId, { skip_setup: true }) const db = new CouchDB(productionAppId, { skip_setup: true })
const info = await db.info() const info = await db.info()
if (info.error) throw info.error 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) { } catch (err) {
return ctx.throw(400, "App has not yet been deployed") return ctx.throw(400, "App has not yet been deployed")
} }

View File

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

View File

@ -8,6 +8,7 @@ const {
generateForeignKey, generateForeignKey,
generateJunctionTableName, generateJunctionTableName,
foreignKeyStructure, foreignKeyStructure,
hasTypeChanged,
} = require("./utils") } = require("./utils")
const { const {
DataSourceOperation, DataSourceOperation,
@ -172,6 +173,10 @@ exports.save = async function (ctx) {
oldTable = await getTable(appId, ctx.request.body._id) 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 db = new CouchDB(appId)
const datasource = await db.get(datasourceId) const datasource = await db.get(datasourceId)
const oldTables = cloneDeep(datasource.entities) const oldTables = cloneDeep(datasource.entities)

View File

@ -2,7 +2,7 @@ const CouchDB = require("../../../db")
const linkRows = require("../../../db/linkedRows") const linkRows = require("../../../db/linkedRows")
const { getRowParams, generateTableID } = require("../../../db/utils") const { getRowParams, generateTableID } = require("../../../db/utils")
const { FieldTypes } = require("../../../constants") const { FieldTypes } = require("../../../constants")
const { TableSaveFunctions } = require("./utils") const { TableSaveFunctions, hasTypeChanged } = require("./utils")
exports.save = async function (ctx) { exports.save = async function (ctx) {
const appId = ctx.appId const appId = ctx.appId
@ -21,6 +21,10 @@ exports.save = async function (ctx) {
oldTable = await db.get(ctx.request.body._id) 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 // 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 // has been broken out into a utility to make it more obvious/easier to manipulate
const tableSaveFunctions = new TableSaveFunctions({ const tableSaveFunctions = new TableSaveFunctions({

View File

@ -8,7 +8,7 @@ const {
const { isEqual } = require("lodash/fp") const { isEqual } = require("lodash/fp")
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants") const { AutoFieldSubTypes, FieldTypes } = require("../../../constants")
const { inputProcessing } = require("../../../utilities/rowProcessor") const { inputProcessing } = require("../../../utilities/rowProcessor")
const { USERS_TABLE_SCHEMA } = require("../../../constants") const { USERS_TABLE_SCHEMA, SwitchableTypes } = require("../../../constants")
const { const {
isExternalTable, isExternalTable,
breakExternalTableId, breakExternalTableId,
@ -335,4 +335,21 @@ exports.foreignKeyStructure = (keyName, meta = null) => {
return structure 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 exports.TableSaveFunctions = TableSaveFunctions

View File

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

View File

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

View File

@ -28,6 +28,7 @@ module PostgresModule {
database: string database: string
user: string user: string
password: string password: string
schema: string
ssl?: boolean ssl?: boolean
ca?: string ca?: string
rejectUnauthorized?: boolean rejectUnauthorized?: boolean
@ -65,6 +66,11 @@ module PostgresModule {
default: "root", default: "root",
required: true, required: true,
}, },
schema: {
type: DatasourceFieldTypes.STRING,
default: "public",
required: true,
},
ssl: { ssl: {
type: DatasourceFieldTypes.BOOLEAN, type: DatasourceFieldTypes.BOOLEAN,
default: false, default: false,
@ -124,8 +130,7 @@ module PostgresModule {
public tables: Record<string, Table> = {} public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
COLUMNS_SQL = COLUMNS_SQL!: string
"select * from information_schema.columns where not table_schema = 'information_schema' and not table_schema = 'pg_catalog'"
PRIMARY_KEYS_SQL = ` PRIMARY_KEYS_SQL = `
select tc.table_schema, tc.table_name, kc.column_name as primary_key select tc.table_schema, tc.table_name, kc.column_name as primary_key
@ -155,6 +160,17 @@ module PostgresModule {
} }
this.client = this.pool 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() 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 () => { it("calls the create method with the correct params", async () => {
const sql = "insert into users (name, age) values ('Joe', 123);" const sql = "insert into users (name, age) values ('Joe', 123);"
await config.integration.create({ 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 * @return {string} The URL to be inserted into appPackage response or server rendered
* app index file. * app index file.
*/ */
exports.clientLibraryPath = appId => { exports.clientLibraryPath = (appId, version) => {
if (env.isProd()) { if (env.isProd()) {
return `${exports.objectStoreUrl()}/${sanitizeKey( let url = `${exports.objectStoreUrl()}/${sanitizeKey(
appId appId
)}/budibase-client.js` )}/budibase-client.js`
// append app version to bust the cache
if (version) {
url += `?v=${version}`
}
return url
} else { } else {
return `/api/assets/client` return `/api/assets/client`
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "0.9.180-alpha.1", "version": "0.9.184",
"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": "0.9.180-alpha.1", "version": "0.9.184",
"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": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.180-alpha.1", "@budibase/auth": "^0.9.184",
"@budibase/string-templates": "^0.9.180-alpha.1", "@budibase/string-templates": "^0.9.184",
"@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",

View File

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