Merge branch 'develop' of github.com:Budibase/budibase into feature/map-actions

This commit is contained in:
Andrew Kingston 2022-03-25 12:00:17 +00:00
commit bedbf4bcae
104 changed files with 5061 additions and 7466 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.0",
"version": "1.0.91-alpha.16",
"npmClient": "yarn",
"packages": [
"packages/*"

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
"version": "1.0.91-alpha.0",
"version": "1.0.91-alpha.16",
"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.0",
"version": "1.0.91-alpha.16",
"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.0",
"@budibase/string-templates": "^1.0.91-alpha.16",
"@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.0",
"version": "1.0.91-alpha.16",
"license": "GPL-3.0",
"private": true,
"scripts": {
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.0.91-alpha.0",
"@budibase/client": "^1.0.91-alpha.0",
"@budibase/frontend-core": "^1.0.91-alpha.0",
"@budibase/string-templates": "^1.0.91-alpha.0",
"@budibase/bbui": "^1.0.91-alpha.16",
"@budibase/client": "^1.0.91-alpha.16",
"@budibase/frontend-core": "^1.0.91-alpha.16",
"@budibase/string-templates": "^1.0.91-alpha.16",
"@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)
}
@ -486,7 +486,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>
</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

@ -0,0 +1,54 @@
<script>
export let width = "100"
export let height = "100"
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="23 6 469 132"
{width}
{height}
>
<defs id="defs202">
<linearGradient id="a" x1="-3.49%" x2="100.83%" y1="17.02%" y2="92.9%">
<stop offset="0%" stop-color="#fff" stop-opacity=".1" id="stop192" />
<stop offset="14%" stop-color="#fff" stop-opacity=".08" id="stop194" />
<stop offset="61%" stop-color="#fff" stop-opacity=".02" id="stop196" />
<stop offset="100%" stop-color="#fff" stop-opacity="0" id="stop198" />
</linearGradient>
<path
id="b"
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z"
/>
</defs>
<g id="g305" transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
<path
fill="#ffa000"
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
id="path204"
/>
<path
fill="#f57c00"
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
id="path206"
/>
<path
fill="#ffca28"
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
id="path208"
/>
<path
fill="#ffffff"
fill-opacity="0.2"
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
id="path210"
/>
<path
fill="#a52714"
opacity="0.2"
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
id="path212"
/>
</g>
</svg>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="23 6 469 132"
width="100"
height="100"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs202">
<linearGradient
id="a"
x1="-3.49%"
x2="100.83%"
y1="17.02%"
y2="92.9%">
<stop
offset="0%"
stop-color="#fff"
stop-opacity=".1"
id="stop192" />
<stop
offset="14%"
stop-color="#fff"
stop-opacity=".08"
id="stop194" />
<stop
offset="61%"
stop-color="#fff"
stop-opacity=".02"
id="stop196" />
<stop
offset="100%"
stop-color="#fff"
stop-opacity="0"
id="stop198" />
</linearGradient>
<path
id="b"
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z" />
</defs>
<g
id="g305"
transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
<path
fill="#ffa000"
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
id="path204" />
<path
fill="#f57c00"
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
id="path206" />
<path
fill="#ffca28"
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
id="path208" />
<path
fill="#ffffff"
fill-opacity="0.2"
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
id="path210" />
<path
fill="#a52714"
opacity="0.2"
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
id="path212" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -12,6 +12,7 @@ import Rest from "./Rest.svelte"
import Budibase from "./Budibase.svelte"
import Oracle from "./Oracle.svelte"
import GoogleSheets from "./GoogleSheets.svelte"
import Firebase from "./Firebase.svelte"
export default {
BUDIBASE: Budibase,
@ -28,4 +29,5 @@ export default {
REST: Rest,
ORACLE: Oracle,
GOOGLE_SHEETS: GoogleSheets,
FIREBASE: Firebase,
}

View File

@ -7,7 +7,6 @@
Layout,
Tabs,
Tab,
Input,
Heading,
TextArea,
Dropzone,
@ -98,15 +97,16 @@
<Body size="XS"
>Import your rest collection using one of the options below</Body
>
<Tabs selected="Link">
<Tab title="Link">
<Tabs selected="File">
<!-- Commenting until nginx csp issue resolved -->
<!-- <Tab title="Link">
<Input
bind:value={$data.url}
on:change={() => (lastTouched = "url")}
label="Enter a URL"
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
/>
</Tab>
</Tab> -->
<Tab title="File">
<Dropzone
gallery={false}
@ -115,7 +115,14 @@
$data.file = e.detail?.[0]
lastTouched = "file"
}}
fileTags={["OpenAPI 2.0", "Swagger 2.0", "cURL", "YAML", "JSON"]}
fileTags={[
"OpenAPI 3.0",
"OpenAPI 2.0",
"Swagger 2.0",
"cURL",
"YAML",
"JSON",
]}
maximum={1}
/>
</Tab>

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">
<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 = ""
}
}
if (routeError) return false
if (screenName) {
draftScreen.props._instanceName = screenName
}
draftScreen.routing.route = route
draftScreen.routing.roleId = roleId
await store.actions.screens.save(draftScreen)
if (draftScreen.props._instanceName.endsWith("List")) {
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
}
// Sanitise URL
screen.routing.route = sanitizeUrl(screen.routing.route)
// Use the currently selected role
screen.routing.roleId = get(selectedAccessRole) || "BASIC"
// Create the screen
await store.actions.screens.save(screen)
// 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")
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

@ -178,6 +178,7 @@ export const IntegrationTypes = {
ORACLE: "ORACLE",
INTERNAL: "INTERNAL",
GOOGLE_SHEETS: "GOOGLE_SHEETS",
FIREBASE: "FIREBASE",
}
export const IntegrationNames = {
@ -195,6 +196,7 @@ export const IntegrationNames = {
[IntegrationTypes.ORACLE]: "Oracle",
[IntegrationTypes.INTERNAL]: "Internal",
[IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets",
[IntegrationTypes.FIREBASE]: "Firebase",
}
export const SchemaTypeOptions = [

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>
</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.0",
"version": "1.0.91-alpha.16",
"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

@ -3125,7 +3125,8 @@
"label": "Table Columns",
"key": "tableColumns",
"dependsOn": "dataSource",
"placeholder": "All columns"
"placeholder": "All columns",
"nested": true
},
{
"type": "boolean",

View File

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "1.0.91-alpha.0",
"version": "1.0.91-alpha.16",
"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.0",
"@budibase/frontend-core": "^1.0.91-alpha.0",
"@budibase/string-templates": "^1.0.91-alpha.0",
"@budibase/bbui": "^1.0.91-alpha.16",
"@budibase/frontend-core": "^1.0.91-alpha.16",
"@budibase/string-templates": "^1.0.91-alpha.16",
"@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,8 +90,7 @@
}
</script>
<DrawerContent>
<div class="container">
<div class="container" class:mobile={$context.device.mobile}>
<Layout noPadding>
<Body size="S">
{#if !filters?.length}
@ -138,6 +138,7 @@
{:else}
<Input disabled />
{/if}
<div class="controls">
<Icon
name="Duplicate"
hoverable
@ -150,6 +151,7 @@
size="S"
on:click={() => removeFilter(filter.id)}
/>
</div>
{/each}
</div>
{/if}
@ -159,8 +161,7 @@
</Button>
</div>
</Layout>
</div>
</DrawerContent>
</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 {
// 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.0",
"version": "1.0.91-alpha.16",
"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.0",
"@budibase/bbui": "^1.0.91-alpha.16",
"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.0",
"version": "1.0.91-alpha.16",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@ -71,12 +71,13 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.91-alpha.0",
"@budibase/client": "^1.0.91-alpha.0",
"@budibase/string-templates": "^1.0.91-alpha.0",
"@budibase/backend-core": "^1.0.91-alpha.16",
"@budibase/client": "^1.0.91-alpha.16",
"@budibase/string-templates": "^1.0.91-alpha.16",
"@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0",
"@google-cloud/firestore": "^5.0.2",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",
"@sentry/node": "^6.0.0",
@ -148,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",
@ -157,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,16 +52,14 @@ 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()
}
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)
)
ctx.request.body = await addRev(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
// in the public API to be correct

View File

@ -2,6 +2,7 @@ import { queryValidation } from "../validation"
import { generateQueryID } from "../../../../db/utils"
import { ImportInfo, ImportSource } from "./sources/base"
import { OpenAPI2 } from "./sources/openapi2"
import { OpenAPI3 } from "./sources/openapi3"
import { Query } from "./../../../../definitions/common"
import { Curl } from "./sources/curl"
// @ts-ignore
@ -18,7 +19,7 @@ export class RestImporter {
constructor(data: string) {
this.data = data
this.sources = [new OpenAPI2(), new Curl()]
this.sources = [new OpenAPI2(), new OpenAPI3(), new Curl()]
}
init = async () => {

View File

@ -23,7 +23,7 @@ export abstract class ImportSource {
name: string,
method: string,
path: string,
url: URL,
url: URL | string | undefined,
queryString: string,
headers: object = {},
parameters: QueryParameter[] = [],
@ -34,7 +34,17 @@ export abstract class ImportSource {
const transformer = "return data"
const schema = {}
path = this.processPath(path)
path = `${url.origin}/${path}`
if (url) {
if (typeof url === "string") {
path = `${url}/${path}`
} else {
let href = url.href
if (href.endsWith("/")) {
href = href.slice(0, -1)
}
path = `${href}/${path}`
}
}
queryString = this.processQuery(queryString)
const requestBody = JSON.stringify(body, null, 2)

View File

@ -74,7 +74,7 @@ export class Curl extends ImportSource {
getQueries = async (datasourceId: string): Promise<Query[]> => {
const url = this.getUrl()
const name = url.pathname
const path = url.pathname
const path = url.origin + url.pathname
const method = this.curl.method
const queryString = url.search
const headers = this.curl.headers
@ -90,7 +90,7 @@ export class Curl extends ImportSource {
name,
method,
path,
url,
undefined,
queryString,
headers,
[],

View File

@ -0,0 +1,205 @@
import { ImportInfo } from "./base"
import { Query, QueryParameter } from "../../../../../definitions/datasource"
import { OpenAPIV3 } from "openapi-types"
import { OpenAPISource } from "./base/openapi"
import { URL } from "url"
const parameterNotRef = (
param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject
): param is OpenAPIV3.ParameterObject => {
// all refs are deferenced by parser library
return true
}
const requestBodyNotRef = (
param: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject | undefined
): param is OpenAPIV3.RequestBodyObject => {
// all refs are deferenced by parser library
return param !== undefined
}
const schemaNotRef = (
param: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined
): param is OpenAPIV3.SchemaObject => {
// all refs are deferenced by parser library
return param !== undefined
}
const isOpenAPI3 = (document: any): document is OpenAPIV3.Document => {
return document.openapi.includes("3.0")
}
const methods: string[] = Object.values(OpenAPIV3.HttpMethods)
const isOperation = (
key: string,
pathItem: any
): pathItem is OpenAPIV3.OperationObject => {
return methods.includes(key)
}
const isParameter = (
key: string,
pathItem: any
): pathItem is OpenAPIV3.ParameterObject => {
return !isOperation(key, pathItem)
}
const getRequestBody = (operation: OpenAPIV3.OperationObject) => {
if (requestBodyNotRef(operation.requestBody)) {
const request: OpenAPIV3.RequestBodyObject = operation.requestBody
const supportedMimeTypes = getMimeTypes(operation)
if (supportedMimeTypes.length > 0) {
const mimeType = supportedMimeTypes[0]
// try get example from request
const content = request.content[mimeType]
if (content.example) {
return content.example
}
// try get example from schema
if (schemaNotRef(content.schema)) {
const schema = content.schema
if (schema.example) {
return schema.example
}
}
}
}
return undefined
}
const getMimeTypes = (operation: OpenAPIV3.OperationObject): string[] => {
if (requestBodyNotRef(operation.requestBody)) {
const request: OpenAPIV3.RequestBodyObject = operation.requestBody
return Object.keys(request.content)
}
return []
}
/**
* OpenAPI Version 3.0
* https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md
*/
export class OpenAPI3 extends OpenAPISource {
document!: OpenAPIV3.Document
isSupported = async (data: string): Promise<boolean> => {
try {
const document: any = await this.parseData(data)
if (isOpenAPI3(document)) {
this.document = document
return true
} else {
return false
}
} catch (err) {
return false
}
}
getInfo = async (): Promise<ImportInfo> => {
const name = this.document.info.title || "OpenAPI Import"
return {
name,
}
}
getQueries = async (datasourceId: string): Promise<Query[]> => {
let url: string | URL | undefined
if (this.document.servers?.length) {
url = this.document.servers[0].url
try {
url = new URL(url)
} catch (err) {
// unable to construct url, e.g. with variables
// proceed with string form of url
}
}
const queries: Query[] = []
for (let [path, pathItemObject] of Object.entries(this.document.paths)) {
// parameters that apply to every operation in the path
let pathParams: OpenAPIV3.ParameterObject[] = []
// pathItemObject can be undefined
if (!pathItemObject) {
continue
}
for (let [key, opOrParams] of Object.entries(pathItemObject)) {
if (isParameter(key, opOrParams)) {
const pathParameters = opOrParams as OpenAPIV3.ParameterObject[]
pathParams.push(...pathParameters)
continue
}
// can not be a parameter, must be an operation
const operation = opOrParams as OpenAPIV3.OperationObject
const methodName = key
const name = operation.operationId || path
let queryString = ""
const headers: any = {}
let requestBody = getRequestBody(operation)
const parameters: QueryParameter[] = []
const mimeTypes = getMimeTypes(operation)
if (mimeTypes.length > 0) {
headers["Content-Type"] = mimeTypes[0]
}
// combine the path parameters with the operation parameters
const operationParams = operation.parameters || []
const allParams = [...pathParams, ...operationParams]
for (let param of allParams) {
if (parameterNotRef(param)) {
switch (param.in) {
case "query":
let prefix = ""
if (queryString) {
prefix = "&"
}
queryString = `${queryString}${prefix}${param.name}={{${param.name}}}`
break
case "header":
headers[param.name] = `{{${param.name}}}`
break
case "path":
// do nothing: param is already in the path
break
case "formData":
// future enhancement
break
}
// add the parameter if it can be bound in our config
if (["query", "header", "path"].includes(param.in)) {
parameters.push({
name: param.name,
default: "",
})
}
}
}
const query = this.constructQuery(
datasourceId,
name,
methodName,
path,
url,
queryString,
headers,
parameters,
requestBody
)
queries.push(query)
}
}
return queries
}
}

View File

@ -0,0 +1,253 @@
{
"openapi": "3.0.2",
"info": {
"description": "A basic swagger file",
"version": "1.0.0",
"title": "CRUD"
},
"servers": [
{
"url": "http://example.com"
}
],
"tags": [
{
"name": "entity"
}
],
"paths": {
"/entities": {
"post": {
"tags": [
"entity"
],
"operationId": "createEntity",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateEntity"
}
}
}
},
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Entity"
}
}
}
}
}
},
"get": {
"tags": [
"entity"
],
"operationId": "getEntities",
"parameters": [
{
"$ref": "#/components/parameters/PageParameter"
},
{
"$ref": "#/components/parameters/SizeParameter"
}
],
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Entities"
}
}
}
}
}
}
},
"/entities/{entityId}": {
"parameters": [
{
"$ref": "#/components/parameters/EntityIdParameter"
}
],
"get": {
"tags": [
"entity"
],
"operationId": "getEntity",
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Entity"
}
}
}
}
}
},
"put": {
"tags": [
"entity"
],
"operationId": "updateEntity",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Entity"
}
}
}
},
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Entity"
}
}
}
}
}
},
"patch": {
"tags": [
"entity"
],
"operationId": "patchEntity",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Entity"
}
}
}
},
"responses": {
"200": {
"description": "successful operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Entity"
}
}
}
}
}
},
"delete": {
"tags": [
"entity"
],
"parameters": [
{
"$ref": "#/components/parameters/APIKeyParameter"
}
],
"operationId": "deleteEntity",
"responses": {
"204": {
"description": "successful operation"
}
}
}
}
},
"components": {
"parameters": {
"EntityIdParameter": {
"schema": {
"type": "integer",
"format": "int64"
},
"name": "entityId",
"in": "path",
"required": true
},
"PageParameter": {
"schema": {
"type": "integer",
"format": "int32"
},
"name": "page",
"in": "query",
"required": false
},
"SizeParameter": {
"schema": {
"type": "integer",
"format": "int32"
},
"name": "size",
"in": "query",
"required": false
},
"APIKeyParameter": {
"schema": {
"type": "string"
},
"name": "x-api-key",
"in": "header",
"required": false
}
},
"schemas": {
"CreateEntity": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"type": {
"type": "string"
}
},
"example": {
"name": "name",
"type": "type"
}
},
"Entity": {
"allOf": [
{
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
}
}
},
{
"$ref": "#/components/schemas/CreateEntity"
}
],
"example": {
"id": 1,
"name": "name",
"type": "type"
}
},
"Entities" : {
"type": "array",
"items": {
"$ref": "#/components/schemas/Entity"
}
}
}
}
}

View File

@ -0,0 +1,153 @@
---
openapi: 3.0.2
info:
description: A basic swagger file
version: 1.0.0
title: CRUD
servers:
- url: http://example.com
tags:
- name: entity
paths:
"/entities":
post:
tags:
- entity
operationId: createEntity
requestBody:
content:
application/json:
schema:
"$ref": "#/components/schemas/CreateEntity"
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Entity"
get:
tags:
- entity
operationId: getEntities
parameters:
- "$ref": "#/components/parameters/PageParameter"
- "$ref": "#/components/parameters/SizeParameter"
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Entities"
"/entities/{entityId}":
parameters:
- "$ref": "#/components/parameters/EntityIdParameter"
get:
tags:
- entity
operationId: getEntity
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Entity"
put:
tags:
- entity
operationId: updateEntity
requestBody:
content:
application/json:
schema:
"$ref": "#/components/schemas/Entity"
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Entity"
patch:
tags:
- entity
operationId: patchEntity
requestBody:
content:
application/json:
schema:
"$ref": "#/components/schemas/Entity"
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Entity"
delete:
tags:
- entity
parameters:
- "$ref": "#/components/parameters/APIKeyParameter"
operationId: deleteEntity
responses:
'204':
description: successful operation
components:
parameters:
EntityIdParameter:
schema:
type: integer
format: int64
name: entityId
in: path
required: true
PageParameter:
schema:
type: integer
format: int32
name: page
in: query
required: false
SizeParameter:
schema:
type: integer
format: int32
name: size
in: query
required: false
APIKeyParameter:
schema:
type: string
name: x-api-key
in: header
required: false
schemas:
CreateEntity:
type: object
properties:
name:
type: string
type:
type: string
example:
name: name
type: type
Entity:
allOf:
- type: object
properties:
id:
type: integer
format: int64
- "$ref": "#/components/schemas/CreateEntity"
example:
id: 1
name: name
type: type
Entities:
type: array
items:
"$ref": "#/components/schemas/Entity"

View File

@ -0,0 +1,804 @@
---
openapi: 3.0.2
info:
title: Swagger Petstore - OpenAPI 3.0
description: |-
This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
You can now help us improve the API whether it's by making changes to the definition itself or to the code.
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
Some useful links:
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
termsOfService: http://swagger.io/terms/
contact:
email: apiteam@swagger.io
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
version: 1.0.11
externalDocs:
description: Find out more about Swagger
url: http://swagger.io
servers:
- url: "/api/v3"
tags:
- name: pet
description: Everything about your Pets
externalDocs:
description: Find out more
url: http://swagger.io
- name: store
description: Access to Petstore orders
externalDocs:
description: Find out more about our store
url: http://swagger.io
- name: user
description: Operations about user
paths:
"/pet":
put:
tags:
- pet
summary: Update an existing pet
description: Update an existing pet by Id
operationId: updatePet
requestBody:
description: Update an existent pet in the store
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/Pet"
required: true
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
application/json:
schema:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- write:pets
- read:pets
post:
tags:
- pet
summary: Add a new pet to the store
description: Add a new pet to the store
operationId: addPet
requestBody:
description: Create a new pet in the store
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/Pet"
required: true
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
application/json:
schema:
"$ref": "#/components/schemas/Pet"
'405':
description: Invalid input
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/findByStatus":
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: false
explode: true
schema:
type: string
default: available
enum:
- available
- pending
- sold
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid status value
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/findByTags":
get:
tags:
- pet
summary: Finds Pets by tags
description: Multiple tags can be provided with comma separated strings. Use
tag1, tag2, tag3 for testing.
operationId: findPetsByTags
parameters:
- name: tags
in: query
description: Tags to filter by
required: false
explode: true
schema:
type: array
items:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid tag value
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/{petId}":
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
application/json:
schema:
"$ref": "#/components/schemas/Pet"
'400':
description: Invalid ID supplied
'404':
description: Pet not found
security:
- api_key: []
- petstore_auth:
- write:pets
- read:pets
post:
tags:
- pet
summary: Updates a pet in the store with form data
description: ''
operationId: updatePetWithForm
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
schema:
type: integer
format: int64
- name: name
in: query
description: Name of pet that needs to be updated
schema:
type: string
- name: status
in: query
description: Status of pet that needs to be updated
schema:
type: string
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- write:pets
- read:pets
delete:
tags:
- pet
summary: Deletes a pet
description: ''
operationId: deletePet
parameters:
- name: api_key
in: header
description: ''
required: false
schema:
type: string
- name: petId
in: path
description: Pet id to delete
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid pet value
security:
- petstore_auth:
- write:pets
- read:pets
"/pet/{petId}/uploadImage":
post:
tags:
- pet
summary: uploads an image
description: ''
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
- name: additionalMetadata
in: query
description: Additional Metadata
required: false
schema:
type: string
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/ApiResponse"
security:
- petstore_auth:
- write:pets
- read:pets
"/store/inventory":
get:
tags:
- store
summary: Returns pet inventories by status
description: Returns a map of status codes to quantities
operationId: getInventory
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
security:
- api_key: []
"/store/order":
post:
tags:
- store
summary: Place an order for a pet
description: Place a new order in the store
operationId: placeOrder
requestBody:
content:
application/json:
schema:
"$ref": "#/components/schemas/Order"
application/xml:
schema:
"$ref": "#/components/schemas/Order"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/Order"
responses:
'200':
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/Order"
'405':
description: Invalid input
"/store/order/{orderId}":
get:
tags:
- store
summary: Find purchase order by ID
description: For valid response try integer IDs with value <= 5 or > 10. Other
values will generate exceptions.
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of order that needs to be fetched
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/xml:
schema:
"$ref": "#/components/schemas/Order"
application/json:
schema:
"$ref": "#/components/schemas/Order"
'400':
description: Invalid ID supplied
'404':
description: Order not found
delete:
tags:
- store
summary: Delete purchase order by ID
description: For valid response try integer IDs with value < 1000. Anything
above 1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid ID supplied
'404':
description: Order not found
"/user":
post:
tags:
- user
summary: Create user
description: This can only be done by the logged in user.
operationId: createUser
requestBody:
description: Created user object
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/User"
responses:
default:
description: successful operation
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
"/user/createWithList":
post:
tags:
- user
summary: Creates list of users with given input array
description: Creates list of users with given input array
operationId: createUsersWithListInput
requestBody:
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/User"
responses:
'200':
description: Successful operation
content:
application/xml:
schema:
"$ref": "#/components/schemas/User"
application/json:
schema:
"$ref": "#/components/schemas/User"
default:
description: successful operation
"/user/login":
get:
tags:
- user
summary: Logs user into the system
description: ''
operationId: loginUser
parameters:
- name: username
in: query
description: The user name for login
required: false
schema:
type: string
- name: password
in: query
description: The password for login in clear text
required: false
schema:
type: string
responses:
'200':
description: successful operation
headers:
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when token expires
schema:
type: string
format: date-time
content:
application/xml:
schema:
type: string
application/json:
schema:
type: string
'400':
description: Invalid username/password supplied
"/user/logout":
get:
tags:
- user
summary: Logs out current logged in user session
description: ''
operationId: logoutUser
parameters: []
responses:
default:
description: successful operation
"/user/{username}":
get:
tags:
- user
summary: Get user by user name
description: ''
operationId: getUserByName
parameters:
- name: username
in: path
description: 'The name that needs to be fetched. Use user1 for testing. '
required: true
schema:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
"$ref": "#/components/schemas/User"
application/json:
schema:
"$ref": "#/components/schemas/User"
'400':
description: Invalid username supplied
'404':
description: User not found
put:
tags:
- user
summary: Update user
description: This can only be done by the logged in user.
operationId: updateUser
parameters:
- name: username
in: path
description: name that need to be deleted
required: true
schema:
type: string
requestBody:
description: Update an existent user in the store
content:
application/json:
schema:
"$ref": "#/components/schemas/User"
application/xml:
schema:
"$ref": "#/components/schemas/User"
application/x-www-form-urlencoded:
schema:
"$ref": "#/components/schemas/User"
responses:
default:
description: successful operation
delete:
tags:
- user
summary: Delete user
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid username supplied
'404':
description: User not found
components:
schemas:
Order:
type: object
properties:
id:
type: integer
format: int64
example: 10
petId:
type: integer
format: int64
example: 198772
quantity:
type: integer
format: int32
example: 7
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
example: approved
enum:
- placed
- approved
- delivered
complete:
type: boolean
xml:
name: order
Customer:
type: object
properties:
id:
type: integer
format: int64
example: 100000
username:
type: string
example: fehguy
address:
type: array
xml:
name: addresses
wrapped: true
items:
"$ref": "#/components/schemas/Address"
xml:
name: customer
Address:
type: object
properties:
street:
type: string
example: 437 Lytton
city:
type: string
example: Palo Alto
state:
type: string
example: CA
zip:
type: string
example: '94301'
xml:
name: address
Category:
type: object
properties:
id:
type: integer
format: int64
example: 1
name:
type: string
example: Dogs
xml:
name: category
User:
type: object
properties:
id:
type: integer
format: int64
example: 10
username:
type: string
example: theUser
firstName:
type: string
example: John
lastName:
type: string
example: James
email:
type: string
example: john@email.com
password:
type: string
example: '12345'
phone:
type: string
example: '12345'
userStatus:
type: integer
description: User Status
format: int32
example: 1
xml:
name: user
Tag:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: tag
Pet:
required:
- name
- photoUrls
type: object
properties:
id:
type: integer
format: int64
example: 10
name:
type: string
example: doggie
category:
"$ref": "#/components/schemas/Category"
photoUrls:
type: array
xml:
wrapped: true
items:
type: string
xml:
name: photoUrl
tags:
type: array
xml:
wrapped: true
items:
"$ref": "#/components/schemas/Tag"
status:
type: string
description: pet status in the store
enum:
- available
- pending
- sold
xml:
name: pet
ApiResponse:
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
xml:
name: "##default"
requestBodies:
Pet:
description: Pet object that needs to be added to the store
content:
application/json:
schema:
"$ref": "#/components/schemas/Pet"
application/xml:
schema:
"$ref": "#/components/schemas/Pet"
UserArray:
description: List of user object
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/User"
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: https://petstore3.swagger.io/oauth/authorize
scopes:
write:pets: modify pets in your account
read:pets: read your pets
api_key:
type: apiKey
name: api_key
in: header

View File

@ -0,0 +1,240 @@
const fs = require("fs")
const path = require("path")
const { OpenAPI3 } = require("../../openapi3")
const getData = (file, extension) => {
return fs.readFileSync(
path.join(__dirname, `./data/${file}/${file}.${extension}`),
"utf8"
)
}
describe("OpenAPI3 Import", () => {
let openapi3
beforeEach(() => {
openapi3 = new OpenAPI3()
})
it("validates unsupported data", async () => {
let data
let supported
// non json / yaml
data = "curl http://example.com"
supported = await openapi3.isSupported(data)
expect(supported).toBe(false)
// Empty
data = ""
supported = await openapi3.isSupported(data)
expect(supported).toBe(false)
})
const runTests = async (filename, test, assertions) => {
for (let extension of ["json", "yaml"]) {
await test(filename, extension, assertions)
}
}
const testImportInfo = async (file, extension) => {
await openapi3.isSupported(getData(file, extension))
const info = await openapi3.getInfo()
expect(info.name).toBe("Swagger Petstore - OpenAPI 3.0")
}
it("returns import info", async () => {
await runTests("petstore", testImportInfo)
})
describe("Returns queries", () => {
const indexQueries = queries => {
return queries.reduce((acc, query) => {
acc[query.name] = query
return acc
}, {})
}
const getQueries = async (file, extension) => {
await openapi3.isSupported(getData(file, extension))
const queries = await openapi3.getQueries()
expect(queries.length).toBe(6)
return indexQueries(queries)
}
const testVerb = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, method] of Object.entries(assertions)) {
expect(queries[operationId].queryVerb).toBe(method)
}
}
it("populates verb", async () => {
const assertions = {
createEntity: "create",
getEntities: "read",
getEntity: "read",
updateEntity: "update",
patchEntity: "patch",
deleteEntity: "delete",
}
await runTests("crud", testVerb, assertions)
})
const testPath = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, urlPath] of Object.entries(assertions)) {
expect(queries[operationId].fields.path).toBe(urlPath)
}
}
it("populates path", async () => {
const assertions = {
createEntity: "http://example.com/entities",
getEntities: "http://example.com/entities",
getEntity: "http://example.com/entities/{{entityId}}",
updateEntity: "http://example.com/entities/{{entityId}}",
patchEntity: "http://example.com/entities/{{entityId}}",
deleteEntity: "http://example.com/entities/{{entityId}}",
}
await runTests("crud", testPath, assertions)
})
const testHeaders = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, headers] of Object.entries(assertions)) {
expect(queries[operationId].fields.headers).toStrictEqual(headers)
}
}
const contentTypeHeader = {
"Content-Type": "application/json",
}
it("populates headers", async () => {
const assertions = {
createEntity: {
...contentTypeHeader,
},
getEntities: {},
getEntity: {},
updateEntity: {
...contentTypeHeader,
},
patchEntity: {
...contentTypeHeader,
},
deleteEntity: {
"x-api-key": "{{x-api-key}}",
},
}
await runTests("crud", testHeaders, assertions)
})
const testQuery = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, queryString] of Object.entries(assertions)) {
expect(queries[operationId].fields.queryString).toStrictEqual(
queryString
)
}
}
it("populates query", async () => {
const assertions = {
createEntity: "",
getEntities: "page={{page}}&size={{size}}",
getEntity: "",
updateEntity: "",
patchEntity: "",
deleteEntity: "",
}
await runTests("crud", testQuery, assertions)
})
const testParameters = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, parameters] of Object.entries(assertions)) {
expect(queries[operationId].parameters).toStrictEqual(parameters)
}
}
it("populates parameters", async () => {
const assertions = {
createEntity: [],
getEntities: [
{
name: "page",
default: "",
},
{
name: "size",
default: "",
},
],
getEntity: [
{
name: "entityId",
default: "",
},
],
updateEntity: [
{
name: "entityId",
default: "",
},
],
patchEntity: [
{
name: "entityId",
default: "",
},
],
deleteEntity: [
{
name: "entityId",
default: "",
},
{
name: "x-api-key",
default: "",
},
],
}
await runTests("crud", testParameters, assertions)
})
const testBody = async (file, extension, assertions) => {
const queries = await getQueries(file, extension)
for (let [operationId, body] of Object.entries(assertions)) {
expect(queries[operationId].fields.requestBody).toStrictEqual(
JSON.stringify(body, null, 2)
)
}
}
it("populates body", async () => {
const assertions = {
createEntity: {
name: "name",
type: "type",
},
getEntities: undefined,
getEntity: undefined,
updateEntity: {
id: 1,
name: "name",
type: "type",
},
patchEntity: {
id: 1,
name: "name",
type: "type",
},
deleteEntity: undefined,
}
await runTests("crud", testBody, assertions)
})
})
})

View File

@ -23,14 +23,27 @@ const oapi2CrudYaml = getData("openapi2/data/crud/crud.json")
const oapi2PetstoreJson = getData("openapi2/data/petstore/petstore.json")
const oapi2PetstoreYaml = getData("openapi2/data/petstore/petstore.json")
// openapi3
const oapi3CrudJson = getData("openapi3/data/crud/crud.json")
const oapi3CrudYaml = getData("openapi3/data/crud/crud.json")
const oapi3PetstoreJson = getData("openapi3/data/petstore/petstore.json")
const oapi3PetstoreYaml = getData("openapi3/data/petstore/petstore.json")
// curl
const curl = getData("curl/data/post.txt")
const datasets = {
// openapi2 (swagger)
oapi2CrudJson,
oapi2CrudYaml,
oapi2PetstoreJson,
oapi2PetstoreYaml,
// openapi3
oapi3CrudJson,
oapi3CrudYaml,
oapi3PetstoreJson,
oapi3PetstoreYaml,
// curl
curl
}
@ -56,6 +69,7 @@ describe("Rest Importer", () => {
it("gets info", async () => {
const assertions = {
// openapi2 (swagger)
"oapi2CrudJson" : {
name: "CRUD",
},
@ -68,6 +82,20 @@ describe("Rest Importer", () => {
"oapi2PetstoreYaml" :{
name: "Swagger Petstore",
},
// openapi3
"oapi3CrudJson" : {
name: "CRUD",
},
"oapi3CrudYaml" : {
name: "CRUD",
},
"oapi3PetstoreJson" : {
name: "Swagger Petstore - OpenAPI 3.0",
},
"oapi3PetstoreYaml" :{
name: "Swagger Petstore - OpenAPI 3.0",
},
// curl
"curl": {
name: "example.com",
}
@ -89,6 +117,7 @@ describe("Rest Importer", () => {
// simple sanity assertions that the whole dataset
// makes it through the importer
const assertions = {
// openapi2 (swagger)
"oapi2CrudJson" : {
count: 6,
},
@ -101,6 +130,20 @@ describe("Rest Importer", () => {
"oapi2PetstoreYaml" :{
count: 20,
},
// openapi3
"oapi3CrudJson" : {
count: 6,
},
"oapi3CrudYaml" :{
count: 6,
},
"oapi3PetstoreJson" : {
count: 19,
},
"oapi3PetstoreYaml" :{
count: 19,
},
// curl
"curl": {
count: 1
}

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

@ -48,6 +48,7 @@ export enum SourceNames {
REST = "REST",
ORACLE = "ORACLE",
GOOGLE_SHEETS = "GOOGLE_SHEETS",
FIREBASE = "FIREBASE",
}
export enum IncludeRelationships {

File diff suppressed because it is too large Load Diff

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)
}
})
}
if (filters.equal) {

View File

@ -0,0 +1,205 @@
import {
DatasourceFieldTypes,
Integration,
QueryTypes,
} from "../definitions/datasource"
import { IntegrationBase } from "./base/IntegrationBase"
import { Firestore, WhereFilterOp } from "@google-cloud/firestore"
module Firebase {
interface FirebaseConfig {
email: string
privateKey: string
projectId: string
serviceAccount?: string
}
const SCHEMA: Integration = {
docs: "https://firebase.google.com/docs/firestore/quickstart",
friendlyName: "Firestore",
description:
"Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.",
datasource: {
email: {
type: DatasourceFieldTypes.STRING,
required: true,
},
privateKey: {
type: DatasourceFieldTypes.STRING,
required: true,
},
projectId: {
type: DatasourceFieldTypes.STRING,
required: true,
},
serviceAccount: {
type: DatasourceFieldTypes.JSON,
required: false,
},
},
query: {
create: {
type: QueryTypes.JSON,
},
read: {
type: QueryTypes.JSON,
},
update: {
type: QueryTypes.JSON,
},
delete: {
type: QueryTypes.JSON,
},
},
extra: {
collection: {
displayName: "Collection",
type: DatasourceFieldTypes.STRING,
required: true,
},
filterField: {
displayName: "Filter field",
type: DatasourceFieldTypes.STRING,
required: false,
},
filter: {
displayName: "Filter comparison",
type: DatasourceFieldTypes.LIST,
required: false,
data: {
read: [
"==",
"<",
"<=",
"==",
"!=",
">=",
">",
"array-contains",
"in",
"not-in",
"array-contains-any",
],
},
},
filterValue: {
displayName: "Filter value",
type: DatasourceFieldTypes.STRING,
required: false,
},
},
}
class FirebaseIntegration implements IntegrationBase {
private config: FirebaseConfig
private db: Firestore
constructor(config: FirebaseConfig) {
this.config = config
if (config.serviceAccount) {
const serviceAccount = JSON.parse(config.serviceAccount)
this.db = new Firestore({
projectId: serviceAccount.project_id,
credentials: {
client_email: serviceAccount.client_email,
private_key: serviceAccount.private_key,
},
})
} else {
this.db = new Firestore({
projectId: config.projectId,
credentials: {
client_email: config.email,
private_key: config.privateKey,
},
})
}
}
async create(query: { json: object; extra: { [key: string]: string } }) {
try {
const documentReference = this.db
.collection(query.extra.collection)
.doc()
await documentReference.set({ ...query.json, id: documentReference.id })
const snapshot = await documentReference.get()
return snapshot.data()
} catch (err) {
console.error("Error writing to Firestore", err)
throw err
}
}
async read(query: { json: object; extra: { [key: string]: string } }) {
try {
let snapshot
const collectionRef = this.db.collection(query.extra.collection)
if (
query.extra.filterField &&
query.extra.filter &&
query.extra.filterValue
) {
snapshot = await collectionRef
.where(
query.extra.filterField,
query.extra.filter as WhereFilterOp,
query.extra.filterValue
)
.get()
} else {
snapshot = await collectionRef.get()
}
const result: any[] = []
snapshot.forEach(doc => result.push(doc.data()))
return result
} catch (err) {
console.error("Error querying Firestore", err)
throw err
}
}
async update(query: {
json: Record<string, any>
extra: { [key: string]: string }
}) {
try {
await this.db
.collection(query.extra.collection)
.doc(query.json.id)
.update(query.json)
return (
await this.db
.collection(query.extra.collection)
.doc(query.json.id)
.get()
).data()
} catch (err) {
console.error("Error writing to firebase", err)
throw err
}
}
async delete(query: {
json: { id: string }
extra: { [key: string]: string }
}) {
try {
await this.db
.collection(query.extra.collection)
.doc(query.json.id)
.delete()
return true
} catch (err) {
console.error("Error writing to mongodb", err)
throw err
}
}
}
module.exports = {
schema: SCHEMA,
integration: FirebaseIntegration,
}
}

View File

@ -10,6 +10,7 @@ const mysql = require("./mysql")
const arangodb = require("./arangodb")
const rest = require("./rest")
const googlesheets = require("./googlesheets")
const firebase = require("./firebase")
const { SourceNames } = require("../definitions/datasource")
const environment = require("../environment")
@ -25,6 +26,7 @@ const DEFINITIONS = {
[SourceNames.MYSQL]: mysql.schema,
[SourceNames.ARANGODB]: arangodb.schema,
[SourceNames.REST]: rest.schema,
[SourceNames.FIREBASE]: firebase.schema,
}
const INTEGRATIONS = {
@ -39,6 +41,7 @@ const INTEGRATIONS = {
[SourceNames.MYSQL]: mysql.integration,
[SourceNames.ARANGODB]: arangodb.integration,
[SourceNames.REST]: rest.integration,
[SourceNames.FIREBASE]: firebase.integration,
}
// optionally add oracle integration if the oracle binary can be installed

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}"`
})
})
})

Some files were not shown because too many files have changed in this diff Show More