Merge branch 'develop' of github.com:Budibase/budibase into develop

This commit is contained in:
Martin McKeaveney 2022-03-24 17:47:59 +00:00
commit 1e4ccdc0e5
85 changed files with 1523 additions and 7183 deletions

View File

@ -102,6 +102,35 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
<br />
---
<br />
## Budibase Public API
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
- Budibase as a backend
- Interoperability
#### Docs
You can learn more about the Budibase API at the following places:
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
#### Guides
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
<p align="center">
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
</p>
<br /><br />
<br /><br /><br />
## 🏁 Get started

View File

@ -12,7 +12,7 @@ All ports are BLOCKED except 22 (SSH), 80 (HTTP), 443 (HTTPS), and 10000
* Budibase website: http://budibase.com
For help and more information, visit https://docs.budibase.com/self-hosting/hosting-methods/digitalocean
For help and more information, visit https://docs.budibase.com/docs/digitalocean
********************************************************************************
To delete this message of the day: rm -rf $(readlink -f ${0})

View File

@ -39,7 +39,7 @@
</p>
<h3 align="center">
<a href="https://docs.budibase.com/getting-started">Los Geht's</a>
<a href="https://docs.budibase.com/docs/quickstart-tutorials">Los Geht's</a>
<span> · </span>
<a href="https://docs.budibase.com">Dokumentation</a>
<span> · </span>
@ -109,7 +109,7 @@ $ budi hosting --start
4. Lege einen Admin-Benutzer an.
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/getting-started).
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/docs/quickstart-tutorials).
<br />

View File

@ -112,7 +112,7 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods).
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb&region=nyc1&refcode=0caaa6085a82&image=budibase-20-04)

View File

@ -1,5 +1,5 @@
{
"version": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"description": "Budibase backend core libraries used in server and worker",
"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": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
],
"dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.91-alpha.3",
"@budibase/string-templates": "^1.0.91-alpha.15",
"@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2",

View File

@ -56,6 +56,7 @@
$: if (!loading) loaded = true
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
$: rows = fields?.length ? data || [] : []
$: totalRowCount = rows?.length || 0
$: visibleRowCount = getVisibleRowCount(
loaded,
height,
@ -63,7 +64,12 @@
rowCount,
rowHeight
)
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
$: heightStyle = getHeightStyle(
visibleRowCount,
rowCount,
totalRowCount,
rowHeight
)
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
$: showEditColumn = allowEditRows || allowSelectRows
@ -107,11 +113,16 @@
return Math.min(allRows, Math.ceil(height / rowHeight))
}
const getContentStyle = (visibleRows, rowCount, rowHeight) => {
if (!rowCount || !visibleRows) {
const getHeightStyle = (
visibleRowCount,
rowCount,
totalRowCount,
rowHeight
) => {
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
return ""
}
return `height: ${headerHeight + visibleRows * rowHeight}px;`
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
}
const getGridStyle = (fields, schema, showEditColumn) => {
@ -264,11 +275,11 @@
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
>
{#if !loaded}
<div class="loading" style={contentStyle}>
<div class="loading" style={heightStyle}>
<ProgressCircle />
</div>
{:else}
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
{#if fields.length}
<div class="spectrum-Table-head">
{#if showEditColumn}

File diff suppressed because it is too large Load Diff

View File

@ -247,7 +247,7 @@ Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get("[aria-label=AddCircle]").click()
cy.get(".spectrum-Modal").within(() => {
cy.get(".item").contains("Blank").click()
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
cy.wait(500)
})
cy.get(".spectrum-Dialog-grid").within(() => {
@ -265,7 +265,7 @@ Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
for (let i = 0; i < screenNames.length; i++) {
cy.get(".item").contains(screenNames[i]).click()
}
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
cy.wait(4000)
})

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.91-alpha.3",
"@budibase/client": "^1.0.91-alpha.3",
"@budibase/frontend-core": "^1.0.91-alpha.3",
"@budibase/string-templates": "^1.0.91-alpha.3",
"@budibase/bbui": "^1.0.91-alpha.15",
"@budibase/client": "^1.0.91-alpha.15",
"@budibase/frontend-core": "^1.0.91-alpha.15",
"@budibase/string-templates": "^1.0.91-alpha.15",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

View File

@ -1,4 +1,10 @@
import { store } from "./index"
import { Helpers } from "@budibase/bbui"
import {
decodeJSBinding,
encodeJSBinding,
findHBSBlocks,
} from "@budibase/string-templates"
/**
* Recursively searches for a specific component ID
@ -161,3 +167,58 @@ export const getComponentSettings = componentType => {
return settings
}
/**
* Randomises a components ID's, including all child component IDs, and also
* updates all data bindings to still be valid.
* This mutates the object in place.
* @param component the component to randomise
*/
export const makeComponentUnique = component => {
if (!component) {
return
}
// Replace component ID
const oldId = component._id
const newId = Helpers.uuid()
component._id = newId
if (component._children?.length) {
let children = JSON.stringify(component._children)
// Replace all instances of this ID in child HBS bindings
children = children.replace(new RegExp(oldId, "g"), newId)
// Replace all instances of this ID in child JS bindings
const bindings = findHBSBlocks(children)
bindings.forEach(binding => {
// JSON.stringify will have escaped double quotes, so we need
// to account for that
let sanitizedBinding = binding.replace(/\\"/g, '"')
// Check if this is a valid JS binding
let js = decodeJSBinding(sanitizedBinding)
if (js != null) {
// Replace ID inside JS binding
js = js.replace(new RegExp(oldId, "g"), newId)
// Create new valid JS binding
let newBinding = encodeJSBinding(js)
// Replace escaped double quotes
newBinding = newBinding.replace(/"/g, '\\"')
// Insert new JS back into binding.
// A single string replace here is better than a regex as
// the binding contains special characters, and we only need
// to replace a single instance.
children = children.replace(binding, newBinding)
}
})
// Recurse on all children
component._children = JSON.parse(children)
component._children.forEach(makeComponentUnique)
}
}

View File

@ -126,7 +126,7 @@ export const getDatasourceForProvider = (asset, component) => {
if (dataProviderSetting) {
const settingValue = component[dataProviderSetting.key]
const providerId = extractLiteralHandlebarsID(settingValue)
const provider = findComponent(asset.props, providerId)
const provider = findComponent(asset?.props, providerId)
return getDatasourceForProvider(asset, provider)
}
@ -458,7 +458,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
// Determine the entity which backs this datasource.
// "provider" datasources are those targeting another data provider
if (type === "provider") {
const component = findComponent(asset.props, datasource.providerId)
const component = findComponent(asset?.props, datasource.providerId)
const source = getDatasourceForProvider(asset, component)
return getSchemaForDatasource(asset, source, options)
}

View File

@ -3,7 +3,7 @@ import { getAutomationStore } from "./store/automation"
import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { findComponent } from "./componentUtils"
import { findComponent, findComponentPath } from "./componentUtils"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
@ -25,7 +25,17 @@ export const selectedComponent = derived(
if (!$currentAsset || !$store.selectedComponentId) {
return null
}
return findComponent($currentAsset.props, $store.selectedComponentId)
return findComponent($currentAsset?.props, $store.selectedComponentId)
}
)
export const selectedComponentPath = derived(
[store, currentAsset],
([$store, $currentAsset]) => {
return findComponentPath(
$currentAsset?.props,
$store.selectedComponentId
).map(component => component._id)
}
)

View File

@ -24,9 +24,9 @@ import {
findAllMatchingComponents,
findComponent,
getComponentSettings,
makeComponentUnique,
} from "../componentUtils"
import { Helpers } from "@budibase/bbui"
import { removeBindings } from "../dataBinding"
const INITIAL_FRONTEND_STATE = {
apps: [],
@ -400,11 +400,11 @@ export const getFrontendStore = () => {
parentComponent = selected
} else {
// Otherwise we need to use the parent of this component
parentComponent = findComponentParent(asset.props, selected._id)
parentComponent = findComponentParent(asset?.props, selected._id)
}
} else {
// Use screen or layout if no component is selected
parentComponent = asset.props
parentComponent = asset?.props
}
// Attach component
@ -490,37 +490,22 @@ export const getFrontendStore = () => {
}
}
},
paste: async (targetComponent, mode, preserveBindings = false) => {
paste: async (targetComponent, mode) => {
let promises = []
store.update(state => {
// Stop if we have nothing to paste
if (!state.componentToPaste) {
return state
}
// defines if this is a copy or a cut
const cut = state.componentToPaste.isCut
// immediately need to remove bindings, currently these aren't valid when pasted
if (!cut && !preserveBindings) {
state.componentToPaste = removeBindings(state.componentToPaste, "")
}
// Clone the component to paste
// Retain the same ID if cutting as things may be referencing this component
// Clone the component to paste and make unique if copying
delete state.componentToPaste.isCut
let componentToPaste = cloneDeep(state.componentToPaste)
if (cut) {
state.componentToPaste = null
} else {
const randomizeIds = component => {
if (!component) {
return
}
component._id = Helpers.uuid()
component._children?.forEach(randomizeIds)
}
randomizeIds(componentToPaste)
makeComponentUnique(componentToPaste)
}
if (mode === "inside") {

View File

@ -10,17 +10,18 @@ const allTemplates = tables => [
]
// Allows us to apply common behaviour to all create() functions
const createTemplateOverride = (frontendState, create) => () => {
const screen = create()
const createTemplateOverride = (frontendState, template) => () => {
const screen = template.create()
screen.name = screen.props._id
screen.routing.route = screen.routing.route.toLowerCase()
screen.template = template.id
return screen
}
export default (frontendState, tables) => {
const enrichTemplate = template => ({
...template,
create: createTemplateOverride(frontendState, template.create),
create: createTemplateOverride(frontendState, template),
})
const fromScratch = enrichTemplate(createFromScratchScreen)

View File

@ -10,12 +10,10 @@
<div class="title">
<Tabs selected="Automations">
<Tab title="Automations">
<div class="tab-content-padding">
<AutomationList />
<Modal bind:this={modal}>
<CreateAutomationModal {webhookModal} />
</Modal>
</div>
<AutomationList />
<Modal bind:this={modal}>
<CreateAutomationModal {webhookModal} />
</Modal>
</Tab>
</Tabs>
<div class="add-button" data-cy="new-screen">
@ -24,9 +22,6 @@
</div>
<style>
.tab-content-padding {
padding: 0 var(--spacing-xl);
}
.add-button {
position: absolute;
top: var(--spacing-l);

View File

@ -56,7 +56,7 @@
<a
slot="footer"
target="_blank"
href="https://docs.budibase.com/automate/introduction-to-automate"
href="https://docs.budibase.com/docs/automation-steps"
>
<Icon name="InfoOutline" />
<span>Learn about automations</span>

View File

@ -69,7 +69,7 @@
<a
slot="footer"
target="_blank"
href="https://docs.budibase.com/automate/steps/triggers"
href="https://docs.budibase.com/docs/trigger"
>
<Icon name="InfoOutline" />
<span>Learn about webhooks</span>

View File

@ -12,7 +12,7 @@
Modal,
notifications,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { createEventDispatcher, onMount } from "svelte"
import { cloneDeep } from "lodash/fp"
import { tables } from "stores/backend"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
@ -321,6 +321,12 @@
}
return newError
}
onMount(() => {
if (primaryDisplay) {
field.constraints.presence = { allowEmpty: false }
}
})
</script>
<ModalContent

View File

@ -22,10 +22,11 @@
const selected = $datasources.selected === datasource._id
const open = openDataSources.includes(datasource._id)
const containsSelected = containsActiveEntity(datasource)
const onlySource = $datasources.list.length === 1
return {
...datasource,
selected,
open: selected || open || containsSelected,
open: selected || open || containsSelected || onlySource,
}
})
: []

View File

@ -1,6 +1,6 @@
<script>
import { Icon } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { createEventDispatcher, getContext } from "svelte"
export let icon
export let withArrow = false
@ -14,29 +14,46 @@
export let iconText
export let iconColor
const scrollApi = getContext("scroll")
const dispatch = createEventDispatcher()
function onIconClick(event) {
event.stopPropagation()
let contentRef
$: selected && contentRef && scrollToView()
const onClick = () => {
scrollToView()
dispatch("click")
}
const onIconClick = e => {
e.stopPropagation()
dispatch("iconClick")
}
const scrollToView = () => {
if (!scrollApi || !contentRef) {
return
}
const bounds = contentRef.getBoundingClientRect()
scrollApi.scrollTo(bounds)
}
</script>
<div
class="nav-item"
class:border
class:selected
style={`padding-left: ${indentLevel * 14}px`}
style={`padding-left: ${20 + indentLevel * 14}px`}
{draggable}
on:dragend
on:dragstart
on:dragover
on:drop
on:click
on:click={onClick}
ondragover="return false"
ondragenter="return false"
>
<div class="content">
<div class="nav-item-content" bind:this={contentRef}>
{#if withArrow}
<div class:opened class="icon arrow" on:click={onIconClick}>
<Icon size="S" name="ChevronRight" />
@ -64,11 +81,16 @@
<style>
.nav-item {
border-radius: var(--border-radius-s);
cursor: pointer;
color: var(--grey-7);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
height: 32px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.nav-item.selected {
background-color: var(--grey-2);
@ -81,14 +103,14 @@
visibility: visible;
}
.content {
padding: 0 var(--spacing-s);
height: 32px;
.nav-item-content {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
width: max-content;
}
.icon {
@ -111,12 +133,13 @@
}
.text {
flex: 1 1 auto;
font-weight: 600;
font-size: var(--spectrum-global-dimension-font-size-75);
white-space: nowrap;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 0 0 auto;
}
.actions {
@ -125,9 +148,9 @@
height: 20px;
cursor: pointer;
position: relative;
flex-direction: row;
justify-content: center;
align-items: center;
display: grid;
margin-left: var(--spacing-s);
place-items: center;
}
.iconText {

View File

@ -238,6 +238,7 @@
border: var(--border-light);
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
border-color 130ms ease-in-out;
word-wrap: break-word;
}
li:not(:last-of-type) {
margin-bottom: var(--spacing-s);

View File

@ -40,7 +40,7 @@
<a
slot="footer"
target="_blank"
href="https://docs.budibase.com/automate/steps/triggers"
href="https://docs.budibase.com/docs/trigger"
>
<i class="ri-information-line" />
<span>Learn about webhooks</span>

View File

@ -68,6 +68,7 @@
customTheme: $store.customTheme,
previewDevice: $store.previewDevice,
messagePassing: $store.clientFeatures.messagePassing,
isBudibaseEvent: true
}
$: json = JSON.stringify(previewData)
@ -160,6 +161,11 @@
await store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) {
confirmDeleteComponent(data.id)
} else if (type === "duplicate-component" && data.id) {
const rootComponent = get(currentAsset).props
const component = findComponent(rootComponent, data.id)
store.actions.components.copy(component)
await store.actions.components.paste(component)
} else if (type === "preview-loaded") {
// Wait for this event to show the client library if intelligent
// loading is supported

View File

@ -52,7 +52,7 @@ export default `
console.error("Client received invalid JSON")
// Ignore
}
if (!parsed) {
if (!parsed || !parsed.isBudibaseEvent) {
return
}

View File

@ -21,7 +21,7 @@
const moveUpComponent = () => {
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
const parent = findComponentParent(asset?.props, component._id)
if (!parent) {
return
}
@ -41,7 +41,7 @@
const moveDownComponent = () => {
const asset = get(currentAsset)
const parent = findComponentParent(asset.props, component._id)
const parent = findComponentParent(asset?.props, component._id)
if (!parent) {
return
}
@ -61,7 +61,7 @@
const duplicateComponent = () => {
storeComponentForCopy(false)
pasteComponent("below", true)
pasteComponent("below")
}
const deleteComponent = async () => {
@ -73,14 +73,12 @@
}
const storeComponentForCopy = (cut = false) => {
// lives in store - also used by drag drop
store.actions.components.copy(component, cut)
}
const pasteComponent = (mode, preserveBindings = false) => {
const pasteComponent = mode => {
try {
// lives in store - also used by drag drop
store.actions.components.paste(component, mode, preserveBindings)
store.actions.components.paste(component, mode)
} catch (error) {
notifications.error("Error saving component")
}
@ -140,3 +138,10 @@
onOk={deleteComponent}
/>
{/if}
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -1,10 +1,11 @@
<script>
import { store } from "builderStore"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import NavItem from "components/common/NavItem.svelte"
import { capitalise } from "helpers"
import { notifications } from "@budibase/bbui"
import { selectedComponentPath } from "builderStore"
export let components = []
export let currentComponent
@ -71,10 +72,20 @@
notifications.error("Error saving component")
}
}
const isOpen = (component, selectedComponentPath, closedNodes) => {
if (!component?._children?.length) {
return false
}
if (selectedComponentPath.includes(component._id)) {
return true
}
return !closedNodes[component._id]
}
</script>
<ul>
{#each components as component, index (component._id)}
{#each components || [] as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
<div
@ -97,12 +108,12 @@
withArrow
indentLevel={level + 1}
selected={$store.selectedComponentId === component._id}
opened={!closedNodes[component._id] && component?._children?.length}
opened={isOpen(component, $selectedComponentPath, closedNodes)}
>
<ComponentDropdownMenu {component} />
</NavItem>
{#if component._children && !closedNodes[component._id]}
{#if isOpen(component, $selectedComponentPath, closedNodes)}
<svelte:self
components={component._children}
{currentComponent}
@ -133,6 +144,10 @@
padding-left: 0;
margin: 0;
}
ul,
li {
min-width: max-content;
}
.drop-item {
border-radius: var(--border-radius-m);

View File

@ -51,7 +51,7 @@
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={"Are you sure you wish to delete this layout?"}
okText="Delete Layout"
okText="Delete layout"
onOk={deleteLayout}
/>
@ -65,3 +65,10 @@
<Input thin type="text" label="Name" bind:value={name} />
</ModalContent>
</Modal>
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -0,0 +1,82 @@
<script>
import { goto } from "@roxi/routify"
import { store } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import {
ActionMenu,
MenuItem,
Icon,
Layout,
notifications,
} from "@budibase/bbui"
import { get } from "svelte/store"
export let path
export let screens
let confirmDeleteDialog
const deleteScreens = async () => {
if (!screens?.length) {
return
}
try {
for (let { id } of screens) {
// We have to fetch the screen to be deleted immediately before deleting
// as otherwise we're very likely to 409
const screen = get(store).screens.find(screen => screen._id === id)
if (!screen) {
continue
}
await store.actions.screens.delete(screen)
}
notifications.success("Screens deleted successfully")
$goto("../")
} catch (error) {
notifications.error("Error deleting screens")
}
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
Delete all screens
</MenuItem>
</ActionMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
okText="Delete screens"
onOk={deleteScreens}
>
<Layout noPadding gap="S">
<div>
Are you sure you want to delete all screens under the <b>{path}</b> route?
</div>
<div>The following screens will be deleted:</div>
<div class="to-delete">
{#each screens as screen}
<div>{screen.route}</div>
{/each}
</div>
</Layout>
</ConfirmDialog>
<style>
.to-delete {
font-weight: bold;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: var(--spacing-xl);
}
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -8,6 +8,7 @@
import instantiateStore from "./dragDropStore"
import ComponentTree from "./ComponentTree.svelte"
import NavItem from "components/common/NavItem.svelte"
import PathDropdownMenu from "./PathDropdownMenu.svelte"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import { get } from "svelte/store"
@ -28,6 +29,7 @@
export let border
let routeManuallyOpened = false
$: selectedScreen = $currentAsset
$: allScreens = getAllScreens(route)
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
@ -73,14 +75,17 @@
opened={routeOpened}
{border}
withArrow={route.subpaths}
/>
>
<PathDropdownMenu screens={allScreens} {path} />
</NavItem>
{#if routeOpened}
{#each filteredScreens as screen (screen.id)}
<NavItem
icon="WebPage"
indentLevel={indent || 1}
selected={$store.selectedScreenId === screen.id}
selected={$store.selectedScreenId === screen.id &&
$store.currentView === "detail"}
opened={$store.selectedScreenId === screen.id}
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
withArrow={route.subpaths}

View File

@ -2,14 +2,57 @@
import { goto } from "@roxi/routify"
import { store, allScreens } from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
import {
ActionMenu,
MenuItem,
Icon,
Modal,
Helpers,
notifications,
} from "@budibase/bbui"
import ScreenDetailsModal from "../ScreenDetailsModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import analytics, { Events } from "analytics"
import { makeComponentUnique } from "builderStore/componentUtils"
export let screenId
let confirmDeleteDialog
let screenDetailsModal
$: screen = $allScreens.find(screen => screen._id === screenId)
const duplicateScreen = () => {
screenDetailsModal.show()
}
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
// Create a dupe and ensure it is unique
let duplicateScreen = Helpers.cloneDeep(screen)
delete duplicateScreen._id
delete duplicateScreen._rev
makeComponentUnique(duplicateScreen.props)
// Attach the new name and URL
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
duplicateScreen.props._instanceName = screenName
try {
// Create the screen
await store.actions.screens.save(duplicateScreen)
// Analytics
if (screen.template) {
analytics.captureEvent(Events.SCREEN.CREATED, {
template: "createFromScratch",
})
}
} catch (error) {
notifications.error("Error duplicating screen")
console.log(error)
}
}
const deleteScreen = async () => {
try {
await store.actions.screens.delete(screen)
@ -19,12 +62,28 @@
notifications.error("Error deleting screen")
}
}
const pasteComponent = mode => {
try {
store.actions.components.paste(screen?.props, mode)
} catch (error) {
notifications.error("Error saving component")
}
}
</script>
<ActionMenu>
<div slot="control" class="icon">
<Icon size="S" hoverable name="MoreSmallList" />
</div>
<MenuItem icon="Duplicate" on:click={duplicateScreen}>Duplicate</MenuItem>
<MenuItem
icon="ShowOneLayer"
on:click={() => pasteComponent("inside")}
disabled={!$store.componentToPaste}
>
Paste inside
</MenuItem>
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
</ActionMenu>
@ -32,6 +91,22 @@
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={"Are you sure you wish to delete this screen?"}
okText="Delete Screen"
okText="Delete screen"
onOk={deleteScreen}
/>
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal
onConfirm={createDuplicateScreen}
screenName={screen?.props._instanceName}
screenUrl={screen?.routing.route}
confirmText="Duplicate"
/>
</Modal>
<style>
.icon {
display: grid;
place-items: center;
}
</style>

View File

@ -55,11 +55,10 @@
}
</script>
<div class="root">
<div class="root" class:has-screens={!!paths?.length}>
{#each paths as path, idx (path)}
<PathTree border={idx > 0} {path} route={routes[path]} />
{/each}
{#if !paths.length}
<div class="empty">
There aren't any screens configured with this access role.
@ -68,9 +67,12 @@
</div>
<style>
.root.has-screens {
min-width: max-content;
}
div.empty {
font-size: var(--font-size-xs);
font-size: var(--font-size-s);
color: var(--grey-5);
padding-top: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-xl);
}
</style>

View File

@ -1,5 +1,5 @@
<script>
import { onMount } from "svelte"
import { onMount, setContext } from "svelte"
import { goto, params } from "@roxi/routify"
import {
store,
@ -18,11 +18,63 @@
Search,
Tabs,
Tab,
Layout as BBUILayout,
notifications,
} from "@budibase/bbui"
export let showModal
let scrollRef
const scrollTo = bounds => {
if (!bounds) {
return
}
const sidebarWidth = 259
const navItemHeight = 32
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
let scrollBounds = scrollRef.getBoundingClientRect()
let newOffsets = {}
// Calculate left offset
const offsetX = bounds.left + bounds.width + scrollLeft + 20
if (offsetX > sidebarWidth) {
newOffsets.left = offsetX - sidebarWidth
} else {
newOffsets.left = 0
}
if (newOffsets.left === scrollLeft) {
delete newOffsets.left
}
// Calculate top offset
const offsetY = bounds.top - scrollBounds?.top + scrollTop
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
} else if (offsetY < scrollTop + navItemHeight) {
newOffsets.top = offsetY - navItemHeight
} else {
delete newOffsets.top
}
// Skip if offset is unchanged
if (newOffsets.left == null && newOffsets.top == null) {
return
}
// Smoothly scroll to the offset
scrollRef.scroll({
...newOffsets,
behavior: "smooth",
})
}
setContext("scroll", {
scrollTo,
})
const tabs = [
{
title: "Screens",
@ -79,7 +131,7 @@
<Tabs {selected} on:select={navigate}>
<Tab title="Screens">
<div class="tab-content-padding">
<div class="role-select">
<BBUILayout noPadding gap="XS">
<Select
on:change={updateAccessRole}
value={$selectedAccessRole}
@ -93,17 +145,24 @@
label="Search Screens"
bind:value={$screenSearchString}
/>
</div>
<div class="nav-items-container">
</BBUILayout>
<div class="nav-items-container" bind:this={scrollRef}>
<ComponentNavigationTree />
</div>
</div>
</Tab>
<Tab title="Layouts">
<div class="tab-content-padding">
{#each $store.layouts as layout, idx (layout._id)}
<Layout {layout} border={idx > 0} />
{/each}
<div
class="nav-items-container nav-items-container--layouts"
bind:this={scrollRef}
>
<div class="layouts-container">
{#each $store.layouts as layout, idx (layout._id)}
<Layout {layout} border={idx > 0} />
{/each}
</div>
</div>
<Modal bind:this={newLayoutModal}>
<NewLayoutModal />
</Modal>
@ -126,23 +185,45 @@
justify-content: flex-start;
align-items: stretch;
position: relative;
flex: 1 1 auto;
}
.title :global(.spectrum-Tabs-content),
.title :global(.spectrum-Tabs-content > div),
.title :global(.spectrum-Tabs-content > div > div) {
height: 100%;
}
.add-button {
position: absolute;
top: var(--spacing-l);
right: var(--spacing-xl);
}
.role-select {
.tab-content-padding {
padding: 0 var(--spacing-xl);
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
margin-bottom: var(--spacing-m);
gap: var(--spacing-m);
gap: var(--spacing-xl);
}
.tab-content-padding {
padding: 0 var(--spacing-xl);
.nav-items-container {
border-top: var(--border-light);
margin: 0 calc(-1 * var(--spacing-xl));
padding: var(--spacing-m) 0;
flex: 1 1 auto;
overflow: auto;
height: 0;
position: relative;
}
.nav-items-container--layouts {
border-top: none;
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
}
.layouts-container {
min-width: max-content;
}
</style>

View File

@ -10,39 +10,19 @@
ProgressCircle,
} from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
import { onDestroy } from "svelte"
import { createEventDispatcher } from "svelte"
export let chooseModal
export let save
export let onConfirm
export let onCancel
export let showProgressCircle = false
let selectedScreens = []
const blankScreen = "createFromScratch"
const dispatch = createEventDispatcher()
function setScreens() {
dispatch("save", {
screens: selectedScreens,
})
}
let selectedScreens = []
let templates = getTemplates($store, $tables.list)
$: blankSelected = selectedScreens?.length === 1
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
let templates = getTemplates($store, $tables.list)
const confirm = async () => {
if (autoSelected) {
setScreens()
await save()
} else {
setScreens()
chooseModal(1)
}
}
const toggleScreenSelection = table => {
if (selectedScreens.find(s => s.table === table.name)) {
selectedScreens = selectedScreens.filter(
@ -56,25 +36,25 @@
}
}
onDestroy(() => {
selectedScreens = []
})
const confirmScreenSelection = async () => {
await onConfirm(selectedScreens)
}
</script>
<div>
<ModalContent
title="Add screens"
confirmText="Add Screens"
confirmText="Add screens"
cancelText="Cancel"
onConfirm={() => confirm()}
onConfirm={confirmScreenSelection}
{onCancel}
disabled={!selectedScreens.length}
size="L"
>
<Body size="S"
>Please select the screens you would like to add to your application.
Autogenerated screens come with CRUD functionality.</Body
>
<Body size="S">
Please select the screens you would like to add to your application.
Autogenerated screens come with CRUD functionality.
</Body>
<Layout noPadding gap="S">
<Detail size="S">Blank screen</Detail>
<div

View File

@ -2,58 +2,62 @@
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { selectedAccessRole, allScreens } from "builderStore"
import { onDestroy } from "svelte"
import { get } from "svelte/store"
export let screenName
export let url
export let chooseModal
export let save
export let onConfirm
export let onCancel
export let showProgressCircle = false
export let screenName
export let screenUrl
export let confirmText = "Continue"
let routeError
let roleId = $selectedAccessRole || "BASIC"
let touched = false
const routeChanged = event => {
if (!event.detail.startsWith("/")) {
url = "/" + event.detail
screenUrl = "/" + event.detail
}
url = sanitizeUrl(url)
if (routeExists(url, roleId)) {
touched = true
screenUrl = sanitizeUrl(screenUrl)
if (routeExists(screenUrl)) {
routeError = "This URL is already taken for this access role"
} else {
routeError = ""
routeError = null
}
}
const routeExists = (url, roleId) => {
return $allScreens.some(
const routeExists = url => {
const roleId = get(selectedAccessRole) || "BASIC"
return get(allScreens).some(
screen =>
screen.routing.route.toLowerCase() === url.toLowerCase() &&
screen.routing.roleId === roleId
)
}
onDestroy(() => {
screenName = ""
url = ""
})
const confirmScreenDetails = async () => {
await onConfirm({
screenName,
screenUrl,
})
}
</script>
<ModalContent
size="M"
title={"Enter details"}
confirmText={"Continue"}
onCancel={() => chooseModal(0)}
onConfirm={() => save()}
{confirmText}
onConfirm={confirmScreenDetails}
{onCancel}
cancelText={"Back"}
disabled={!screenName || !url || routeError}
disabled={!screenName || !screenUrl || routeError || !touched}
>
<Input label="Name" bind:value={screenName} />
<Input
label="URL"
error={routeError}
bind:value={url}
bind:value={screenUrl}
on:change={routeChanged}
/>
<div slot="footer">

View File

@ -3,141 +3,133 @@
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
import { Modal, notifications } from "@budibase/bbui"
import { store, selectedAccessRole, allScreens } from "builderStore"
import { store, selectedAccessRole } from "builderStore"
import analytics, { Events } from "analytics"
import { get } from "svelte/store"
let newScreenModal
let navigationSelectionModal
let screenDetailsModal
let screenName = ""
let url = ""
let selectedScreens = []
let pendingScreen
let showProgressCircle = false
let routeError
let createdScreens = []
$: roleId = $selectedAccessRole || "BASIC"
// Modal refs
let newScreenModal
let screenDetailsModal
const createScreens = async () => {
for (let screen of selectedScreens) {
let test = screen.create()
createdScreens.push(test)
analytics.captureEvent(Events.SCREEN.CREATED, {
template: screen.id || screen.name,
})
}
}
// External handler to show the screen wizard
export const showModal = () => {
newScreenModal.show()
const save = async () => {
showProgressCircle = true
try {
await createScreens()
for (let screen of createdScreens) {
await saveScreens(screen)
}
await store.actions.routing.fetch()
selectedScreens = []
createdScreens = []
screenName = ""
url = ""
} catch (error) {
notifications.error("Error creating screens")
}
// Reset state when showing modal again
pendingScreen = null
showProgressCircle = false
}
const saveScreens = async draftScreen => {
let existingScreenCount = $store.screens.filter(
s => s.props._instanceName == draftScreen.props._instanceName
).length
if (existingScreenCount > 0) {
let oldUrlArr = draftScreen.routing.route.split("/")
oldUrlArr[1] = `${oldUrlArr[1]}-${existingScreenCount + 1}`
draftScreen.routing.route = oldUrlArr.join("/")
// Creates an array of screens, checking and sanitising their URLs
const createScreens = async screens => {
if (!screens?.length) {
return
}
showProgressCircle = true
let route = url ? sanitizeUrl(`${url}`) : draftScreen.routing.route
if (draftScreen) {
if (!route) {
routeError = "URL is required"
} else {
if (routeExists(route, roleId)) {
routeError = "This URL is already taken for this access role"
} else {
routeError = ""
try {
for (let screen of screens) {
// Check we aren't clashing with an existing URL
if (hasExistingUrl(screen.routing.route)) {
let suffix = 2
let candidateUrl = makeCandidateUrl(screen, suffix)
while (hasExistingUrl(candidateUrl)) {
candidateUrl = makeCandidateUrl(screen, ++suffix)
}
screen.routing.route = candidateUrl
}
}
if (routeError) return false
// Sanitise URL
screen.routing.route = sanitizeUrl(screen.routing.route)
if (screenName) {
draftScreen.props._instanceName = screenName
}
// Use the currently selected role
screen.routing.roleId = get(selectedAccessRole) || "BASIC"
draftScreen.routing.route = route
draftScreen.routing.roleId = roleId
// Create the screen
await store.actions.screens.save(screen)
await store.actions.screens.save(draftScreen)
if (draftScreen.props._instanceName.endsWith("List")) {
try {
// Analytics
if (screen.template) {
analytics.captureEvent(Events.SCREEN.CREATED, {
template: screen.template,
})
}
// Add link in layout for list screens
if (screen.props._instanceName.endsWith("List")) {
await store.actions.components.links.save(
draftScreen.routing.route,
draftScreen.routing.route.split("/")[1]
screen.routing.route,
screen.routing.route.split("/")[1]
)
} catch (error) {
notifications.error("Error creating link to screen")
}
}
} catch (error) {
notifications.error("Error creating screens")
}
showProgressCircle = false
}
// Checks if any screens exist in the store with the given route and
// currently selected role
const hasExistingUrl = url => {
const roleId = get(selectedAccessRole) || "BASIC"
const screens = get(store).screens.filter(s => s.routing.roleId === roleId)
return !!screens.find(s => s.routing?.route === url)
}
// Constructs a candidate URL for a new screen, suffixing the base of the
// screen's URL with a given suffix.
// e.g. "/sales/:id" => "/sales-1/:id"
const makeCandidateUrl = (screen, suffix) => {
let url = screen.routing?.route || ""
if (url.startsWith("/")) {
url = url.slice(1)
}
if (!url.includes("/")) {
return `/${url}-${suffix}`
} else {
const split = url.split("/")
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
}
}
const routeExists = (route, roleId) => {
return $allScreens.some(
screen =>
screen.routing.route.toLowerCase() === route.toLowerCase() &&
screen.routing.roleId === roleId
)
}
export const showModal = () => {
newScreenModal.show()
}
const setScreens = evt => {
selectedScreens = evt.detail.screens
}
const chooseModal = index => {
/*
0 = newScreenModal
1 = screenDetailsModal
2 = navigationSelectionModal
*/
if (index === 0) {
newScreenModal.show()
} else if (index === 1) {
// Handler for NewScreenModal
const confirmScreenSelection = async templates => {
// Handle template selection
if (templates?.length > 1) {
// Autoscreens, so create immediately
const screens = templates.map(template => template.create())
await createScreens(screens)
} else {
// Empty screen, so proceed to the next modal
pendingScreen = templates[0].create()
screenDetailsModal.show()
} else if (index === 2) {
navigationSelectionModal.show()
}
}
// Handler for ScreenDetailsModal
const confirmScreenDetails = async ({ screenName, screenUrl }) => {
if (!pendingScreen) {
return
}
pendingScreen.props._instanceName = screenName
pendingScreen.routing.route = screenUrl
await createScreens([pendingScreen])
}
</script>
<Modal bind:this={newScreenModal}>
<NewScreenModal
on:save={setScreens}
{showProgressCircle}
{save}
{chooseModal}
/>
<NewScreenModal onConfirm={confirmScreenSelection} {showProgressCircle} />
</Modal>
<Modal bind:this={screenDetailsModal}>
<ScreenDetailsModal
bind:screenName
bind:url
{showProgressCircle}
{save}
{chooseModal}
onConfirm={confirmScreenDetails}
onCancel={() => newScreenModal.show()}
/>
</Modal>

View File

@ -33,7 +33,7 @@
const customSections = settings.filter(setting => setting.section)
return [
{
name: "General",
name: componentDefinition?.name || "General",
info: componentDefinition?.info,
settings: generalSettings,
},

View File

@ -5,7 +5,7 @@
export let parameters
$: components = findAllMatchingComponents($currentAsset.props, component =>
$: components = findAllMatchingComponents($currentAsset?.props, component =>
component._component.endsWith("s3upload")
)
</script>

View File

@ -10,7 +10,7 @@
const dispatch = createEventDispatcher()
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
// Set initial value to closest data provider

View File

@ -18,9 +18,7 @@
let tempValue = value || []
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, dataSource, {
searchableSchema: true,
})?.schema
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
$: schemaFields = Object.values(schema || {})
const saveFilter = async () => {

View File

@ -12,7 +12,7 @@
export let type
$: form = findClosestMatchingComponent(
$currentAsset.props,
$currentAsset?.props,
componentInstance._id,
component => component._component === "@budibase/standard-components/form"
)

View File

@ -11,7 +11,7 @@
const resetFormFields = async () => {
const form = findClosestMatchingComponent(
$currentAsset.props,
$currentAsset?.props,
componentInstance._id,
component => component._component.endsWith("/form")
)

View File

@ -1,171 +0,0 @@
<script>
import analytics from "analytics"
import { createEventDispatcher } from "svelte"
import { fade, fly } from "svelte/transition"
import {
ActionButton,
ClearButton,
RadioGroup,
TextArea,
ButtonGroup,
Button,
Heading,
Detail,
Divider,
Layout,
notifications,
} from "@budibase/bbui"
import { auth } from "stores/portal"
let step = 0
let ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let options = [
"Importing / managing data",
"Designing",
"Automations",
"Managing users / groups",
"Deployment / hosting",
"Documentation",
]
const dispatch = createEventDispatcher()
// Data to send off
let rating
let improvements = ""
let comment = ""
function selectNumber(n) {
rating = n
step = 1
}
function submitFeedback() {
analytics.submitFeedback({
rating,
improvements,
comment,
})
try {
auth.updateSelf({
flags: {
feedbackSubmitted: true,
},
})
} catch (error) {
notifications.error("Error updating user")
}
dispatch("complete")
}
function cancelFeedback() {
try {
auth.updateSelf({
flags: {
feedbackSubmitted: true,
},
})
} catch (error) {
notifications.error("Error updating user")
}
dispatch("complete")
}
</script>
<div
class="position"
in:fade={{ duration: 200 }}
out:fade|local={{ duration: 200 }}
>
<div
class="feedback-frame"
in:fly={{ y: 30, duration: 200 }}
out:fly|local={{ y: 30, duration: 200 }}
>
<div class="close">
<ClearButton on:click={cancelFeedback} />
</div>
<Layout gap="XS">
{#if step === 0}
<Heading size="XS"
>How likely are you to recommend Budibase to a colleague?</Heading
>
<Divider />
<div class="ratings">
{#each ratings as number}
<ActionButton
size="L"
emphasized
selected={number === rating}
on:click={() => selectNumber(number)}
>
{number}
</ActionButton>
{/each}
</div>
<div class="footer">
<Detail size="S">NOT LIKELY</Detail>
<Detail size="S">EXTREMELY LIKELY</Detail>
</div>
{:else if step === 1}
<Heading size="XS">What could be improved most in Budibase?</Heading>
<Divider />
<RadioGroup bind:value={improvements} {options} />
<div class="footer">
<Detail size="S">STEP 2 OF 3</Detail>
<ButtonGroup>
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
<Button primary on:click={() => (step += 1)}>Next</Button>
</ButtonGroup>
</div>
{:else}
<Heading size="XS">How can we improve your experience?</Heading>
<Divider />
<TextArea bind:value={comment} placeholder="Add comments" />
<div class="footer">
<Detail size="S">STEP 3 OF 3</Detail>
<ButtonGroup>
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
<Button cta on:click={submitFeedback}>Complete</Button>
</ButtonGroup>
</div>
{/if}
</Layout>
</div>
</div>
<style>
.feedback-frame :global(textarea) {
min-height: 180px !important;
}
.position {
position: absolute;
right: var(--spacing-l);
bottom: calc(5 * var(--spacing-xl));
}
.feedback-frame {
position: absolute;
bottom: 0;
right: 0;
min-width: 510px;
background: var(--background);
border-radius: var(--spectrum-global-dimension-size-50);
border: 2px solid var(--spectrum-global-color-blue-400);
padding: var(--spacing-xl);
}
.ratings {
display: flex;
justify-content: space-between;
}
.close {
position: absolute;
top: 0;
right: 0;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -33,8 +33,7 @@
let parameters
let data = []
let saveId
const transformerDocs =
"https://docs.budibase.com/building-apps/data/transformers"
const transformerDocs = "https://docs.budibase.com/docs/transformers"
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
$: query.schema = fieldsToSchema(fields)

View File

@ -5,7 +5,6 @@
import DeployModal from "components/deploy/DeployModal.svelte"
import RevertModal from "components/deploy/RevertModal.svelte"
import VersionModal from "components/deploy/VersionModal.svelte"
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
import { API } from "api"
import { auth, admin } from "stores/portal"
import { isActive, goto, layout, redirect } from "@roxi/routify"
@ -21,15 +20,11 @@
// Sync once when you load the app
let hasSynced = false
let userShouldPostFeedback = false
$: selected = capitalise(
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
)
function previewApp() {
if (!$auth?.user?.flags?.feedbackSubmitted) {
userShouldPostFeedback = true
}
window.open(`/${application}`)
}
@ -126,10 +121,6 @@
<p>Something went wrong: {error.message}</p>
{/await}
{#if userShouldPostFeedback}
<NPSFeedbackForm on:complete={() => (userShouldPostFeedback = false)} />
{/if}
<style>
.loading {
min-height: 100%;

View File

@ -23,10 +23,8 @@
<div class="nav">
<Tabs {selected} on:select={selectFirstDatasource}>
<Tab title="Sources">
<div class="tab-content-padding">
<DatasourceNavigator />
<CreateDatasourceModal bind:modal />
</div>
<DatasourceNavigator />
<CreateDatasourceModal bind:modal />
</Tab>
</Tabs>
<div
@ -63,10 +61,6 @@
display: contents;
}
.tab-content-padding {
padding: 0 var(--spacing-xl);
}
.nav {
overflow-y: auto;
background: var(--background);

View File

@ -415,9 +415,7 @@
<Banner
extraButtonText="Learn more"
extraButtonAction={() =>
window.open(
"https://docs.budibase.com/building-apps/data/transformers"
)}
window.open("https://docs.budibase.com/docs/transformers")}
on:change={() => updateFlag("queryTransformerBanner", true)}
>
Add a JavaScript function to transform the query result.

View File

@ -135,7 +135,7 @@
if (asset?._id) {
url += `/${asset._id}`
if (componentId) {
const componentPath = findComponentPath(asset.props, componentId)
const componentPath = findComponentPath(asset?.props, componentId)
const componentURL = componentPath
.slice(1)
.map(comp => comp._id)
@ -244,8 +244,6 @@
display: flex;
flex-direction: column;
gap: var(--spacing-l);
padding: 0 0 60px 0;
overflow-y: auto;
border-right: var(--border-light);
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -35,7 +35,7 @@ async function downloadFiles() {
async function checkDockerConfigured() {
const error =
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/self-hosting/hosting-methods/docker-compose#installing-docker"
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
const docker = await lookpath("docker")
const compose = await lookpath("docker-compose")
if (!docker || !compose) {

View File

@ -264,7 +264,8 @@
{
"label": "Primary",
"value": "primary"
}, {
},
{
"label": "Secondary",
"value": "secondary"
},
@ -507,7 +508,7 @@
},
{
"type": "static",
"values": [
"values": [
{
"label": "Row Index",
"key": "index"
@ -626,28 +627,36 @@
"defaultValue": "M",
"showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Extra Small",
"value": "XS"
}, {
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}, {
"label": "Extra Large",
"value": "XL"
}, {
"label": "2XL",
"value": "XXL"
}, {
"label": "3XL",
"value": "XXXL"
}]
"options": [
{
"label": "Extra Small",
"value": "XS"
},
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
},
{
"label": "Extra Large",
"value": "XL"
},
{
"label": "2XL",
"value": "XXL"
},
{
"label": "3XL",
"value": "XXXL"
}
]
},
{
"type": "color",
@ -689,27 +698,32 @@
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
"options": [
{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
},
{
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
},
{
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
},
{
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}
]
}
]
},
@ -733,28 +747,36 @@
"defaultValue": "M",
"showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Extra Small",
"value": "XS"
}, {
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}, {
"label": "Extra Large",
"value": "XL"
}, {
"label": "2XL",
"value": "XXL"
}, {
"label": "3XL",
"value": "XXXL"
}]
"options": [
{
"label": "Extra Small",
"value": "XS"
},
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
},
{
"label": "Extra Large",
"value": "XL"
},
{
"label": "2XL",
"value": "XXL"
},
{
"label": "3XL",
"value": "XXXL"
}
]
},
{
"type": "color",
@ -796,27 +818,32 @@
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
"options": [
{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
},
{
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
},
{
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
},
{
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}
]
}
]
},
@ -837,16 +864,20 @@
"defaultValue": "M",
"showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}]
"options": [
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
}
]
},
{
"type": "color",
@ -1037,16 +1068,20 @@
"defaultValue": "M",
"showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}]
"options": [
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
}
]
},
{
"type": "color",
@ -1088,27 +1123,32 @@
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
"options": [
{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
},
{
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
},
{
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
},
{
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}
]
}
]
},
@ -1165,7 +1205,15 @@
"type": "select",
"label": "Card Width",
"key": "cardWidth",
"options": ["24rem", "28rem", "32rem", "40rem", "48rem", "60rem", "100%"],
"options": [
"24rem",
"28rem",
"32rem",
"40rem",
"48rem",
"60rem",
"100%"
],
"defaultValue": "32rem"
},
{
@ -1785,11 +1833,7 @@
"icon": "Form",
"hasChildren": true,
"illegalChildren": ["section", "form"],
"actions": [
"ValidateForm",
"ClearForm",
"ChangeFormStep"
],
"actions": ["ValidateForm", "ClearForm", "ChangeFormStep"],
"styles": ["size"],
"settings": [
{
@ -1816,7 +1860,8 @@
{
"label": "Medium",
"value": "spectrum--medium"
}, {
},
{
"label": "Large",
"value": "spectrum--large"
}
@ -1833,7 +1878,7 @@
"context": [
{
"type": "static",
"values": [
"values": [
{
"label": "Value",
"key": "__value"
@ -1947,27 +1992,32 @@
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
"options": [
{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
},
{
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
},
{
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
},
{
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}
]
}
]
},
@ -2634,15 +2684,15 @@
],
"context": {
"type": "static",
"values": [
"values": [
{
"label": "Rows",
"key": "rows"
},
{
"label": "Extra Info",
"key": "info"
},
{
"label": "Extra Info",
"key": "info"
},
{
"label": "Rows Length",
"key": "rowsLength"
@ -2964,7 +3014,8 @@
"label": "Table Columns",
"key": "tableColumns",
"dependsOn": "dataSource",
"placeholder": "All columns"
"placeholder": "All columns",
"nested": true
},
{
"type": "boolean",
@ -3116,7 +3167,6 @@
"key": "cardDescription",
"label": "Description",
"nested": true
},
{
"type": "text",
@ -3381,12 +3431,12 @@
{
"type": "static",
"suffix": "provider",
"values": [
"values": [
{
"label": "Rows",
"key": "rows"
},
{
{
"label": "Extra Info",
"key": "info"
},
@ -3407,12 +3457,12 @@
{
"type": "static",
"suffix": "repeater",
"values": [
"values": [
{
"label": "Row Index",
"key": "index"
}
]
]
},
{
"type": "schema",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"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": "^1.0.91-alpha.3",
"@budibase/frontend-core": "^1.0.91-alpha.3",
"@budibase/string-templates": "^1.0.91-alpha.3",
"@budibase/bbui": "^1.0.91-alpha.15",
"@budibase/frontend-core": "^1.0.91-alpha.15",
"@budibase/string-templates": "^1.0.91-alpha.15",
"@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3",

View File

@ -226,4 +226,13 @@
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
}
/* Print styles */
@media print {
#spectrum-root,
#clip-root,
#app-root {
overflow: visible !important;
}
}
</style>

View File

@ -427,4 +427,20 @@
height: var(--height);
z-index: 998;
}
/* Print styles */
@media print {
.layout,
.main-wrapper {
overflow: visible !important;
}
.nav-wrapper {
display: none !important;
}
.layout {
flex-direction: column !important;
justify-content: flex-start !important;
align-items: stretch !important;
}
}
</style>

View File

@ -130,6 +130,7 @@
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
}
.button-container {

View File

@ -36,4 +36,13 @@
div :global(.apexcharts-datalabel) {
fill: var(--spectrum-global-color-gray-800);
}
div :global(.apexcharts-tooltip) {
background-color: var(--spectrum-global-color-gray-200) !important;
border-color: var(--spectrum-global-color-gray-300) !important;
box-shadow: 2px 2px 6px -4px rgba(0, 0, 0, 0.1) !important;
}
div :global(.apexcharts-tooltip-title) {
background-color: var(--spectrum-global-color-gray-100) !important;
border-color: var(--spectrum-global-color-gray-300) !important;
}
</style>

View File

@ -18,16 +18,53 @@
export let palette
export let horizontal
$: options = setUpChart(dataProvider)
$: options = setUpChart(
title,
dataProvider,
labelColumn,
valueColumns,
xAxisLabel,
yAxisLabel,
height,
width,
dataLabels,
animate,
legend,
stacked,
yAxisUnits,
palette,
horizontal
)
const setUpChart = provider => {
const setUpChart = (
title,
dataProvider,
labelColumn,
valueColumns,
xAxisLabel,
yAxisLabel,
height,
width,
dataLabels,
animate,
legend,
stacked,
yAxisUnits,
palette,
horizontal
) => {
console.log("new chart")
const allCols = [labelColumn, ...(valueColumns || [null])]
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
if (
!dataProvider ||
!dataProvider.rows?.length ||
allCols.find(x => x == null)
) {
return null
}
// Fetch data
const { schema, rows } = provider
const { schema, rows } = dataProvider
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = rows.filter(row => hasAllColumns(row)).slice(0, 100)

View File

@ -16,17 +16,48 @@
export let animate
export let yAxisUnits
$: options = setUpChart(dataProvider)
$: options = setUpChart(
title,
dataProvider,
dateColumn,
openColumn,
highColumn,
lowColumn,
closeColumn,
xAxisLabel,
yAxisLabel,
height,
width,
animate,
yAxisUnits
)
// Fetch data on mount
const setUpChart = provider => {
const setUpChart = (
title,
dataProvider,
dateColumn,
openColumn,
highColumn,
lowColumn,
closeColumn,
xAxisLabel,
yAxisLabel,
height,
width,
animate,
yAxisUnits
) => {
const allCols = [dateColumn, openColumn, highColumn, lowColumn, closeColumn]
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
if (
!dataProvider ||
!dataProvider.rows?.length ||
allCols.find(x => x == null)
) {
return null
}
// Fetch data
const { schema, rows } = provider
const { schema, rows } = dataProvider
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = rows.filter(row => hasAllColumns(row))

View File

@ -23,17 +23,56 @@
export let stacked
export let gradient
$: options = setUpChart(dataProvider)
$: options = setUpChart(
title,
dataProvider,
labelColumn,
valueColumns,
xAxisLabel,
yAxisLabel,
height,
width,
animate,
dataLabels,
curve,
legend,
yAxisUnits,
palette,
area,
stacked,
gradient
)
// Fetch data on mount
const setUpChart = provider => {
const setUpChart = (
title,
dataProvider,
labelColumn,
valueColumns,
xAxisLabel,
yAxisLabel,
height,
width,
animate,
dataLabels,
curve,
legend,
yAxisUnits,
palette,
area,
stacked,
gradient
) => {
const allCols = [labelColumn, ...(valueColumns || [null])]
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
if (
!dataProvider ||
!dataProvider.rows?.length ||
allCols.find(x => x == null)
) {
return null
}
// Fetch, filter and sort data
const { schema, rows } = provider
const { schema, rows } = dataProvider
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = rows.filter(row => hasAllColumns(row))

View File

@ -14,16 +14,44 @@
export let donut
export let palette
$: options = setUpChart(dataProvider)
$: options = setUpChart(
title,
dataProvider,
labelColumn,
valueColumn,
height,
width,
dataLabels,
animate,
legend,
donut,
palette
)
// Fetch data on mount
const setUpChart = provider => {
if (!provider || !provider.rows?.length || !labelColumn || !valueColumn) {
const setUpChart = (
title,
dataProvider,
labelColumn,
valueColumn,
height,
width,
dataLabels,
animate,
legend,
donut,
palette
) => {
if (
!dataProvider ||
!dataProvider.rows?.length ||
!labelColumn ||
!valueColumn
) {
return null
}
// Fetch, filter and sort data
const { schema, rows } = provider
const { schema, rows } = dataProvider
const data = rows
.filter(row => row[labelColumn] != null && row[valueColumn] != null)
.slice(0, 100)

View File

@ -4,7 +4,6 @@
Button,
Combobox,
DatePicker,
DrawerContent,
Icon,
Input,
Layout,
@ -12,10 +11,12 @@
} from "@budibase/bbui"
import { generate } from "shortid"
import { LuceneUtils, Constants } from "@budibase/frontend-core"
import { getContext } from "svelte"
export let schemaFields
export let filters = []
const context = getContext("context")
const BannedTypes = ["link", "attachment", "json"]
$: fieldOptions = (schemaFields ?? [])
@ -89,55 +90,55 @@
}
</script>
<DrawerContent>
<div class="container">
<Layout noPadding>
<Body size="S">
{#if !filters?.length}
Add your first filter expression.
{:else}
Results are filtered to only those which match all of the following
constraints.
{/if}
</Body>
{#if filters?.length}
<div class="fields">
{#each filters as filter, idx}
<Select
bind:value={filter.field}
options={fieldOptions}
on:change={e => onFieldChange(filter, e.detail)}
placeholder="Column"
<div class="container" class:mobile={$context.device.mobile}>
<Layout noPadding>
<Body size="S">
{#if !filters?.length}
Add your first filter expression.
{:else}
Results are filtered to only those which match all of the following
constraints.
{/if}
</Body>
{#if filters?.length}
<div class="fields">
{#each filters as filter, idx}
<Select
bind:value={filter.field}
options={fieldOptions}
on:change={e => onFieldChange(filter, e.detail)}
placeholder="Column"
/>
<Select
disabled={!filter.field}
options={LuceneUtils.getValidOperatorsForType(filter.type)}
bind:value={filter.operator}
on:change={e => onOperatorChange(filter, e.detail)}
placeholder={null}
/>
{#if ["string", "longform", "number", "formula"].includes(filter.type)}
<Input disabled={filter.noValue} bind:value={filter.value} />
{:else if ["options", "array"].includes(filter.type)}
<Combobox
disabled={filter.noValue}
options={getFieldOptions(filter.field)}
bind:value={filter.value}
/>
<Select
disabled={!filter.field}
options={LuceneUtils.getValidOperatorsForType(filter.type)}
bind:value={filter.operator}
on:change={e => onOperatorChange(filter, e.detail)}
placeholder={null}
{:else if filter.type === "boolean"}
<Combobox
disabled={filter.noValue}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
bind:value={filter.value}
/>
{#if ["string", "longform", "number", "formula"].includes(filter.type)}
<Input disabled={filter.noValue} bind:value={filter.value} />
{:else if ["options", "array"].includes(filter.type)}
<Combobox
disabled={filter.noValue}
options={getFieldOptions(filter.field)}
bind:value={filter.value}
/>
{:else if filter.type === "boolean"}
<Combobox
disabled={filter.noValue}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
bind:value={filter.value}
/>
{:else if filter.type === "datetime"}
<DatePicker disabled={filter.noValue} bind:value={filter.value} />
{:else}
<Input disabled />
{/if}
{:else if filter.type === "datetime"}
<DatePicker disabled={filter.noValue} bind:value={filter.value} />
{:else}
<Input disabled />
{/if}
<div class="controls">
<Icon
name="Duplicate"
hoverable
@ -150,17 +151,17 @@
size="S"
on:click={() => removeFilter(filter.id)}
/>
{/each}
</div>
{/if}
<div>
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
Add filter
</Button>
</div>
{/each}
</div>
</Layout>
</div>
</DrawerContent>
{/if}
<div>
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
Add filter
</Button>
</div>
</Layout>
</div>
<style>
.container {
@ -175,4 +176,19 @@
align-items: center;
grid-template-columns: 1fr 120px 1fr auto auto;
}
.controls {
display: contents;
}
.container.mobile .fields {
grid-template-columns: 1fr;
}
.container.mobile .controls {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: var(--spacing-s) 0;
gap: var(--spacing-s);
}
</style>

View File

@ -17,6 +17,7 @@
let table
$: fetchSchema(dataSource)
$: fetchTable(dataSource)
// Returns the closes data context which isn't a built in context
const getInitialValues = (type, dataSource, context) => {
@ -74,6 +75,16 @@
}
}
const fetchTable = async dataSource => {
if (dataSource?.tableId) {
try {
table = await API.fetchTableDefinition(dataSource.tableId)
} catch (error) {
table = null
}
}
}
$: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = Helpers.hashString(
JSON.stringify(initialValues) + JSON.stringify(schema)

View File

@ -157,13 +157,9 @@
const { fieldState } = get(existingField)
fieldId = fieldState.fieldId
// Use new default value if default value changed,
// otherwise use the current value if possible
if (defaultValue !== fieldState.defaultValue) {
initialValue = defaultValue
} else {
initialValue = fieldState.value ?? initialValue
}
// Determine the initial value for this field, reusing the current
// value if one exists
initialValue = fieldState.value ?? initialValue
// If this field has already been registered and we previously had an
// error set, then re-run the validator to see if we can unset it

View File

@ -146,6 +146,15 @@
<div class="divider" />
{/if}
{/each}
<SettingsButton
icon="Duplicate"
on:click={() => {
builderStore.actions.duplicateComponent(
$builderStore.selectedComponent._id
)
}}
title="Duplicate component"
/>
<SettingsButton
icon="Delete"
on:click={() => {
@ -153,6 +162,7 @@
$builderStore.selectedComponent._id
)
}}
title="Delete component"
/>
</div>
{/if}

View File

@ -6,12 +6,26 @@ const createAuthStore = () => {
// Fetches the user object if someone is logged in and has reloaded the page
const fetchUser = async () => {
let globalSelf = null
let appSelf = null
// First try and get the global user, to see if we are logged in at all
try {
const user = await API.fetchSelf()
store.set(user)
globalSelf = await API.fetchBuilderSelf()
} catch (error) {
store.set(null)
return
}
// Then try and get the user for this app to provide via context
try {
appSelf = await API.fetchSelf()
} catch (error) {
// Swallow
}
// Use the app self if present, otherwise fallback to the global self
store.set(appSelf || globalSelf || null)
}
const logOut = async () => {

View File

@ -62,6 +62,9 @@ const createBuilderStore = () => {
deleteComponent: id => {
dispatchEvent("delete-component", { id })
},
duplicateComponent: id => {
dispatchEvent("duplicate-component", { id })
},
notifyLoaded: () => {
dispatchEvent("preview-loaded")
},

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
{
"name": "@budibase/frontend-core",
"version": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase",
"license": "MPL-2.0",
"svelte": "src/index.js",
"dependencies": {
"@budibase/bbui": "^1.0.91-alpha.3",
"@budibase/bbui": "^1.0.91-alpha.15",
"lodash": "^4.17.21",
"svelte": "^3.46.2"
}

View File

@ -4,7 +4,7 @@ export const buildAnalyticsEndpoints = API => ({
*/
pingEndUser: async () => {
return await API.post({
url: `/api/analytics/ping`,
url: `/api/bbtel/ping`,
})
},
@ -13,7 +13,7 @@ export const buildAnalyticsEndpoints = API => ({
*/
getAnalyticsStatus: async () => {
return await API.get({
url: "/api/analytics",
url: "/api/bbtel",
})
},
})

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -71,9 +71,9 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.91-alpha.3",
"@budibase/client": "^1.0.91-alpha.3",
"@budibase/string-templates": "^1.0.91-alpha.3",
"@budibase/backend-core": "^1.0.91-alpha.15",
"@budibase/client": "^1.0.91-alpha.15",
"@budibase/string-templates": "^1.0.91-alpha.15",
"@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0",
@ -149,6 +149,7 @@
"@types/jest": "^26.0.23",
"@types/koa": "^2.13.3",
"@types/koa-router": "^7.4.2",
"@types/lodash": "4.14.180",
"@types/node": "^15.12.4",
"@types/oracledb": "^5.2.1",
"@types/redis": "^4.0.11",
@ -158,6 +159,7 @@
"copyfiles": "^2.4.1",
"docker-compose": "^0.23.6",
"eslint": "^6.8.0",
"is-wsl": "^2.2.0",
"jest": "^27.0.5",
"jest-openapi": "^0.14.2",
"nodemon": "^2.0.4",

View File

@ -2,10 +2,11 @@
const compose = require("docker-compose")
const path = require("path")
const fs = require("fs")
const isWsl = require("is-wsl")
const { processStringSync } = require("@budibase/string-templates")
function isLinux() {
return process.platform !== "darwin" && process.platform !== "win32"
return !isWsl && process.platform !== "darwin" && process.platform !== "win32"
}
// This script wraps docker-compose allowing you to manage your dev infrastructure with simple commands.

View File

@ -52,7 +52,7 @@ export async function read(ctx: any, next: any) {
}
export async function update(ctx: any, next: any) {
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params.tableId))
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params))
await rowController.save(ctx)
await next()
}
@ -60,7 +60,7 @@ export async function update(ctx: any, next: any) {
export async function destroy(ctx: any, next: any) {
// set the body as expected, with the _id and _rev fields
ctx.request.body = await addRev(
fixRow({ _id: ctx.params.rowId }, ctx.params.tableId)
fixRow({ _id: ctx.params.rowId }, ctx.params)
)
await rowController.destroy(ctx)
// destroy controller doesn't currently return the row as the body, need to adjust this

View File

@ -85,7 +85,11 @@ exports.patch = async ctx => {
const isUserTable = tableId === InternalTables.USER_METADATA
let oldRow
try {
oldRow = await db.get(inputs._id)
let dbTable = await db.get(tableId)
oldRow = await outputProcessing(
dbTable,
await findRow(ctx, tableId, inputs._id)
)
} catch (err) {
if (isUserTable) {
// don't include the rev, it'll be the global rev

View File

@ -4,7 +4,7 @@ const controller = require("../controllers/analytics")
const router = Router()
router
.get("/api/analytics", controller.isEnabled)
.post("/api/analytics/ping", controller.endUserPing)
.get("/api/bbtel", controller.isEnabled)
.post("/api/bbtel/ping", controller.endUserPing)
module.exports = router

View File

@ -11,10 +11,10 @@ describe("run misc tests", () => {
await config.init()
})
describe("/analytics", () => {
describe("/bbtel", () => {
it("check if analytics enabled", async () => {
const res = await request
.get(`/api/analytics`)
.get(`/api/bbtel`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)

View File

@ -27,11 +27,8 @@ function parse(input: any) {
if (typeof input !== "string") {
return input
}
if (input === MAX_ISO_DATE) {
return new Date(8640000000000000)
}
if (input === MIN_ISO_DATE) {
return new Date(-8640000000000000)
if (input === MAX_ISO_DATE || input === MIN_ISO_DATE) {
return null
}
if (isIsoDateString(input)) {
return new Date(input)
@ -130,11 +127,19 @@ class InternalBuilder {
}
if (filters.range) {
iterate(filters.range, (key, value) => {
if (!value.high || !value.low) {
return
if (value.low && value.high) {
// Use a between operator if we have 2 valid range values
const fnc = allOr ? "orWhereBetween" : "whereBetween"
query = query[fnc](key, [value.low, value.high])
} else if (value.low) {
// Use just a single greater than operator if we only have a low
const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, ">", value.low)
} else if (value.high) {
// Use just a single less than operator if we only have a high
const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, "<", value.high)
}
const fnc = allOr ? "orWhereBetween" : "whereBetween"
query = query[fnc](key, [value.low, value.high])
})
}
if (filters.equal) {

View File

@ -11,6 +11,7 @@ import {
PaginationValues,
} from "../definitions/datasource"
import { IntegrationBase } from "./base/IntegrationBase"
import { get } from "lodash"
const BodyTypes = {
NONE: "none",
@ -163,7 +164,7 @@ module RestModule {
// Check if a pagination cursor exists in the response
let nextCursor = null
if (pagination?.responseParam) {
nextCursor = data?.[pagination.responseParam]
nextCursor = get(data, pagination.responseParam)
}
return {

View File

@ -187,4 +187,55 @@ describe("SQL query builder", () => {
sql: `select * from (select * from \`${TABLE_NAME}\` limit ?) as \`${TABLE_NAME}\``
})
})
it("should use greater than when only low range specified", () => {
const date = new Date()
const query = sql._query(generateReadJson({
filters: {
range: {
property: {
low: date,
}
}
}
}))
expect(query).toEqual({
bindings: [date, limit],
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`
})
})
it("should use less than when only high range specified", () => {
const date = new Date()
const query = sql._query(generateReadJson({
filters: {
range: {
property: {
high: date,
}
}
}
}))
expect(query).toEqual({
bindings: [date, limit],
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" < $1 limit $2) as "${TABLE_NAME}"`
})
})
it("should use greater than when only low range specified", () => {
const date = new Date()
const query = sql._query(generateReadJson({
filters: {
range: {
property: {
low: date,
}
}
}
}))
expect(query).toEqual({
bindings: [date, limit],
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
"version": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"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": "1.0.91-alpha.3",
"version": "1.0.91-alpha.15",
"description": "Budibase background service",
"main": "src/index.ts",
"repository": {
@ -34,8 +34,8 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
"@budibase/backend-core": "^1.0.91-alpha.3",
"@budibase/string-templates": "^1.0.91-alpha.3",
"@budibase/backend-core": "^1.0.91-alpha.15",
"@budibase/string-templates": "^1.0.91-alpha.15",
"@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0",

File diff suppressed because it is too large Load Diff