commit
1a6ffbe655
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.9.146",
|
"version": "0.9.147-alpha.0",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "0.9.146",
|
"version": "0.9.147-alpha.0",
|
||||||
"description": "Authentication middlewares for budibase builder and apps",
|
"description": "Authentication middlewares for budibase builder and apps",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -12,7 +12,7 @@ const populateFromDB = async (userId, tenantId) => {
|
||||||
const user = await getGlobalDB(tenantId).get(userId)
|
const user = await getGlobalDB(tenantId).get(userId)
|
||||||
user.budibaseAccess = true
|
user.budibaseAccess = true
|
||||||
|
|
||||||
if (!env.SELF_HOSTED) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
const account = await accounts.getAccount(user.email)
|
const account = await accounts.getAccount(user.email)
|
||||||
if (account) {
|
if (account) {
|
||||||
user.account = account
|
user.account = account
|
||||||
|
|
|
@ -21,6 +21,7 @@ module.exports = {
|
||||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
||||||
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
||||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||||
isTest,
|
isTest,
|
||||||
|
|
|
@ -19,6 +19,22 @@ const removeTenantFromInfoDB = async tenantId => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.removeUserFromInfoDB = async dbUser => {
|
||||||
|
const infoDb = getDB(PLATFORM_INFO_DB)
|
||||||
|
const keys = [dbUser._id, dbUser.email]
|
||||||
|
const userDocs = await infoDb.allDocs({
|
||||||
|
keys,
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
const toDelete = userDocs.rows.map(row => {
|
||||||
|
return {
|
||||||
|
...row.doc,
|
||||||
|
_deleted: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await infoDb.bulkDocs(toDelete)
|
||||||
|
}
|
||||||
|
|
||||||
const removeUsersFromInfoDB = async tenantId => {
|
const removeUsersFromInfoDB = async tenantId => {
|
||||||
try {
|
try {
|
||||||
const globalDb = getGlobalDB(tenantId)
|
const globalDb = getGlobalDB(tenantId)
|
||||||
|
|
|
@ -73,7 +73,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => {
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getGlobalDB = (tenantId = null) => {
|
exports.getGlobalDBName = (tenantId = null) => {
|
||||||
// tenant ID can be set externally, for example user API where
|
// tenant ID can be set externally, for example user API where
|
||||||
// new tenants are being created, this may be the case
|
// new tenants are being created, this may be the case
|
||||||
if (!tenantId) {
|
if (!tenantId) {
|
||||||
|
@ -81,13 +81,16 @@ exports.getGlobalDB = (tenantId = null) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let dbName
|
let dbName
|
||||||
|
|
||||||
if (tenantId === DEFAULT_TENANT_ID) {
|
if (tenantId === DEFAULT_TENANT_ID) {
|
||||||
dbName = StaticDatabases.GLOBAL.name
|
dbName = StaticDatabases.GLOBAL.name
|
||||||
} else {
|
} else {
|
||||||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
||||||
}
|
}
|
||||||
|
return dbName
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getGlobalDB = (tenantId = null) => {
|
||||||
|
const dbName = exports.getGlobalDBName(tenantId)
|
||||||
return getDB(dbName)
|
return getDB(dbName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "0.9.146",
|
"version": "0.9.147-alpha.0",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
export let showConfirmButton = true
|
export let showConfirmButton = true
|
||||||
export let showCloseIcon = true
|
export let showCloseIcon = true
|
||||||
export let onConfirm = undefined
|
export let onConfirm = undefined
|
||||||
|
export let onCancel = undefined
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let showDivider = true
|
export let showDivider = true
|
||||||
|
|
||||||
|
@ -28,6 +29,14 @@
|
||||||
}
|
}
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function close() {
|
||||||
|
loading = true
|
||||||
|
if (!onCancel || (await onCancel()) !== false) {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -65,7 +74,7 @@
|
||||||
>
|
>
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
{#if showCancelButton}
|
{#if showCancelButton}
|
||||||
<Button group secondary on:click={cancel}>{cancelText}</Button>
|
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showConfirmButton}
|
{#if showConfirmButton}
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
export let selectedRows = []
|
export let selectedRows = []
|
||||||
export let editColumnTitle = "Edit"
|
export let editColumnTitle = "Edit"
|
||||||
export let customRenderers = []
|
export let customRenderers = []
|
||||||
|
export let disableSorting = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset state when data changes
|
// Reset state when data changes
|
||||||
$: data.length, reset()
|
$: rows.length, reset()
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
nextScrollTop = 0
|
nextScrollTop = 0
|
||||||
scrollTop = 0
|
scrollTop = 0
|
||||||
|
@ -107,7 +108,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortRows = (rows, sortColumn, sortOrder) => {
|
const sortRows = (rows, sortColumn, sortOrder) => {
|
||||||
if (!sortColumn || !sortOrder) {
|
if (!sortColumn || !sortOrder || disableSorting) {
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
return rows.slice().sort((a, b) => {
|
return rows.slice().sort((a, b) => {
|
||||||
|
@ -131,6 +132,7 @@
|
||||||
sortColumn = fieldSchema.name
|
sortColumn = fieldSchema.name
|
||||||
sortOrder = "Descending"
|
sortOrder = "Descending"
|
||||||
}
|
}
|
||||||
|
dispatch("sort", { column: sortColumn, order: sortOrder })
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDisplayName = schema => {
|
const getDisplayName = schema => {
|
||||||
|
|
|
@ -6,7 +6,7 @@ context("Create a Table", () => {
|
||||||
|
|
||||||
it("should create a new Table", () => {
|
it("should create a new Table", () => {
|
||||||
cy.createTable("dog")
|
cy.createTable("dog")
|
||||||
|
cy.wait(1000)
|
||||||
// Check if Table exists
|
// Check if Table exists
|
||||||
cy.get(".table-title h1").should("have.text", "dog")
|
cy.get(".table-title h1").should("have.text", "dog")
|
||||||
})
|
})
|
||||||
|
@ -36,7 +36,8 @@ context("Create a Table", () => {
|
||||||
it("edits a row", () => {
|
it("edits a row", () => {
|
||||||
cy.contains("button", "Edit").click({ force: true })
|
cy.contains("button", "Edit").click({ force: true })
|
||||||
cy.wait(1000)
|
cy.wait(1000)
|
||||||
cy.get(".spectrum-Modal input").type("Updated")
|
cy.get(".spectrum-Modal input").clear()
|
||||||
|
cy.get(".spectrum-Modal input").type("RoverUpdated")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
|
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
|
||||||
})
|
})
|
||||||
|
@ -62,7 +63,7 @@ context("Create a Table", () => {
|
||||||
|
|
||||||
it("deletes a table", () => {
|
it("deletes a table", () => {
|
||||||
cy.get(".actions > :nth-child(1) > .icon > .spectrum-Icon > use")
|
cy.get(".actions > :nth-child(1) > .icon > .spectrum-Icon > use")
|
||||||
.first()
|
.eq(1)
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
cy.get(".spectrum-Menu > :nth-child(2)").click()
|
cy.get(".spectrum-Menu > :nth-child(2)").click()
|
||||||
cy.contains("Delete Table").click()
|
cy.contains("Delete Table").click()
|
||||||
|
|
|
@ -35,8 +35,11 @@ Cypress.Commands.add("createApp", name => {
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||||
|
cy.wait(7000)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// Because we show the datasource modal on entry, we need to create a table to get rid of the modal in the future
|
||||||
|
cy.createInitialDatasource("initialTable")
|
||||||
cy.expandBudibaseConnection()
|
cy.expandBudibaseConnection()
|
||||||
cy.get(".nav-item.selected > .content").should("be.visible")
|
cy.get(".nav-item.selected > .content").should("be.visible")
|
||||||
})
|
})
|
||||||
|
@ -69,11 +72,28 @@ Cypress.Commands.add("createTestTableWithData", () => {
|
||||||
cy.addColumn("dog", "age", "Number")
|
cy.addColumn("dog", "age", "Number")
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("createTable", tableName => {
|
Cypress.Commands.add("createInitialDatasource", tableName => {
|
||||||
// Enter table name
|
// Enter table name
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.contains("Budibase DB").trigger("mouseover").click().click()
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.contains("Continue").click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get("input").first().type(tableName).blur()
|
||||||
|
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||||
|
})
|
||||||
|
cy.contains(tableName).should("be.visible")
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("createTable", tableName => {
|
||||||
cy.contains("Budibase DB").click()
|
cy.contains("Budibase DB").click()
|
||||||
cy.contains("Create new table").click()
|
cy.contains("Create new table").click()
|
||||||
|
|
||||||
cy.get(".spectrum-Modal").within(() => {
|
cy.get(".spectrum-Modal").within(() => {
|
||||||
|
cy.wait(1000)
|
||||||
cy.get("input").first().type(tableName).blur()
|
cy.get("input").first().type(tableName).blur()
|
||||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||||
})
|
})
|
||||||
|
@ -145,7 +165,7 @@ Cypress.Commands.add("getComponent", componentId => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.should("not.be.null")
|
.should("not.be.null")
|
||||||
.then(cy.wrap)
|
.then(cy.wrap)
|
||||||
.find(`[data-component-id=${componentId}]`)
|
.find(`[data-id=${componentId}]`)
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("navigateToFrontend", () => {
|
Cypress.Commands.add("navigateToFrontend", () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "0.9.146",
|
"version": "0.9.147-alpha.0",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.146",
|
"@budibase/bbui": "^0.9.147-alpha.0",
|
||||||
"@budibase/client": "^0.9.146",
|
"@budibase/client": "^0.9.147-alpha.0",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^0.9.146",
|
"@budibase/string-templates": "^0.9.147-alpha.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||||
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
||||||
// need the client lucene builder to convert to the structure API expects
|
// need the client lucene builder to convert to the structure API expects
|
||||||
import { buildLuceneQuery } from "../../../../../client/src/utils/lucene"
|
import { buildLuceneQuery } from "helpers/lucene"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let webhookModal
|
export let webhookModal
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { tables, views } from "stores/backend"
|
import { fade } from "svelte/transition"
|
||||||
|
import { tables } from "stores/backend"
|
||||||
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
||||||
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
||||||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||||
|
@ -8,59 +8,84 @@
|
||||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||||
import * as api from "./api"
|
import TableFilterButton from "./buttons/TableFilterButton.svelte"
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
|
import { fetchTableData } from "helpers/fetchTableData"
|
||||||
|
import { Pagination } from "@budibase/bbui"
|
||||||
|
|
||||||
let hideAutocolumns = true
|
let hideAutocolumns = true
|
||||||
let data = []
|
|
||||||
let loading = false
|
|
||||||
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
||||||
$: title = $tables.selected?.name
|
|
||||||
$: schema = $tables.selected?.schema
|
$: schema = $tables.selected?.schema
|
||||||
$: tableView = {
|
|
||||||
schema,
|
|
||||||
name: $views.selected?.name,
|
|
||||||
}
|
|
||||||
$: type = $tables.selected?.type
|
$: type = $tables.selected?.type
|
||||||
$: isInternal = type !== "external"
|
$: isInternal = type !== "external"
|
||||||
|
$: id = $tables.selected?._id
|
||||||
|
$: search = searchTable(id)
|
||||||
|
$: columnOptions = Object.keys($search.schema || {})
|
||||||
|
|
||||||
// Fetch rows for specified table
|
// Fetches new data whenever the table changes
|
||||||
$: {
|
const searchTable = tableId => {
|
||||||
loading = true
|
return fetchTableData({
|
||||||
const loadingTableId = $tables.selected?._id
|
tableId,
|
||||||
api.fetchDataForTable($tables.selected?._id).then(rows => {
|
schema,
|
||||||
loading = false
|
limit: 10,
|
||||||
|
paginate: true,
|
||||||
// If we started a slow request then quickly change table, sometimes
|
})
|
||||||
// the old data overwrites the new data.
|
|
||||||
// This check ensures that we don't do that.
|
|
||||||
if (loadingTableId !== $tables.selected?._id) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data = rows || []
|
// Fetch data whenever sorting option changes
|
||||||
|
const onSort = e => {
|
||||||
|
search.update({
|
||||||
|
sortColumn: e.detail.column,
|
||||||
|
sortOrder: e.detail.order,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever filters change
|
||||||
|
const onFilter = e => {
|
||||||
|
search.update({
|
||||||
|
filters: e.detail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever schema changes
|
||||||
|
const onUpdateColumns = () => {
|
||||||
|
search.update({
|
||||||
|
schema,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
||||||
|
// our pagination place, as our bookmarks will have shifted.
|
||||||
|
const onUpdateRows = () => {
|
||||||
|
search.update()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table
|
<div>
|
||||||
{title}
|
<Table
|
||||||
|
title={$tables.selected?.name}
|
||||||
{schema}
|
{schema}
|
||||||
tableId={$tables.selected?._id}
|
|
||||||
{data}
|
|
||||||
{type}
|
{type}
|
||||||
allowEditing={true}
|
tableId={id}
|
||||||
|
data={$search.rows}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
{loading}
|
loading={$search.loading}
|
||||||
>
|
on:sort={onSort}
|
||||||
|
allowEditing
|
||||||
|
disableSorting
|
||||||
|
on:updatecolumns={onUpdateColumns}
|
||||||
|
on:updaterows={onUpdateRows}
|
||||||
|
>
|
||||||
{#if isInternal}
|
{#if isInternal}
|
||||||
<CreateColumnButton />
|
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if schema && Object.keys(schema).length > 0}
|
{#if schema && Object.keys(schema).length > 0}
|
||||||
{#if !isUsersTable}
|
{#if !isUsersTable}
|
||||||
<CreateRowButton
|
<CreateRowButton
|
||||||
|
on:updaterows={onUpdateRows}
|
||||||
title={"Create row"}
|
title={"Create row"}
|
||||||
modalContentComponent={CreateEditRow}
|
modalContentComponent={CreateEditRow}
|
||||||
/>
|
/>
|
||||||
|
@ -75,5 +100,32 @@
|
||||||
<HideAutocolumnButton bind:hideAutocolumns />
|
<HideAutocolumnButton bind:hideAutocolumns />
|
||||||
<!-- always have the export last -->
|
<!-- always have the export last -->
|
||||||
<ExportButton view={$tables.selected?._id} />
|
<ExportButton view={$tables.selected?._id} />
|
||||||
|
{#key id}
|
||||||
|
<TableFilterButton {schema} on:change={onFilter} />
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</Table>
|
</Table>
|
||||||
|
{#key id}
|
||||||
|
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={$search.pageNumber + 1}
|
||||||
|
hasPrevPage={$search.hasPrevPage}
|
||||||
|
hasNextPage={$search.hasNextPage}
|
||||||
|
goToPrevPage={$search.loading ? null : search.prevPage}
|
||||||
|
goToNextPage={$search.loading ? null : search.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { Table, Modal, Heading, notifications } from "@budibase/bbui"
|
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
|
||||||
|
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||||
|
@ -21,6 +20,7 @@
|
||||||
export let hideAutocolumns
|
export let hideAutocolumns
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let type
|
export let type
|
||||||
|
export let disableSorting = false
|
||||||
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
let editableColumn
|
let editableColumn
|
||||||
|
@ -98,13 +98,14 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<Layout noPadding gap="S">
|
||||||
|
<div>
|
||||||
<div class="table-title">
|
<div class="table-title">
|
||||||
{#if title}
|
{#if title}
|
||||||
<Heading size="S">{title}</Heading>
|
<Heading size="S">{title}</Heading>
|
||||||
{/if}
|
{/if}
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div transition:fade>
|
<div transition:fade|local>
|
||||||
<Spinner size="10" />
|
<Spinner size="10" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -112,17 +113,19 @@
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<slot />
|
<slot />
|
||||||
{#if !isUsersTable && selectedRows.length > 0}
|
{#if !isUsersTable && selectedRows.length > 0}
|
||||||
<DeleteRowsButton {selectedRows} {deleteRows} />
|
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#key tableId}
|
{#key tableId}
|
||||||
|
<div class="table-wrapper" in:fade={{ delay: 200, duration: 100 }}>
|
||||||
<Table
|
<Table
|
||||||
{data}
|
{data}
|
||||||
{schema}
|
{schema}
|
||||||
{loading}
|
{loading}
|
||||||
{customRenderers}
|
{customRenderers}
|
||||||
{rowCount}
|
{rowCount}
|
||||||
|
{disableSorting}
|
||||||
bind:selectedRows
|
bind:selectedRows
|
||||||
allowSelectRows={allowEditing && !isUsersTable}
|
allowSelectRows={allowEditing && !isUsersTable}
|
||||||
allowEditRows={allowEditing}
|
allowEditRows={allowEditing}
|
||||||
|
@ -131,14 +134,21 @@
|
||||||
on:editcolumn={e => editColumn(e.detail)}
|
on:editcolumn={e => editColumn(e.detail)}
|
||||||
on:editrow={e => editRow(e.detail)}
|
on:editrow={e => editRow(e.detail)}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
|
on:sort
|
||||||
/>
|
/>
|
||||||
{/key}
|
</div>
|
||||||
|
{/key}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={editRowModal}>
|
<Modal bind:this={editRowModal}>
|
||||||
<svelte:component this={editRowComponent} row={editableRow} />
|
<svelte:component this={editRowComponent} on:updaterows row={editableRow} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal bind:this={editColumnModal}>
|
<Modal bind:this={editColumnModal}>
|
||||||
<CreateEditColumn field={editableColumn} onClosed={editColumnModal.hide} />
|
<CreateEditColumn
|
||||||
|
field={editableColumn}
|
||||||
|
on:updatecolumns
|
||||||
|
onClosed={editColumnModal.hide}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -152,6 +162,9 @@
|
||||||
.table-title > div {
|
.table-title > div {
|
||||||
margin-left: var(--spacing-xs);
|
margin-left: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
.table-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.popovers {
|
.popovers {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import Table from "./Table.svelte"
|
import Table from "./Table.svelte"
|
||||||
import CalculateButton from "./buttons/CalculateButton.svelte"
|
import CalculateButton from "./buttons/CalculateButton.svelte"
|
||||||
import GroupByButton from "./buttons/GroupByButton.svelte"
|
import GroupByButton from "./buttons/GroupByButton.svelte"
|
||||||
import FilterButton from "./buttons/FilterButton.svelte"
|
import ViewFilterButton from "./buttons/ViewFilterButton.svelte"
|
||||||
import ExportButton from "./buttons/ExportButton.svelte"
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
allowEditing={!view?.calculation}
|
allowEditing={!view?.calculation}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
>
|
>
|
||||||
<FilterButton {view} />
|
<ViewFilterButton {view} />
|
||||||
<CalculateButton {view} />
|
<CalculateButton {view} />
|
||||||
{#if view.calculation}
|
{#if view.calculation}
|
||||||
<GroupByButton {view} />
|
<GroupByButton {view} />
|
||||||
|
|
|
@ -9,5 +9,5 @@
|
||||||
Create column
|
Create column
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateEditColumn />
|
<CreateEditColumn on:updatecolumns />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -12,5 +12,5 @@
|
||||||
{title}
|
{title}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<svelte:component this={modalContentComponent} />
|
<svelte:component this={modalContentComponent} on:updaterows />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
|
||||||
export let selectedRows
|
export let selectedRows
|
||||||
export let deleteRows
|
export let deleteRows
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
async function confirmDeletion() {
|
async function confirmDeletion() {
|
||||||
await deleteRows()
|
await deleteRows()
|
||||||
modal?.hide()
|
modal?.hide()
|
||||||
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
import { ActionButton, Modal, ModalContent } from "@budibase/bbui"
|
||||||
|
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
||||||
|
|
||||||
|
export let schema
|
||||||
|
export let filters
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let modal
|
||||||
|
let tempValue = filters || []
|
||||||
|
|
||||||
|
$: schemaFields = Object.values(schema || {})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
icon="Filter"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
active={tempValue?.length > 0}
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
</ActionButton>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<ModalContent
|
||||||
|
title="Filter"
|
||||||
|
confirmText="Save"
|
||||||
|
size="XL"
|
||||||
|
onConfirm={() => dispatch("change", tempValue)}
|
||||||
|
>
|
||||||
|
<div class="wrapper">
|
||||||
|
<FilterDrawer
|
||||||
|
allowBindings={false}
|
||||||
|
bind:filters={tempValue}
|
||||||
|
{schemaFields}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper :global(.main) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -10,6 +10,7 @@
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Context,
|
Context,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||||
|
@ -30,8 +31,9 @@
|
||||||
const AUTO_TYPE = "auto"
|
const AUTO_TYPE = "auto"
|
||||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||||
const LINK_TYPE = FIELDS.LINK.type
|
const LINK_TYPE = FIELDS.LINK.type
|
||||||
let fieldDefinitions = cloneDeep(FIELDS)
|
const dispatch = createEventDispatcher()
|
||||||
const { hide } = getContext(Context.Modal)
|
const { hide } = getContext(Context.Modal)
|
||||||
|
let fieldDefinitions = cloneDeep(FIELDS)
|
||||||
|
|
||||||
export let field = {
|
export let field = {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -81,12 +83,13 @@
|
||||||
if (field.type === AUTO_TYPE) {
|
if (field.type === AUTO_TYPE) {
|
||||||
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
||||||
}
|
}
|
||||||
tables.saveField({
|
await tables.saveField({
|
||||||
originalName,
|
originalName,
|
||||||
field,
|
field,
|
||||||
primaryDisplay,
|
primaryDisplay,
|
||||||
indexes,
|
indexes,
|
||||||
})
|
})
|
||||||
|
dispatch("updatecolumns")
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteColumn() {
|
function deleteColumn() {
|
||||||
|
@ -99,6 +102,7 @@
|
||||||
hide()
|
hide()
|
||||||
deletion = false
|
deletion = false
|
||||||
}
|
}
|
||||||
|
dispatch("updatecolumns")
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTypeChange(event) {
|
function handleTypeChange(event) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { tables, rows } from "stores/backend"
|
import { tables, rows } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import RowFieldControl from "../RowFieldControl.svelte"
|
import RowFieldControl from "../RowFieldControl.svelte"
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
export let row = {}
|
export let row = {}
|
||||||
|
|
||||||
let errors = []
|
let errors = []
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: creating = row?._id == null
|
$: creating = row?._id == null
|
||||||
$: table = row.tableId
|
$: table = row.tableId
|
||||||
|
@ -43,6 +45,7 @@
|
||||||
|
|
||||||
notifications.success("Row saved successfully.")
|
notifications.success("Row saved successfully.")
|
||||||
rows.save(rowResponse)
|
rows.save(rowResponse)
|
||||||
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
import { tables, rows } from "stores/backend"
|
import { tables, rows } from "stores/backend"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
@ -9,6 +10,7 @@
|
||||||
|
|
||||||
export let row = {}
|
export let row = {}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
let errors = []
|
let errors = []
|
||||||
|
|
||||||
$: creating = row?._id == null
|
$: creating = row?._id == null
|
||||||
|
@ -71,6 +73,7 @@
|
||||||
|
|
||||||
notifications.success("User saved successfully")
|
notifications.success("User saved successfully")
|
||||||
rows.save(rowResponse)
|
rows.save(rowResponse)
|
||||||
|
dispatch("updaterows")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { BUDIBASE_INTERNAL_DB } from "constants"
|
import { BUDIBASE_INTERNAL_DB } from "constants"
|
||||||
import { database, datasources, queries, tables } from "stores/backend"
|
import { database, datasources, queries, tables, views } from "stores/backend"
|
||||||
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
||||||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
|
@ -11,16 +11,27 @@
|
||||||
import ICONS from "./icons"
|
import ICONS from "./icons"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
$: enrichedDataSources = $datasources.list.map(datasource => ({
|
$: enrichedDataSources = $datasources.list.map(datasource => {
|
||||||
|
const selected = $datasources.selected === datasource._id
|
||||||
|
const open = openDataSources.includes(datasource._id)
|
||||||
|
const containsSelected = containsActiveEntity(datasource)
|
||||||
|
return {
|
||||||
...datasource,
|
...datasource,
|
||||||
open:
|
selected,
|
||||||
openDataSources.includes(datasource._id) ||
|
open: selected || open || containsSelected,
|
||||||
containsActiveTable(datasource),
|
}
|
||||||
selected: $datasources.selected === datasource._id,
|
})
|
||||||
}))
|
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||||
|
$: {
|
||||||
|
// Ensure the open data source is always included in the list of open
|
||||||
|
// data sources
|
||||||
|
if (openDataSource) {
|
||||||
|
openNode(openDataSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function selectDatasource(datasource) {
|
function selectDatasource(datasource) {
|
||||||
toggleNode(datasource)
|
openNode(datasource)
|
||||||
datasources.select(datasource._id)
|
datasources.select(datasource._id)
|
||||||
$goto(`./datasource/${datasource._id}`)
|
$goto(`./datasource/${datasource._id}`)
|
||||||
}
|
}
|
||||||
|
@ -30,12 +41,22 @@
|
||||||
$goto(`./datasource/${query.datasourceId}/${query._id}`)
|
$goto(`./datasource/${query.datasourceId}/${query._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeNode(datasource) {
|
||||||
|
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNode(datasource) {
|
||||||
|
if (!openDataSources.includes(datasource._id)) {
|
||||||
|
openDataSources = [...openDataSources, datasource._id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleNode(datasource) {
|
function toggleNode(datasource) {
|
||||||
const isOpen = openDataSources.includes(datasource._id)
|
const isOpen = openDataSources.includes(datasource._id)
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
closeNode(datasource)
|
||||||
} else {
|
} else {
|
||||||
openDataSources = [...openDataSources, datasource._id]
|
openNode(datasource)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,16 +65,35 @@
|
||||||
queries.fetch()
|
queries.fetch()
|
||||||
})
|
})
|
||||||
|
|
||||||
const containsActiveTable = datasource => {
|
const containsActiveEntity = datasource => {
|
||||||
const activeTableId = get(tables).selected?._id
|
// If we're view a query then the data source ID is in the URL
|
||||||
|
if ($params.selectedDatasource === datasource._id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no entities it can't contain anything
|
||||||
if (!datasource.entities) {
|
if (!datasource.entities) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let tableOptions = datasource.entities
|
|
||||||
if (!Array.isArray(tableOptions)) {
|
// Get a list of table options
|
||||||
tableOptions = Object.values(tableOptions)
|
let options = datasource.entities
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
options = Object.values(options)
|
||||||
}
|
}
|
||||||
return tableOptions.find(x => x._id === activeTableId) != null
|
|
||||||
|
// Check for a matching table
|
||||||
|
if ($params.selectedTable) {
|
||||||
|
const selectedTable = get(tables).selected?._id
|
||||||
|
return options.find(x => x._id === selectedTable) != null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for a matching view
|
||||||
|
const selectedView = get(views).selected?.name
|
||||||
|
const table = options.find(table => {
|
||||||
|
return table.views?.[selectedView] != null
|
||||||
|
})
|
||||||
|
return table != null
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
<script>
|
<script>
|
||||||
import { Label, Input, Layout, Toggle } from "@budibase/bbui"
|
import { Label, Input, Layout, Toggle, Button } from "@budibase/bbui"
|
||||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
|
||||||
export let integration
|
export let integration
|
||||||
export let schema
|
export let schema
|
||||||
|
let addButton
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<Layout gap="S">
|
<Layout gap="S">
|
||||||
{#each Object.keys(schema) as configKey}
|
{#each Object.keys(schema) as configKey}
|
||||||
{#if schema[configKey].type === "object"}
|
{#if schema[configKey].type === "object"}
|
||||||
|
<div class="form-row ssl">
|
||||||
<Label>{capitalise(configKey)}</Label>
|
<Label>{capitalise(configKey)}</Label>
|
||||||
|
<Button secondary thin outline on:click={addButton.addEntry()}
|
||||||
|
>Add</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
<KeyValueBuilder
|
<KeyValueBuilder
|
||||||
|
bind:this={addButton}
|
||||||
defaults={schema[configKey].default}
|
defaults={schema[configKey].default}
|
||||||
bind:object={integration[configKey]}
|
bind:object={integration[configKey]}
|
||||||
|
noAddButton={true}
|
||||||
/>
|
/>
|
||||||
{:else if schema[configKey].type === "boolean"}
|
{:else if schema[configKey].type === "boolean"}
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
@ -42,4 +50,11 @@
|
||||||
grid-gap: var(--spacing-l);
|
grid-gap: var(--spacing-l);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-row.ssl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20% 20%;
|
||||||
|
grid-gap: var(--spacing-l);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,74 +1,160 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { ModalContent, Modal, Body, Layout, Detail } from "@budibase/bbui"
|
||||||
import { datasources } from "stores/backend"
|
import { onMount } from "svelte"
|
||||||
import { notifications } from "@budibase/bbui"
|
import ICONS from "../icons"
|
||||||
import { Input, Label, ModalContent, Modal, Context } from "@budibase/bbui"
|
import api from "builderStore/api"
|
||||||
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
|
import { IntegrationNames } from "constants"
|
||||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||||
import analytics, { Events } from "analytics"
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const modalContext = getContext(Context.Modal)
|
export let modal
|
||||||
|
let integrations = []
|
||||||
|
let integration = {}
|
||||||
|
let internalTableModal
|
||||||
|
let externalDatasourceModal
|
||||||
|
|
||||||
let tableModal
|
const INTERNAL = "BUDIBASE"
|
||||||
let name
|
|
||||||
let error = ""
|
|
||||||
let integration
|
|
||||||
|
|
||||||
$: checkOpenModal(integration && integration.type === "BUDIBASE")
|
onMount(() => {
|
||||||
|
fetchIntegrations()
|
||||||
function checkValid(evt) {
|
|
||||||
const datasourceName = evt.target.value
|
|
||||||
if (
|
|
||||||
$datasources?.list.some(datasource => datasource.name === datasourceName)
|
|
||||||
) {
|
|
||||||
error = `Datasource with name ${datasourceName} already exists. Please choose another name.`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
error = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkOpenModal(isInternal) {
|
|
||||||
if (isInternal) {
|
|
||||||
tableModal.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveDatasource() {
|
|
||||||
const { type, plus, ...config } = integration
|
|
||||||
|
|
||||||
// Create datasource
|
|
||||||
const response = await datasources.save({
|
|
||||||
name,
|
|
||||||
source: type,
|
|
||||||
config,
|
|
||||||
plus,
|
|
||||||
})
|
})
|
||||||
notifications.success(`Datasource ${name} created successfully.`)
|
|
||||||
analytics.captureEvent(Events.DATASOURCE.CREATED, { name, type })
|
|
||||||
|
|
||||||
// Navigate to new datasource
|
function selectIntegration(integrationType) {
|
||||||
$goto(`./datasource/${response._id}`)
|
const selected = integrations[integrationType]
|
||||||
|
|
||||||
|
// build the schema
|
||||||
|
const config = {}
|
||||||
|
for (let key of Object.keys(selected.datasource)) {
|
||||||
|
config[key] = selected.datasource[key].default
|
||||||
|
}
|
||||||
|
integration = {
|
||||||
|
type: integrationType,
|
||||||
|
plus: selected.plus,
|
||||||
|
config,
|
||||||
|
schema: selected.datasource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseNextModal() {
|
||||||
|
if (integration.type === INTERNAL) {
|
||||||
|
externalDatasourceModal.hide()
|
||||||
|
internalTableModal.show()
|
||||||
|
} else {
|
||||||
|
externalDatasourceModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchIntegrations() {
|
||||||
|
const response = await api.get("/api/integrations")
|
||||||
|
const json = await response.json()
|
||||||
|
integrations = {
|
||||||
|
[INTERNAL]: { datasource: {}, name: "INTERNAL/CSV" },
|
||||||
|
...json,
|
||||||
|
}
|
||||||
|
return json
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={tableModal} on:hide={modalContext.hide}>
|
<Modal bind:this={internalTableModal}>
|
||||||
<CreateTableModal bind:name />
|
<CreateTableModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
<ModalContent
|
|
||||||
title="Create Datasource"
|
<Modal bind:this={externalDatasourceModal}>
|
||||||
size="L"
|
<DatasourceConfigModal {integration} />
|
||||||
confirmText="Create"
|
</Modal>
|
||||||
onConfirm={saveDatasource}
|
|
||||||
disabled={error || !name || !integration?.type}
|
<Modal bind:this={modal}>
|
||||||
>
|
<ModalContent
|
||||||
<Input
|
disabled={!Object.keys(integration).length}
|
||||||
data-cy="datasource-name-input"
|
title="Data"
|
||||||
label="Datasource Name"
|
confirmText="Continue"
|
||||||
on:input={checkValid}
|
showCancelButton={false}
|
||||||
bind:value={name}
|
size="M"
|
||||||
{error}
|
onConfirm={() => {
|
||||||
|
chooseNextModal()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Layout noPadding>
|
||||||
|
<Body size="XS"
|
||||||
|
>All apps need data. You can connect to a data source below, or add data
|
||||||
|
to your app using Budibase's built-in database.
|
||||||
|
</Body>
|
||||||
|
<div
|
||||||
|
class:selected={integration.type === INTERNAL}
|
||||||
|
on:click={() => selectIntegration(INTERNAL)}
|
||||||
|
class="item hoverable"
|
||||||
|
>
|
||||||
|
<div class="item-body">
|
||||||
|
<svelte:component this={ICONS.BUDIBASE} height="18" width="18" />
|
||||||
|
<span class="icon-spacing"> <Body size="S">Budibase DB</Body></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<div class="title-spacing">
|
||||||
|
<Detail size="S">Connect to data source</Detail>
|
||||||
|
</div>
|
||||||
|
<div class="item-list">
|
||||||
|
{#each Object.entries(integrations).filter(([key]) => key !== INTERNAL) as [integrationType, schema]}
|
||||||
|
<div
|
||||||
|
class:selected={integration.type === integrationType}
|
||||||
|
on:click={() => selectIntegration(integrationType)}
|
||||||
|
class="item hoverable"
|
||||||
|
>
|
||||||
|
<div class="item-body">
|
||||||
|
<svelte:component
|
||||||
|
this={ICONS[integrationType]}
|
||||||
|
height="18"
|
||||||
|
width="18"
|
||||||
/>
|
/>
|
||||||
<Label>Datasource Type</Label>
|
|
||||||
<TableIntegrationMenu bind:integration />
|
<span class="icon-spacing">
|
||||||
</ModalContent>
|
<Body size="S"
|
||||||
|
>{schema.name || IntegrationNames[integrationType]}</Body
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon-spacing {
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.item-body {
|
||||||
|
display: flex;
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.item-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||||
|
padding: var(--spectrum-alias-item-padding-s);
|
||||||
|
background: var(--spectrum-alias-background-color-secondary);
|
||||||
|
transition: 0.3s all;
|
||||||
|
border: solid var(--spectrum-alias-border-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background: var(--spectrum-alias-background-color-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover,
|
||||||
|
.selected {
|
||||||
|
background: var(--spectrum-alias-background-color-tertiary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
<script>
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
|
||||||
|
import analytics, { Events } from "analytics"
|
||||||
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
|
import { datasources } from "stores/backend"
|
||||||
|
import { IntegrationNames } from "constants"
|
||||||
|
|
||||||
|
export let integration
|
||||||
|
|
||||||
|
function prepareData() {
|
||||||
|
let datasource = {}
|
||||||
|
let existingTypeCount = $datasources.list.filter(
|
||||||
|
ds => ds.source == integration.type
|
||||||
|
).length
|
||||||
|
|
||||||
|
let baseName = IntegrationNames[integration.type]
|
||||||
|
let name =
|
||||||
|
existingTypeCount == 0 ? baseName : `${baseName}-${existingTypeCount + 1}`
|
||||||
|
|
||||||
|
datasource.type = "datasource"
|
||||||
|
datasource.source = integration.type
|
||||||
|
datasource.config = integration.config
|
||||||
|
datasource.name = name
|
||||||
|
datasource.plus = integration.plus
|
||||||
|
|
||||||
|
return datasource
|
||||||
|
}
|
||||||
|
async function saveDatasource() {
|
||||||
|
const datasource = prepareData()
|
||||||
|
try {
|
||||||
|
// Create datasource
|
||||||
|
const resp = await datasources.save(datasource, datasource.plus)
|
||||||
|
|
||||||
|
await datasources.select(resp._id)
|
||||||
|
$goto(`./datasource/${resp._id}`)
|
||||||
|
notifications.success(`Datasource updated successfully.`)
|
||||||
|
analytics.captureEvent(Events.DATASOURCE.CREATED, {
|
||||||
|
name: resp.name,
|
||||||
|
source: resp.source,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error saving datasource: ${err}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title={`Connect to ${IntegrationNames[integration.type]}`}
|
||||||
|
onConfirm={() => saveDatasource()}
|
||||||
|
confirmText={integration.plus
|
||||||
|
? "Fetch tables from database"
|
||||||
|
: "Save and continue to query"}
|
||||||
|
cancelText="Back"
|
||||||
|
size="M"
|
||||||
|
>
|
||||||
|
<Layout noPadding>
|
||||||
|
<Body size="XS"
|
||||||
|
>Connect your database to Budibase using the config below.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<IntegrationConfigForm
|
||||||
|
schema={integration.schema}
|
||||||
|
bind:integration={integration.config}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import iframeTemplate from "./iframeTemplate"
|
import iframeTemplate from "./iframeTemplate"
|
||||||
|
@ -7,6 +8,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui"
|
import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui"
|
||||||
import ErrorSVG from "assets/error.svg?raw"
|
import ErrorSVG from "assets/error.svg?raw"
|
||||||
|
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let layout
|
let layout
|
||||||
|
@ -102,7 +104,7 @@
|
||||||
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
// remove all iframe event listeners on component destroy
|
// Remove all iframe event listeners on component destroy
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (iframe.contentWindow) {
|
if (iframe.contentWindow) {
|
||||||
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
||||||
|
@ -122,6 +124,26 @@
|
||||||
// Wait for this event to show the client library if intelligent
|
// Wait for this event to show the client library if intelligent
|
||||||
// loading is supported
|
// loading is supported
|
||||||
loading = false
|
loading = false
|
||||||
|
} else if (type === "move-component") {
|
||||||
|
const { componentId, destinationComponentId } = data
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
|
||||||
|
// Get source and destination components
|
||||||
|
const source = findComponent(rootComponent, componentId)
|
||||||
|
const destination = findComponent(rootComponent, destinationComponentId)
|
||||||
|
|
||||||
|
// Stop if the target is a child of source
|
||||||
|
const path = findComponentPath(source, destinationComponentId)
|
||||||
|
const ids = path.map(component => component._id)
|
||||||
|
if (ids.includes(data.destinationComponentId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut and paste the component to the new destination
|
||||||
|
if (source && destination) {
|
||||||
|
store.actions.components.copy(source, true)
|
||||||
|
store.actions.components.paste(destination, data.mode)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warning(`Client sent unknown event type: ${type}`)
|
console.warning(`Client sent unknown event type: ${type}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
import { dndzone } from "svelte-dnd-action"
|
import { dndzone } from "svelte-dnd-action"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
|
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
|
||||||
import { selectedComponent, store } from "builderStore"
|
import { selectedComponent, store } from "builderStore"
|
||||||
import { getComponentForSettingType } from "./componentSettings"
|
import { getComponentForSettingType } from "./componentSettings"
|
||||||
import PropertyControl from "./PropertyControl.svelte"
|
import PropertyControl from "./PropertyControl.svelte"
|
||||||
|
|
|
@ -13,18 +13,20 @@
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
import { getValidOperatorsForType, OperatorOptions } from "helpers/lucene"
|
import { getValidOperatorsForType, OperatorOptions } from "constants/lucene"
|
||||||
|
|
||||||
export let schemaFields
|
export let schemaFields
|
||||||
export let filters = []
|
export let filters = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let panel = BindingPanel
|
export let panel = BindingPanel
|
||||||
|
export let allowBindings = true
|
||||||
|
|
||||||
const BannedTypes = ["link", "attachment", "formula"]
|
const BannedTypes = ["link", "attachment", "formula"]
|
||||||
|
|
||||||
$: fieldOptions = (schemaFields ?? [])
|
$: fieldOptions = (schemaFields ?? [])
|
||||||
.filter(field => !BannedTypes.includes(field.type))
|
.filter(field => !BannedTypes.includes(field.type))
|
||||||
.map(field => field.name)
|
.map(field => field.name)
|
||||||
|
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||||
|
|
||||||
const addFilter = () => {
|
const addFilter = () => {
|
||||||
filters = [
|
filters = [
|
||||||
|
@ -93,7 +95,7 @@
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
{#if !filters?.length}
|
{#if !filters?.length}
|
||||||
Add your first filter column.
|
Add your first filter expression.
|
||||||
{:else}
|
{:else}
|
||||||
Results are filtered to only those which match all of the following
|
Results are filtered to only those which match all of the following
|
||||||
constraints.
|
constraints.
|
||||||
|
@ -117,7 +119,7 @@
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
disabled={filter.noValue || !filter.field}
|
disabled={filter.noValue || !filter.field}
|
||||||
options={["Value", "Binding"]}
|
options={valueTypeOptions}
|
||||||
bind:value={filter.valueType}
|
bind:value={filter.valueType}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||||
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
||||||
$: schemaFields = Object.values(schema || {})
|
$: schemaFields = Object.values(schema || {})
|
||||||
$: internalTable = dataSource?.type === "table"
|
|
||||||
|
|
||||||
const saveFilter = async () => {
|
const saveFilter = async () => {
|
||||||
dispatch("change", tempValue)
|
dispatch("change", tempValue)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
export let defaults
|
export let defaults
|
||||||
export let object = defaults || {}
|
export let object = defaults || {}
|
||||||
export let readOnly
|
export let readOnly
|
||||||
|
export let noAddButton
|
||||||
|
|
||||||
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
|
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
function addEntry() {
|
export function addEntry() {
|
||||||
fields = [...fields, {}]
|
fields = [...fields, {}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if !readOnly}
|
{#if !readOnly && !noAddButton}
|
||||||
<div>
|
<div>
|
||||||
<Button secondary thin outline on:click={addEntry}>Add</Button>
|
<Button secondary thin outline on:click={addEntry}>Add</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,6 +15,20 @@ export const AppStatus = {
|
||||||
DEPLOYED: "published",
|
DEPLOYED: "published",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const IntegrationNames = {
|
||||||
|
POSTGRES: "PostgreSQL",
|
||||||
|
MONGODB: "MongoDB",
|
||||||
|
COUCHDB: "CouchDB",
|
||||||
|
S3: "S3",
|
||||||
|
MYSQL: "MySQL",
|
||||||
|
REST: "REST",
|
||||||
|
DYNAMODB: "DynamoDB",
|
||||||
|
ELASTICSEARCH: "ElasticSearch",
|
||||||
|
SQL_SERVER: "SQL Server",
|
||||||
|
AIRTABLE: "Airtable",
|
||||||
|
ARANGODB: "ArangoDB",
|
||||||
|
}
|
||||||
|
|
||||||
// fields on the user table that cannot be edited
|
// fields on the user table that cannot be edited
|
||||||
export const UNEDITABLE_USER_FIELDS = [
|
export const UNEDITABLE_USER_FIELDS = [
|
||||||
"email",
|
"email",
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* Operator options for lucene queries
|
||||||
|
*/
|
||||||
|
export const OperatorOptions = {
|
||||||
|
Equals: {
|
||||||
|
value: "equal",
|
||||||
|
label: "Equals",
|
||||||
|
},
|
||||||
|
NotEquals: {
|
||||||
|
value: "notEqual",
|
||||||
|
label: "Not equals",
|
||||||
|
},
|
||||||
|
Empty: {
|
||||||
|
value: "empty",
|
||||||
|
label: "Is empty",
|
||||||
|
},
|
||||||
|
NotEmpty: {
|
||||||
|
value: "notEmpty",
|
||||||
|
label: "Is not empty",
|
||||||
|
},
|
||||||
|
StartsWith: {
|
||||||
|
value: "string",
|
||||||
|
label: "Starts with",
|
||||||
|
},
|
||||||
|
Like: {
|
||||||
|
value: "fuzzy",
|
||||||
|
label: "Like",
|
||||||
|
},
|
||||||
|
MoreThan: {
|
||||||
|
value: "rangeLow",
|
||||||
|
label: "More than",
|
||||||
|
},
|
||||||
|
LessThan: {
|
||||||
|
value: "rangeHigh",
|
||||||
|
label: "Less than",
|
||||||
|
},
|
||||||
|
Contains: {
|
||||||
|
value: "equal",
|
||||||
|
label: "Contains",
|
||||||
|
},
|
||||||
|
NotContains: {
|
||||||
|
value: "notEqual",
|
||||||
|
label: "Does Not Contain",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the valid operator options for a certain data type
|
||||||
|
* @param type the data type
|
||||||
|
*/
|
||||||
|
export const getValidOperatorsForType = type => {
|
||||||
|
const Op = OperatorOptions
|
||||||
|
if (type === "string") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.StartsWith,
|
||||||
|
Op.Like,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
} else if (type === "number") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.MoreThan,
|
||||||
|
Op.LessThan,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
} else if (type === "options") {
|
||||||
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
|
} else if (type === "array") {
|
||||||
|
return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
|
||||||
|
} else if (type === "boolean") {
|
||||||
|
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||||
|
} else if (type === "longform") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.StartsWith,
|
||||||
|
Op.Like,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
} else if (type === "datetime") {
|
||||||
|
return [
|
||||||
|
Op.Equals,
|
||||||
|
Op.NotEquals,
|
||||||
|
Op.MoreThan,
|
||||||
|
Op.LessThan,
|
||||||
|
Op.Empty,
|
||||||
|
Op.NotEmpty,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
// Do not use any aliased imports in common files, as these will be bundled
|
||||||
|
// by multiple bundlers which may not be able to resolve them
|
||||||
|
import { writable, derived, get } from "svelte/store"
|
||||||
|
import * as API from "../builderStore/api"
|
||||||
|
import { buildLuceneQuery } from "./lucene"
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
tableId: null,
|
||||||
|
filters: null,
|
||||||
|
limit: 10,
|
||||||
|
sortColumn: null,
|
||||||
|
sortOrder: "ascending",
|
||||||
|
paginate: true,
|
||||||
|
schema: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchTableData = opts => {
|
||||||
|
// Save option set so we can override it later rather than relying on params
|
||||||
|
let options = {
|
||||||
|
...defaultOptions,
|
||||||
|
...opts,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local non-observable state
|
||||||
|
let query
|
||||||
|
let sortType
|
||||||
|
let lastBookmark
|
||||||
|
|
||||||
|
// Local observable state
|
||||||
|
const store = writable({
|
||||||
|
rows: [],
|
||||||
|
schema: null,
|
||||||
|
loading: false,
|
||||||
|
loaded: false,
|
||||||
|
bookmarks: [],
|
||||||
|
pageNumber: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Derive certain properties to return
|
||||||
|
const derivedStore = derived(store, $store => {
|
||||||
|
return {
|
||||||
|
...$store,
|
||||||
|
hasNextPage: $store.bookmarks[$store.pageNumber + 1] != null,
|
||||||
|
hasPrevPage: $store.pageNumber > 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchPage = async bookmark => {
|
||||||
|
lastBookmark = bookmark
|
||||||
|
const { tableId, limit, sortColumn, sortOrder, paginate } = options
|
||||||
|
store.update($store => ({ ...$store, loading: true }))
|
||||||
|
const res = await API.post(`/api/${options.tableId}/search`, {
|
||||||
|
tableId,
|
||||||
|
query,
|
||||||
|
limit,
|
||||||
|
sort: sortColumn,
|
||||||
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||||
|
sortType,
|
||||||
|
paginate,
|
||||||
|
bookmark,
|
||||||
|
})
|
||||||
|
store.update($store => ({ ...$store, loading: false, loaded: true }))
|
||||||
|
return await res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches a fresh set of results from the server
|
||||||
|
const fetchData = async () => {
|
||||||
|
const { tableId, schema, sortColumn, filters } = options
|
||||||
|
|
||||||
|
// Ensure table ID exists
|
||||||
|
if (!tableId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and enrich schema.
|
||||||
|
// Ensure there are "name" properties for all fields and that field schema
|
||||||
|
// are objects
|
||||||
|
let enrichedSchema = schema
|
||||||
|
if (!enrichedSchema) {
|
||||||
|
const definition = await API.get(`/api/tables/${tableId}`)
|
||||||
|
enrichedSchema = definition?.schema ?? null
|
||||||
|
}
|
||||||
|
if (enrichedSchema) {
|
||||||
|
Object.entries(schema).forEach(([fieldName, fieldSchema]) => {
|
||||||
|
if (typeof fieldSchema === "string") {
|
||||||
|
enrichedSchema[fieldName] = {
|
||||||
|
type: fieldSchema,
|
||||||
|
name: fieldName,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enrichedSchema[fieldName] = {
|
||||||
|
...fieldSchema,
|
||||||
|
name: fieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save fixed schema so we can provide it later
|
||||||
|
options.schema = enrichedSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure schema exists
|
||||||
|
if (!schema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.update($store => ({ ...$store, schema }))
|
||||||
|
|
||||||
|
// Work out what sort type to use
|
||||||
|
if (!sortColumn || !schema[sortColumn]) {
|
||||||
|
sortType = "string"
|
||||||
|
}
|
||||||
|
const type = schema?.[sortColumn]?.type
|
||||||
|
sortType = type === "number" ? "number" : "string"
|
||||||
|
|
||||||
|
// Build the lucene query
|
||||||
|
query = buildLuceneQuery(filters)
|
||||||
|
|
||||||
|
// Actually fetch data
|
||||||
|
const page = await fetchPage()
|
||||||
|
store.update($store => ({
|
||||||
|
...$store,
|
||||||
|
loading: false,
|
||||||
|
loaded: true,
|
||||||
|
pageNumber: 0,
|
||||||
|
rows: page.rows,
|
||||||
|
bookmarks: page.hasNextPage ? [null, page.bookmark] : [null],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the next page of data
|
||||||
|
const nextPage = async () => {
|
||||||
|
const state = get(derivedStore)
|
||||||
|
if (state.loading || !options.paginate || !state.hasNextPage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch next page
|
||||||
|
const page = await fetchPage(state.bookmarks[state.pageNumber + 1])
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
store.update($store => {
|
||||||
|
let { bookmarks, pageNumber } = $store
|
||||||
|
if (page.hasNextPage) {
|
||||||
|
bookmarks[pageNumber + 2] = page.bookmark
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...$store,
|
||||||
|
pageNumber: pageNumber + 1,
|
||||||
|
rows: page.rows,
|
||||||
|
bookmarks,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the previous page of data
|
||||||
|
const prevPage = async () => {
|
||||||
|
const state = get(derivedStore)
|
||||||
|
if (state.loading || !options.paginate || !state.hasPrevPage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch previous page
|
||||||
|
const page = await fetchPage(state.bookmarks[state.pageNumber - 1])
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
store.update($store => {
|
||||||
|
return {
|
||||||
|
...$store,
|
||||||
|
pageNumber: $store.pageNumber - 1,
|
||||||
|
rows: page.rows,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resets the data set and updates options
|
||||||
|
const update = async newOptions => {
|
||||||
|
if (newOptions) {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
...newOptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the same page again
|
||||||
|
const refresh = async () => {
|
||||||
|
if (get(store).loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const page = await fetchPage(lastBookmark)
|
||||||
|
store.update($store => ({ ...$store, rows: page.rows }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initially fetch data but don't bother waiting for the result
|
||||||
|
fetchData()
|
||||||
|
|
||||||
|
// Return our derived store which will be updated over time
|
||||||
|
return {
|
||||||
|
subscribe: derivedStore.subscribe,
|
||||||
|
nextPage,
|
||||||
|
prevPage,
|
||||||
|
update,
|
||||||
|
refresh,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,90 +1,179 @@
|
||||||
export const OperatorOptions = {
|
/**
|
||||||
Equals: {
|
* Builds a lucene JSON query from the filter structure generated in the builder
|
||||||
value: "equal",
|
* @param filter the builder filter structure
|
||||||
label: "Equals",
|
*/
|
||||||
},
|
export const buildLuceneQuery = filter => {
|
||||||
NotEquals: {
|
let query = {
|
||||||
value: "notEqual",
|
string: {},
|
||||||
label: "Not equals",
|
fuzzy: {},
|
||||||
},
|
range: {},
|
||||||
Empty: {
|
equal: {},
|
||||||
value: "empty",
|
notEqual: {},
|
||||||
label: "Is empty",
|
empty: {},
|
||||||
},
|
notEmpty: {},
|
||||||
NotEmpty: {
|
contains: {},
|
||||||
value: "notEmpty",
|
notContains: {},
|
||||||
label: "Is not empty",
|
}
|
||||||
},
|
if (Array.isArray(filter)) {
|
||||||
StartsWith: {
|
filter.forEach(expression => {
|
||||||
value: "string",
|
let { operator, field, type, value } = expression
|
||||||
label: "Starts with",
|
// Parse all values into correct types
|
||||||
},
|
if (type === "datetime" && value) {
|
||||||
Like: {
|
value = new Date(value).toISOString()
|
||||||
value: "fuzzy",
|
}
|
||||||
label: "Like",
|
if (type === "number") {
|
||||||
},
|
value = parseFloat(value)
|
||||||
MoreThan: {
|
}
|
||||||
value: "rangeLow",
|
if (type === "boolean") {
|
||||||
label: "More than",
|
value = `${value}`?.toLowerCase() === "true"
|
||||||
},
|
}
|
||||||
LessThan: {
|
if (operator.startsWith("range")) {
|
||||||
value: "rangeHigh",
|
if (!query.range[field]) {
|
||||||
label: "Less than",
|
query.range[field] = {
|
||||||
},
|
low:
|
||||||
Contains: {
|
type === "number"
|
||||||
value: "equal",
|
? Number.MIN_SAFE_INTEGER
|
||||||
label: "Contains",
|
: "0000-00-00T00:00:00.000Z",
|
||||||
},
|
high:
|
||||||
NotContains: {
|
type === "number"
|
||||||
value: "notEqual",
|
? Number.MAX_SAFE_INTEGER
|
||||||
label: "Does Not Contain",
|
: "9999-00-00T00:00:00.000Z",
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
if (operator === "rangeLow" && value != null && value !== "") {
|
||||||
|
query.range[field].low = value
|
||||||
|
} else if (operator === "rangeHigh" && value != null && value !== "") {
|
||||||
|
query.range[field].high = value
|
||||||
|
}
|
||||||
|
} else if (query[operator]) {
|
||||||
|
if (type === "boolean") {
|
||||||
|
// Transform boolean filters to cope with null.
|
||||||
|
// "equals false" needs to be "not equals true"
|
||||||
|
// "not equals false" needs to be "equals true"
|
||||||
|
if (operator === "equal" && value === false) {
|
||||||
|
query.notEqual[field] = true
|
||||||
|
} else if (operator === "notEqual" && value === false) {
|
||||||
|
query.equal[field] = true
|
||||||
|
} else {
|
||||||
|
query[operator][field] = value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query[operator][field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getValidOperatorsForType = type => {
|
/**
|
||||||
const Op = OperatorOptions
|
* Performs a client-side lucene search on an array of data
|
||||||
if (type === "string") {
|
* @param docs the data
|
||||||
return [
|
* @param query the JSON lucene query
|
||||||
Op.Equals,
|
*/
|
||||||
Op.NotEquals,
|
export const luceneQuery = (docs, query) => {
|
||||||
Op.StartsWith,
|
if (!query) {
|
||||||
Op.Like,
|
return docs
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
} else if (type === "number") {
|
|
||||||
return [
|
|
||||||
Op.Equals,
|
|
||||||
Op.NotEquals,
|
|
||||||
Op.MoreThan,
|
|
||||||
Op.LessThan,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
} else if (type === "options") {
|
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
|
||||||
} else if (type === "array") {
|
|
||||||
return [Op.Contains, Op.NotContains, Op.Empty, Op.NotEmpty]
|
|
||||||
} else if (type === "boolean") {
|
|
||||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
|
||||||
} else if (type === "longform") {
|
|
||||||
return [
|
|
||||||
Op.Equals,
|
|
||||||
Op.NotEquals,
|
|
||||||
Op.StartsWith,
|
|
||||||
Op.Like,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
} else if (type === "datetime") {
|
|
||||||
return [
|
|
||||||
Op.Equals,
|
|
||||||
Op.NotEquals,
|
|
||||||
Op.MoreThan,
|
|
||||||
Op.LessThan,
|
|
||||||
Op.Empty,
|
|
||||||
Op.NotEmpty,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
return []
|
|
||||||
|
// Iterates over a set of filters and evaluates a fail function against a doc
|
||||||
|
const match = (type, failFn) => doc => {
|
||||||
|
const filters = Object.entries(query[type] || {})
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
if (failFn(filters[i][0], filters[i][1], doc)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process a string match (fails if the value does not start with the string)
|
||||||
|
const stringMatch = match("string", (key, value, doc) => {
|
||||||
|
return !doc[key] || !doc[key].startsWith(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||||
|
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
|
||||||
|
return !doc[key] || !doc[key].startsWith(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a range match
|
||||||
|
const rangeMatch = match("range", (key, value, doc) => {
|
||||||
|
return !doc[key] || doc[key] < value.low || doc[key] > value.high
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process an equal match (fails if the value is different)
|
||||||
|
const equalMatch = match("equal", (key, value, doc) => {
|
||||||
|
return value != null && value !== "" && doc[key] !== value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a not-equal match (fails if the value is the same)
|
||||||
|
const notEqualMatch = match("notEqual", (key, value, doc) => {
|
||||||
|
return value != null && value !== "" && doc[key] === value
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process an empty match (fails if the value is not empty)
|
||||||
|
const emptyMatch = match("empty", (key, value, doc) => {
|
||||||
|
return doc[key] != null && doc[key] !== ""
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process a not-empty match (fails is the value is empty)
|
||||||
|
const notEmptyMatch = match("notEmpty", (key, value, doc) => {
|
||||||
|
return doc[key] == null || doc[key] === ""
|
||||||
|
})
|
||||||
|
|
||||||
|
// Match a document against all criteria
|
||||||
|
const docMatch = doc => {
|
||||||
|
return (
|
||||||
|
stringMatch(doc) &&
|
||||||
|
fuzzyMatch(doc) &&
|
||||||
|
rangeMatch(doc) &&
|
||||||
|
equalMatch(doc) &&
|
||||||
|
notEqualMatch(doc) &&
|
||||||
|
emptyMatch(doc) &&
|
||||||
|
notEmptyMatch(doc)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all docs
|
||||||
|
return docs.filter(docMatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a client-side sort from the equivalent server-side lucene sort
|
||||||
|
* parameters.
|
||||||
|
* @param docs the data
|
||||||
|
* @param sort the sort column
|
||||||
|
* @param sortOrder the sort order ("ascending" or "descending")
|
||||||
|
* @param sortType the type of sort ("string" or "number")
|
||||||
|
*/
|
||||||
|
export const luceneSort = (docs, sort, sortOrder, sortType = "string") => {
|
||||||
|
if (!sort || !sortOrder || !sortType) {
|
||||||
|
return docs
|
||||||
|
}
|
||||||
|
const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x)
|
||||||
|
return docs.slice().sort((a, b) => {
|
||||||
|
const colA = parse(a[sort])
|
||||||
|
const colB = parse(b[sort])
|
||||||
|
if (sortOrder === "Descending") {
|
||||||
|
return colA > colB ? -1 : 1
|
||||||
|
} else {
|
||||||
|
return colA > colB ? 1 : -1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limits the specified docs to the specified number of rows from the equivalent
|
||||||
|
* server-side lucene limit parameters.
|
||||||
|
* @param docs the data
|
||||||
|
* @param limit the number of docs to limit to
|
||||||
|
*/
|
||||||
|
export const luceneLimit = (docs, limit) => {
|
||||||
|
const numLimit = parseFloat(limit)
|
||||||
|
if (isNaN(numLimit)) {
|
||||||
|
return docs
|
||||||
|
}
|
||||||
|
return docs.slice(0, numLimit)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
export const suppressWarnings = warnings => {
|
||||||
|
if (!warnings?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const regex = new RegExp(warnings.map(x => `(${x})`).join("|"), "gi")
|
||||||
|
const warn = console.warn
|
||||||
|
console.warn = (...params) => {
|
||||||
|
const msg = params[0]
|
||||||
|
if (msg && typeof msg === "string") {
|
||||||
|
if (msg.match(regex)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warn(...params)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,11 +7,19 @@ import "@spectrum-css/vars/dist/spectrum-light.css"
|
||||||
import "@spectrum-css/vars/dist/spectrum-lightest.css"
|
import "@spectrum-css/vars/dist/spectrum-lightest.css"
|
||||||
import "@spectrum-css/page/dist/index-vars.css"
|
import "@spectrum-css/page/dist/index-vars.css"
|
||||||
import "./global.css"
|
import "./global.css"
|
||||||
|
import { suppressWarnings } from "./helpers/warnings"
|
||||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
||||||
|
import App from "./App.svelte"
|
||||||
|
|
||||||
|
// Init spectrum icons
|
||||||
loadSpectrumIcons()
|
loadSpectrumIcons()
|
||||||
|
|
||||||
import App from "./App.svelte"
|
// Suppress svelte runtime warnings
|
||||||
|
suppressWarnings([
|
||||||
|
"was created with unknown prop",
|
||||||
|
"was created without expected prop",
|
||||||
|
"received an unexpected slot",
|
||||||
|
])
|
||||||
|
|
||||||
export default new App({
|
export default new App({
|
||||||
target: document.getElementById("app"),
|
target: document.getElementById("app"),
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
// don't react to these
|
||||||
|
let cloud = $admin.cloud
|
||||||
|
let shouldRedirect = !cloud || $admin.disableAccountPortal
|
||||||
|
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
$: hasAdminUser = $admin?.checklist?.adminUser?.checked
|
$: hasAdminUser = $admin?.checklist?.adminUser?.checked
|
||||||
|
@ -39,30 +42,35 @@
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
// We should never see the org or admin user creation screens in the cloud
|
// We should never see the org or admin user creation screens in the cloud
|
||||||
if (!cloud) {
|
|
||||||
const apiReady = $admin.loaded && $auth.loaded
|
const apiReady = $admin.loaded && $auth.loaded
|
||||||
// if tenant is not set go to it
|
// if tenant is not set go to it
|
||||||
if (loaded && apiReady && multiTenancyEnabled && !tenantSet) {
|
if (
|
||||||
|
loaded &&
|
||||||
|
shouldRedirect &&
|
||||||
|
apiReady &&
|
||||||
|
multiTenancyEnabled &&
|
||||||
|
!tenantSet
|
||||||
|
) {
|
||||||
$redirect("./auth/org")
|
$redirect("./auth/org")
|
||||||
}
|
}
|
||||||
// Force creation of an admin user if one doesn't exist
|
// Force creation of an admin user if one doesn't exist
|
||||||
else if (loaded && apiReady && !hasAdminUser) {
|
else if (loaded && shouldRedirect && apiReady && !hasAdminUser) {
|
||||||
$redirect("./admin")
|
$redirect("./admin")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
// Redirect to log in at any time if the user isn't authenticated
|
// Redirect to log in at any time if the user isn't authenticated
|
||||||
$: {
|
else if (
|
||||||
if (
|
|
||||||
loaded &&
|
loaded &&
|
||||||
(hasAdminUser || cloud) &&
|
(hasAdminUser || cloud) &&
|
||||||
!$auth.user &&
|
!$auth.user &&
|
||||||
!$isActive("./auth") &&
|
!$isActive("./auth") &&
|
||||||
!$isActive("./invite")
|
!$isActive("./invite") &&
|
||||||
|
!$isActive("./admin")
|
||||||
) {
|
) {
|
||||||
const returnUrl = encodeURIComponent(window.location.pathname)
|
const returnUrl = encodeURIComponent(window.location.pathname)
|
||||||
$redirect("./auth?", { returnUrl })
|
$redirect("./auth?", { returnUrl })
|
||||||
} else if ($auth?.user?.forceResetPassword) {
|
}
|
||||||
|
// check if password reset required for user
|
||||||
|
else if ($auth.user?.forceResetPassword) {
|
||||||
$redirect("./auth/reset")
|
$redirect("./auth/reset")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script>
|
||||||
|
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
|
||||||
|
import { post } from "builderStore/api"
|
||||||
|
|
||||||
|
let submitting = false
|
||||||
|
|
||||||
|
$: value = { file: null }
|
||||||
|
|
||||||
|
async function importApps() {
|
||||||
|
submitting = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create form data to create app
|
||||||
|
let data = new FormData()
|
||||||
|
data.append("importFile", value.file)
|
||||||
|
|
||||||
|
// Create App
|
||||||
|
const importResp = await post("/api/cloud/import", data, {})
|
||||||
|
const importJson = await importResp.json()
|
||||||
|
if (!importResp.ok) {
|
||||||
|
throw new Error(importJson.message)
|
||||||
|
}
|
||||||
|
// now reload to get to login
|
||||||
|
window.location.reload()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error(error)
|
||||||
|
submitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
title="Import apps"
|
||||||
|
confirmText="Import apps"
|
||||||
|
onConfirm={importApps}
|
||||||
|
disabled={!value.file}
|
||||||
|
>
|
||||||
|
<Body
|
||||||
|
>Please upload the file that was exported from your Cloud environment to get
|
||||||
|
started</Body
|
||||||
|
>
|
||||||
|
<Dropzone
|
||||||
|
gallery={false}
|
||||||
|
label="File to import"
|
||||||
|
value={[value.file]}
|
||||||
|
on:change={e => {
|
||||||
|
value.file = e.detail?.[0]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
|
@ -7,18 +7,22 @@
|
||||||
Input,
|
Input,
|
||||||
Body,
|
Body,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { admin, auth } from "stores/portal"
|
import { admin, auth } from "stores/portal"
|
||||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||||
|
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
|
|
||||||
let adminUser = {}
|
let adminUser = {}
|
||||||
let error
|
let error
|
||||||
|
let modal
|
||||||
|
|
||||||
$: tenantId = $auth.tenantId
|
$: tenantId = $auth.tenantId
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
|
$: cloud = $admin.cloud
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
try {
|
try {
|
||||||
|
@ -38,6 +42,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={modal} padding={false} width="600px">
|
||||||
|
<ImportAppsModal />
|
||||||
|
</Modal>
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -66,6 +73,15 @@
|
||||||
>
|
>
|
||||||
Change organisation
|
Change organisation
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
{:else if !cloud}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
on:click={() => {
|
||||||
|
modal.show()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import from cloud
|
||||||
|
</ActionButton>
|
||||||
{/if}
|
{/if}
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { Icon, Modal, Tabs, Tab } from "@budibase/bbui"
|
import { Icon, Tabs, Tab } from "@budibase/bbui"
|
||||||
import { BUDIBASE_INTERNAL_DB } from "constants"
|
import { BUDIBASE_INTERNAL_DB } from "constants"
|
||||||
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||||
|
|
||||||
let selected = "Sources"
|
let selected = "Sources"
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
$: isExternal =
|
$: isExternal =
|
||||||
$params.selectedDatasource &&
|
$params.selectedDatasource &&
|
||||||
$params.selectedDatasource !== BUDIBASE_INTERNAL_DB
|
$params.selectedDatasource !== BUDIBASE_INTERNAL_DB
|
||||||
|
@ -23,9 +25,7 @@
|
||||||
<Tab title="Sources">
|
<Tab title="Sources">
|
||||||
<div class="tab-content-padding">
|
<div class="tab-content-padding">
|
||||||
<DatasourceNavigator />
|
<DatasourceNavigator />
|
||||||
<Modal bind:this={modal}>
|
<CreateDatasourceModal bind:modal />
|
||||||
<CreateDatasourceModal />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
|
@ -1 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
|
import { queries } from "stores/backend"
|
||||||
|
|
||||||
|
if ($params.query) {
|
||||||
|
const query = $queries.list.find(q => q._id === $params.query)
|
||||||
|
if (query) {
|
||||||
|
queries.select(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { params } from "@roxi/routify"
|
import { params } from "@roxi/routify"
|
||||||
import { datasources } from "stores/backend"
|
import { datasources } from "stores/backend"
|
||||||
|
|
||||||
if ($params.selectedDatasource) {
|
if ($params.selectedDatasource && !$params.query) {
|
||||||
const datasource = $datasources.list.find(
|
const datasource = $datasources.list.find(
|
||||||
m => m._id === $params.selectedDatasource
|
m => m._id === $params.selectedDatasource
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1 @@
|
||||||
<script>
|
|
||||||
import { datasources } from "stores/backend"
|
|
||||||
import { goto, leftover } from "@roxi/routify"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// navigate to first datasource in list, if not already selected
|
|
||||||
if (!$leftover && $datasources.list.length > 0 && !$datasources.selected) {
|
|
||||||
$goto(`./${$datasources.list[0]._id}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<script>
|
|
||||||
import { params } from "@roxi/routify"
|
|
||||||
import { tables } from "stores/backend"
|
|
||||||
|
|
||||||
if ($params.selectedTable) {
|
|
||||||
const table = $tables.list.find(m => m._id === $params.selectedTable)
|
|
||||||
if (table) {
|
|
||||||
tables.select(table)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
import TableDataTable from "components/backend/DataTable/DataTable.svelte"
|
|
||||||
import { tables, database } from "stores/backend"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $database?._id && $tables?.selected?.name}
|
|
||||||
<TableDataTable />
|
|
||||||
{:else}<i>Create your first table to start building</i>{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
i {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
color: var(--grey-5);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,10 +0,0 @@
|
||||||
<script>
|
|
||||||
import { params } from "@roxi/routify"
|
|
||||||
import RelationshipDataTable from "components/backend/DataTable/RelationshipDataTable.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<RelationshipDataTable
|
|
||||||
tableId={$params.selectedTable}
|
|
||||||
rowId={$params.selectedRow}
|
|
||||||
fieldName={decodeURI($params.selectedField)}
|
|
||||||
/>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
$goto("../../")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- routify:options index=false -->
|
|
|
@ -1,6 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
$goto("../")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- routify:options index=false -->
|
|
|
@ -1,19 +0,0 @@
|
||||||
<script>
|
|
||||||
import { tables } from "stores/backend"
|
|
||||||
import { goto, leftover } from "@roxi/routify"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// navigate to first table in list, if not already selected
|
|
||||||
// and this is the final url (i.e. no selectedTable)
|
|
||||||
if (
|
|
||||||
!$leftover &&
|
|
||||||
$tables.list.length > 0
|
|
||||||
// (!$tables.selected || !$tables.selected._id)
|
|
||||||
) {
|
|
||||||
$goto(`./${$tables.list[0]._id}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,21 +0,0 @@
|
||||||
<script>
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { tables } from "stores/backend"
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
$tables.list.length > 0 && $goto(`./${$tables.list[0]._id}`)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $tables.list.length === 0}
|
|
||||||
<i>Create your first table to start building</i>
|
|
||||||
{:else}<i>Select a table to edit</i>{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
i {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
color: var(--grey-5);
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,6 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||||
|
import { datasources } from "stores/backend"
|
||||||
|
|
||||||
|
let modal
|
||||||
|
$: setupComplete =
|
||||||
|
$datasources.list.find(x => (x._id = "bb_internal")).entities.length > 1 ||
|
||||||
|
$datasources.list.length > 1
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!setupComplete) {
|
||||||
|
modal.show()
|
||||||
|
} else {
|
||||||
$goto("./table")
|
$goto("./table")
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<CreateDatasourceModal bind:modal />
|
||||||
<!-- routify:options index=false -->
|
<!-- routify:options index=false -->
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
$redirect("../")
|
$redirect("../")
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to account portal for authentication in the cloud
|
|
||||||
if (
|
if (
|
||||||
!$auth.user &&
|
!$auth.user &&
|
||||||
$admin.cloud &&
|
$admin.cloud &&
|
||||||
|
!$admin.disableAccountPortal &&
|
||||||
$admin.accountPortalUrl &&
|
$admin.accountPortalUrl &&
|
||||||
!$admin?.checklist?.sso?.checked
|
!$admin?.checklist?.sso?.checked
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
let tenantId = get(auth).tenantSet ? get(auth).tenantId : ""
|
let tenantId = get(auth).tenantSet ? get(auth).tenantId : ""
|
||||||
$: multiTenancyEnabled = $admin.multiTenancy
|
$: multiTenancyEnabled = $admin.multiTenancy
|
||||||
$: cloud = $admin.cloud
|
$: cloud = $admin.cloud
|
||||||
|
$: disableAccountPortal = $admin.disableAccountPortal
|
||||||
|
|
||||||
async function setOrg() {
|
async function setOrg() {
|
||||||
if (tenantId == null || tenantId === "") {
|
if (tenantId == null || tenantId === "") {
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await auth.checkQueryString()
|
await auth.checkQueryString()
|
||||||
if (!multiTenancyEnabled || cloud) {
|
if (!multiTenancyEnabled || (cloud && !disableAccountPortal)) {
|
||||||
$goto("../")
|
$goto("../")
|
||||||
} else {
|
} else {
|
||||||
admin.unload()
|
admin.unload()
|
||||||
|
|
|
@ -5,11 +5,9 @@
|
||||||
auth.checkQueryString()
|
auth.checkQueryString()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (!$auth.user) {
|
if ($auth.user?.builder?.global) {
|
||||||
$redirect(`./auth`)
|
|
||||||
} else if ($auth.user.builder?.global) {
|
|
||||||
$redirect(`./portal`)
|
$redirect(`./portal`)
|
||||||
} else {
|
} else if ($auth.user) {
|
||||||
$redirect(`./apps`)
|
$redirect(`./apps`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
Page,
|
Page,
|
||||||
notifications,
|
notifications,
|
||||||
Body,
|
Body,
|
||||||
|
Search,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
@ -35,8 +36,13 @@
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
let creatingApp = false
|
let creatingApp = false
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
let searchTerm = ""
|
||||||
|
let cloud = $admin.cloud
|
||||||
|
|
||||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||||
|
$: filteredApps = enrichedApps.filter(app =>
|
||||||
|
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
|
@ -45,6 +51,7 @@
|
||||||
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
|
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
|
||||||
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
|
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if (sortBy === "status") {
|
if (sortBy === "status") {
|
||||||
return enrichedApps.sort((a, b) => {
|
return enrichedApps.sort((a, b) => {
|
||||||
if (a.status === b.status) {
|
if (a.status === b.status) {
|
||||||
|
@ -70,6 +77,15 @@
|
||||||
creatingApp = true
|
creatingApp = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initiateAppsExport = () => {
|
||||||
|
try {
|
||||||
|
download(`/api/cloud/export`)
|
||||||
|
notifications.success("Apps exported successfully")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error(`Error exporting apps: ${err}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const initiateAppImport = () => {
|
const initiateAppImport = () => {
|
||||||
template = { fromFile: true }
|
template = { fromFile: true }
|
||||||
creationModal.show()
|
creationModal.show()
|
||||||
|
@ -190,6 +206,9 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<Heading>Apps</Heading>
|
<Heading>Apps</Heading>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
{#if cloud}
|
||||||
|
<Button secondary on:click={initiateAppsExport}>Export apps</Button>
|
||||||
|
{/if}
|
||||||
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
||||||
<Button cta on:click={initiateAppCreation}>Create app</Button>
|
<Button cta on:click={initiateAppCreation}>Create app</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
@ -205,6 +224,7 @@
|
||||||
{ label: "Sort by status", value: "status" },
|
{ label: "Sort by status", value: "status" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Search placeholder="Search" bind:value={searchTerm} />
|
||||||
</div>
|
</div>
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
@ -225,7 +245,7 @@
|
||||||
class:appGrid={layout === "grid"}
|
class:appGrid={layout === "grid"}
|
||||||
class:appTable={layout === "table"}
|
class:appTable={layout === "table"}
|
||||||
>
|
>
|
||||||
{#each enrichedApps as app (app.appId)}
|
{#each filteredApps as app (app.appId)}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={layout === "grid" ? AppCard : AppRow}
|
this={layout === "grid" ? AppCard : AppRow}
|
||||||
{releaseLock}
|
{releaseLock}
|
||||||
|
@ -301,7 +321,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
width: 190px;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.appGrid {
|
.appGrid {
|
||||||
|
|
|
@ -67,7 +67,7 @@ export function createDatasourcesStore() {
|
||||||
})
|
})
|
||||||
return json
|
return json
|
||||||
},
|
},
|
||||||
save: async datasource => {
|
save: async (datasource, fetchSchema = false) => {
|
||||||
let response
|
let response
|
||||||
if (datasource._id) {
|
if (datasource._id) {
|
||||||
response = await api.put(
|
response = await api.put(
|
||||||
|
@ -75,7 +75,10 @@ export function createDatasourcesStore() {
|
||||||
datasource
|
datasource
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
response = await api.post("/api/datasources", datasource)
|
response = await api.post("/api/datasources", {
|
||||||
|
datasource: datasource,
|
||||||
|
fetchSchema,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { datasources, integrations, tables } from "./"
|
import { datasources, integrations, tables, views } from "./"
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
export function createQueriesStore() {
|
export function createQueriesStore() {
|
||||||
|
@ -55,10 +55,9 @@ export function createQueriesStore() {
|
||||||
},
|
},
|
||||||
select: query => {
|
select: query => {
|
||||||
update(state => ({ ...state, selected: query._id }))
|
update(state => ({ ...state, selected: query._id }))
|
||||||
tables.update(state => ({
|
views.unselect()
|
||||||
...state,
|
tables.unselect()
|
||||||
selected: null,
|
datasources.unselect()
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
unselect: () => {
|
unselect: () => {
|
||||||
update(state => ({ ...state, selected: null }))
|
update(state => ({ ...state, selected: null }))
|
||||||
|
|
|
@ -95,7 +95,13 @@ export function createTablesStore() {
|
||||||
selected: {},
|
selected: {},
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
saveField: ({ originalName, field, primaryDisplay = false, indexes }) => {
|
saveField: async ({
|
||||||
|
originalName,
|
||||||
|
field,
|
||||||
|
primaryDisplay = false,
|
||||||
|
indexes,
|
||||||
|
}) => {
|
||||||
|
let promise
|
||||||
update(state => {
|
update(state => {
|
||||||
// delete the original if renaming
|
// delete the original if renaming
|
||||||
// need to handle if the column had no name, empty string
|
// need to handle if the column had no name, empty string
|
||||||
|
@ -126,9 +132,12 @@ export function createTablesStore() {
|
||||||
...state.draft.schema,
|
...state.draft.schema,
|
||||||
[field.name]: cloneDeep(field),
|
[field.name]: cloneDeep(field),
|
||||||
}
|
}
|
||||||
save(state.draft)
|
promise = save(state.draft)
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
|
if (promise) {
|
||||||
|
await promise
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteField: field => {
|
deleteField: field => {
|
||||||
update(state => {
|
update(state => {
|
||||||
|
|
|
@ -16,6 +16,7 @@ export function createViewsStore() {
|
||||||
...state,
|
...state,
|
||||||
selected: view,
|
selected: view,
|
||||||
}))
|
}))
|
||||||
|
tables.unselect()
|
||||||
queries.unselect()
|
queries.unselect()
|
||||||
datasources.unselect()
|
datasources.unselect()
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@ export function createAdminStore() {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
multiTenancy: false,
|
multiTenancy: false,
|
||||||
cloud: false,
|
cloud: false,
|
||||||
|
disableAccountPortal: false,
|
||||||
accountPortalUrl: "",
|
accountPortalUrl: "",
|
||||||
onboardingProgress: 0,
|
onboardingProgress: 0,
|
||||||
checklist: {
|
checklist: {
|
||||||
|
@ -47,12 +48,14 @@ export function createAdminStore() {
|
||||||
async function getEnvironment() {
|
async function getEnvironment() {
|
||||||
let multiTenancyEnabled = false
|
let multiTenancyEnabled = false
|
||||||
let cloud = false
|
let cloud = false
|
||||||
|
let disableAccountPortal = false
|
||||||
let accountPortalUrl = ""
|
let accountPortalUrl = ""
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/api/system/environment`)
|
const response = await api.get(`/api/system/environment`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
multiTenancyEnabled = json.multiTenancy
|
multiTenancyEnabled = json.multiTenancy
|
||||||
cloud = json.cloud
|
cloud = json.cloud
|
||||||
|
disableAccountPortal = json.disableAccountPortal
|
||||||
accountPortalUrl = json.accountPortalUrl
|
accountPortalUrl = json.accountPortalUrl
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// just let it stay disabled
|
// just let it stay disabled
|
||||||
|
@ -60,6 +63,7 @@ export function createAdminStore() {
|
||||||
admin.update(store => {
|
admin.update(store => {
|
||||||
store.multiTenancy = multiTenancyEnabled
|
store.multiTenancy = multiTenancyEnabled
|
||||||
store.cloud = cloud
|
store.cloud = cloud
|
||||||
|
store.disableAccountPortal = disableAccountPortal
|
||||||
store.accountPortalUrl = accountPortalUrl
|
store.accountPortalUrl = accountPortalUrl
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "0.9.146",
|
"version": "0.9.147-alpha.0",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "0.9.146",
|
"version": "0.9.147-alpha.0",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^0.9.146",
|
"@budibase/bbui": "^0.9.147-alpha.0",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@budibase/string-templates": "^0.9.146",
|
"@budibase/string-templates": "^0.9.147-alpha.0",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
|
|
|
@ -58,6 +58,10 @@ export default {
|
||||||
find: "sdk",
|
find: "sdk",
|
||||||
replacement: path.resolve("./src/sdk"),
|
replacement: path.resolve("./src/sdk"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
find: "builder",
|
||||||
|
replacement: path.resolve("../builder"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
svelte({
|
svelte({
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
import ErrorSVG from "../../../builder/assets/error.svg"
|
import DNDHandler from "components/preview/DNDHandler.svelte"
|
||||||
|
import ErrorSVG from "builder/assets/error.svg"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
|
@ -104,7 +105,10 @@
|
||||||
<div id="app-root">
|
<div id="app-root">
|
||||||
<CustomThemeWrapper>
|
<CustomThemeWrapper>
|
||||||
{#key $screenStore.activeLayout._id}
|
{#key $screenStore.activeLayout._id}
|
||||||
<Component instance={$screenStore.activeLayout.props} />
|
<Component
|
||||||
|
isLayout
|
||||||
|
instance={$screenStore.activeLayout.props}
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
<!-- Layers on top of app -->
|
<!-- Layers on top of app -->
|
||||||
|
@ -122,6 +126,7 @@
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<SelectionIndicator />
|
<SelectionIndicator />
|
||||||
<HoverIndicator />
|
<HoverIndicator />
|
||||||
|
<DNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</StateBindingsProvider>
|
</StateBindingsProvider>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "components/app/Placeholder.svelte"
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
|
export let isLayout = false
|
||||||
|
export let isScreen = false
|
||||||
|
|
||||||
// The enriched component settings
|
// The enriched component settings
|
||||||
let enrichedSettings
|
let enrichedSettings
|
||||||
|
@ -49,11 +51,11 @@
|
||||||
$: children = instance._children || []
|
$: children = instance._children || []
|
||||||
$: id = instance._id
|
$: id = instance._id
|
||||||
$: name = instance._instanceName
|
$: name = instance._instanceName
|
||||||
$: empty =
|
$: interactive =
|
||||||
!children.length &&
|
$builderStore.inBuilder &&
|
||||||
definition?.hasChildren &&
|
($builderStore.previewType === "layout" || insideScreenslot)
|
||||||
definition?.showEmptyState !== false &&
|
$: empty = interactive && !children.length && definition?.hasChildren
|
||||||
$builderStore.inBuilder
|
$: emptyState = empty && definition?.showEmptyState !== false
|
||||||
$: rawProps = getRawProps(instance)
|
$: rawProps = getRawProps(instance)
|
||||||
$: instanceKey = JSON.stringify(rawProps)
|
$: instanceKey = JSON.stringify(rawProps)
|
||||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||||
|
@ -61,16 +63,16 @@
|
||||||
$builderStore.inBuilder &&
|
$builderStore.inBuilder &&
|
||||||
$builderStore.selectedComponentId === instance._id
|
$builderStore.selectedComponentId === instance._id
|
||||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||||
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
|
|
||||||
$: evaluateConditions(enrichedSettings?._conditions)
|
$: evaluateConditions(enrichedSettings?._conditions)
|
||||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||||
|
$: renderKey = `${propsHash}-${emptyState}`
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: componentStore.set({
|
$: componentStore.set({
|
||||||
id,
|
id,
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: { ...instance._styles, id, empty, interactive },
|
styles: { ...instance._styles, id, empty: emptyState, interactive },
|
||||||
empty,
|
empty: emptyState,
|
||||||
selected,
|
selected,
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
@ -169,13 +171,22 @@
|
||||||
conditionalSettings = result.settingUpdates
|
conditionalSettings = result.settingUpdates
|
||||||
visible = nextVisible
|
visible = nextVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drag and drop helper tags
|
||||||
|
$: draggable = interactive && !isLayout && !isScreen
|
||||||
|
$: droppable = interactive && !isLayout && !isScreen
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key propsHash}
|
{#key renderKey}
|
||||||
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
||||||
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||||
|
<!-- and the performance matters for the selection indicators -->
|
||||||
<div
|
<div
|
||||||
class={`component ${id}`}
|
class={`component ${id}`}
|
||||||
data-type={interactive ? "component" : ""}
|
class:draggable
|
||||||
|
class:droppable
|
||||||
|
class:empty
|
||||||
|
class:interactive
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
>
|
>
|
||||||
|
@ -184,7 +195,7 @@
|
||||||
{#each children as child (child._id)}
|
{#each children as child (child._id)}
|
||||||
<svelte:self instance={child} />
|
<svelte:self instance={child} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if empty}
|
{:else if emptyState}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
|
@ -196,4 +207,10 @@
|
||||||
.component {
|
.component {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
.interactive :global(*:hover) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.draggable :global(*:hover) {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,6 +22,6 @@
|
||||||
<!-- Ensure to fully remount when screen changes -->
|
<!-- Ensure to fully remount when screen changes -->
|
||||||
{#key screenDefinition?._id}
|
{#key screenDefinition?._id}
|
||||||
<Provider key="url" data={params}>
|
<Provider key="url" data={params}>
|
||||||
<Component instance={screenDefinition} />
|
<Component isScreen instance={screenDefinition} />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -31,4 +31,7 @@
|
||||||
.spectrum-Button--overBackground:hover {
|
.spectrum-Button--overBackground:hover {
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
.spectrum-Button::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.valid-container :global([data-type="component"] > *) {
|
.valid-container :global(.component > *) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.direction-row {
|
.direction-row {
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
/* Grow containers inside a row need 0 width 0 so that they ignore content */
|
/* Grow containers inside a row need 0 width 0 so that they ignore content */
|
||||||
/* The nested selector for data-type is the wrapper around all components */
|
/* The nested selector for data-type is the wrapper around all components */
|
||||||
.direction-row :global(> [data-type="component"] > .size-grow) {
|
.direction-row :global(> .component > .size-grow) {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
luceneQuery,
|
luceneQuery,
|
||||||
luceneSort,
|
luceneSort,
|
||||||
luceneLimit,
|
luceneLimit,
|
||||||
} from "utils/lucene"
|
} from "builder/src/helpers/lucene"
|
||||||
import Placeholder from "./Placeholder.svelte"
|
import Placeholder from "./Placeholder.svelte"
|
||||||
|
|
||||||
export let dataSource
|
export let dataSource
|
||||||
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
<script context="module">
|
||||||
|
export const Sides = {
|
||||||
|
Top: "Top",
|
||||||
|
Right: "Right",
|
||||||
|
Bottom: "Bottom",
|
||||||
|
Left: "Left",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
|
let dragInfo
|
||||||
|
let dropInfo
|
||||||
|
|
||||||
|
const getEdges = (bounds, mousePoint) => {
|
||||||
|
const { width, height, top, left } = bounds
|
||||||
|
return {
|
||||||
|
[Sides.Top]: [mousePoint[0], top],
|
||||||
|
[Sides.Right]: [left + width, mousePoint[1]],
|
||||||
|
[Sides.Bottom]: [mousePoint[0], top + height],
|
||||||
|
[Sides.Left]: [left, mousePoint[1]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculatePointDelta = (point1, point2) => {
|
||||||
|
const deltaX = Math.abs(point1[0] - point2[0])
|
||||||
|
const deltaY = Math.abs(point1[1] - point2[1])
|
||||||
|
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDOMNodeForComponent = component => {
|
||||||
|
const parent = component.closest(".component")
|
||||||
|
const children = Array.from(parent.childNodes)
|
||||||
|
return children?.find(node => node?.nodeType === 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when initially starting a drag on a draggable component
|
||||||
|
const onDragStart = e => {
|
||||||
|
const parent = e.target.closest(".component")
|
||||||
|
if (!parent?.classList.contains("draggable")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
dragInfo = {
|
||||||
|
target: parent.dataset.id,
|
||||||
|
parent: parent.dataset.parent,
|
||||||
|
}
|
||||||
|
builderStore.actions.selectComponent(dragInfo.target)
|
||||||
|
builderStore.actions.setDragging(true)
|
||||||
|
|
||||||
|
// Highlight being dragged by setting opacity
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
if (child) {
|
||||||
|
child.style.opacity = "0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when drag stops (whether dropped or not)
|
||||||
|
const onDragEnd = e => {
|
||||||
|
// Reset opacity style
|
||||||
|
if (dragInfo) {
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
if (child) {
|
||||||
|
child.style.opacity = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state and styles
|
||||||
|
dragInfo = null
|
||||||
|
dropInfo = null
|
||||||
|
builderStore.actions.setDragging(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when on top of a component
|
||||||
|
const onDragOver = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo || !dropInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
const { droppableInside, bounds } = dropInfo
|
||||||
|
const { top, left, height, width } = bounds
|
||||||
|
const mouseY = e.clientY
|
||||||
|
const mouseX = e.clientX
|
||||||
|
const snapFactor = droppableInside ? 0.33 : 0.5
|
||||||
|
const snapLimitV = Math.min(40, height * snapFactor)
|
||||||
|
const snapLimitH = Math.min(40, width * snapFactor)
|
||||||
|
|
||||||
|
// Determine all sies we are within snap range of
|
||||||
|
let sides = []
|
||||||
|
if (mouseY <= top + snapLimitV) {
|
||||||
|
sides.push(Sides.Top)
|
||||||
|
} else if (mouseY >= top + height - snapLimitV) {
|
||||||
|
sides.push(Sides.Bottom)
|
||||||
|
}
|
||||||
|
if (mouseX < left + snapLimitH) {
|
||||||
|
sides.push(Sides.Left)
|
||||||
|
} else if (mouseX > left + width - snapLimitH) {
|
||||||
|
sides.push(Sides.Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When no edges match, drop inside if possible
|
||||||
|
if (!sides.length) {
|
||||||
|
dropInfo.mode = droppableInside ? "inside" : null
|
||||||
|
dropInfo.side = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When one edge matches, use that edge
|
||||||
|
if (sides.length === 1) {
|
||||||
|
dropInfo.side = sides[0]
|
||||||
|
if ([Sides.Top, Sides.Left].includes(sides[0])) {
|
||||||
|
dropInfo.mode = "above"
|
||||||
|
} else {
|
||||||
|
dropInfo.mode = "below"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When 2 edges match, work out which is closer
|
||||||
|
const mousePoint = [mouseX, mouseY]
|
||||||
|
const edges = getEdges(bounds, mousePoint)
|
||||||
|
const edge1 = edges[sides[0]]
|
||||||
|
const delta1 = calculatePointDelta(mousePoint, edge1)
|
||||||
|
const edge2 = edges[sides[1]]
|
||||||
|
const delta2 = calculatePointDelta(mousePoint, edge2)
|
||||||
|
const edge = delta1 < delta2 ? sides[0] : sides[1]
|
||||||
|
dropInfo.side = edge
|
||||||
|
if ([Sides.Top, Sides.Left].includes(edge)) {
|
||||||
|
dropInfo.mode = "above"
|
||||||
|
} else {
|
||||||
|
dropInfo.mode = "below"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when entering a potential drop target
|
||||||
|
const onDragEnter = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = e.target.closest(".component")
|
||||||
|
if (
|
||||||
|
element &&
|
||||||
|
element.classList.contains("droppable") &&
|
||||||
|
element.dataset.id !== dragInfo.target
|
||||||
|
) {
|
||||||
|
// Do nothing if this is the same target
|
||||||
|
if (element.dataset.id === dropInfo?.target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the dragging flag is always set.
|
||||||
|
// There's a bit of a race condition between the app reinitialisation
|
||||||
|
// after selecting the DND component and setting this the first time
|
||||||
|
if (!get(builderStore).isDragging) {
|
||||||
|
builderStore.actions.setDragging(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store target ID
|
||||||
|
const target = element.dataset.id
|
||||||
|
|
||||||
|
// Precompute and store some info to avoid recalculating everything in
|
||||||
|
// dragOver
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
const bounds = child.getBoundingClientRect()
|
||||||
|
dropInfo = {
|
||||||
|
target,
|
||||||
|
name: element.dataset.name,
|
||||||
|
droppableInside: element.classList.contains("empty"),
|
||||||
|
bounds,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dropInfo = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when leaving a potential drop target.
|
||||||
|
// Since we don't style our targets, we don't need to unset anything.
|
||||||
|
const onDragLeave = () => {}
|
||||||
|
|
||||||
|
// Callback when dropping a drag on top of some component
|
||||||
|
const onDrop = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (dropInfo?.mode) {
|
||||||
|
builderStore.actions.moveComponent(
|
||||||
|
dragInfo.target,
|
||||||
|
dropInfo.target,
|
||||||
|
dropInfo.mode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Events fired on the draggable target
|
||||||
|
document.addEventListener("dragstart", onDragStart, false)
|
||||||
|
document.addEventListener("dragend", onDragEnd, false)
|
||||||
|
|
||||||
|
// Events fired on the drop targets
|
||||||
|
document.addEventListener("dragover", onDragOver, false)
|
||||||
|
document.addEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.addEventListener("dragleave", onDragLeave, false)
|
||||||
|
document.addEventListener("drop", onDrop, false)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Events fired on the draggable target
|
||||||
|
document.removeEventListener("dragstart", onDragStart, false)
|
||||||
|
document.removeEventListener("dragend", onDragEnd, false)
|
||||||
|
|
||||||
|
// Events fired on the drop targets
|
||||||
|
document.removeEventListener("dragover", onDragOver, false)
|
||||||
|
document.removeEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.removeEventListener("dragleave", onDragLeave, false)
|
||||||
|
document.removeEventListener("drop", onDrop, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<IndicatorSet
|
||||||
|
componentId={dropInfo?.mode === "inside" ? dropInfo.target : null}
|
||||||
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
|
zIndex="930"
|
||||||
|
transition
|
||||||
|
prefix="Inside"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DNDPositionIndicator
|
||||||
|
{dropInfo}
|
||||||
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
|
zIndex="940"
|
||||||
|
transition
|
||||||
|
/>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
import Indicator from "./Indicator.svelte"
|
||||||
|
import { Sides } from "./DNDHandler.svelte"
|
||||||
|
|
||||||
|
export let dropInfo
|
||||||
|
export let zIndex
|
||||||
|
export let color
|
||||||
|
export let transition
|
||||||
|
|
||||||
|
$: dimensions = getDimensions(dropInfo)
|
||||||
|
$: prefix = dropInfo?.mode === "above" ? "Before" : "After"
|
||||||
|
$: text = `${prefix} ${dropInfo?.name}`
|
||||||
|
$: renderKey = `${dropInfo?.target}-${dropInfo?.side}`
|
||||||
|
|
||||||
|
const getDimensions = info => {
|
||||||
|
const { bounds, side } = info ?? {}
|
||||||
|
if (!bounds || !side) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const { left, top, width, height } = bounds
|
||||||
|
if (side === Sides.Top || side === Sides.Bottom) {
|
||||||
|
return {
|
||||||
|
top: side === Sides.Top ? top - 4 : top + height,
|
||||||
|
left: left - 2,
|
||||||
|
width: width + 4,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
top: top - 2,
|
||||||
|
left: side === Sides.Left ? left - 4 : left + width,
|
||||||
|
width: 0,
|
||||||
|
height: height + 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key renderKey}
|
||||||
|
{#if dimensions && dropInfo?.mode !== "inside"}
|
||||||
|
<Indicator
|
||||||
|
left={Math.round(dimensions.left)}
|
||||||
|
top={Math.round(dimensions.top)}
|
||||||
|
width={dimensions.width}
|
||||||
|
height={dimensions.height}
|
||||||
|
{text}
|
||||||
|
{zIndex}
|
||||||
|
{color}
|
||||||
|
{transition}
|
||||||
|
alignRight={dropInfo?.side === Sides.Right}
|
||||||
|
line
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
|
@ -7,7 +7,7 @@
|
||||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = e => {
|
||||||
const element = e.target.closest("[data-type='component']")
|
const element = e.target.closest(".interactive.component")
|
||||||
const newId = element?.dataset?.id
|
const newId = element?.dataset?.id
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
componentId = newId
|
componentId = newId
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
{componentId}
|
componentId={$builderStore.isDragging ? null : componentId}
|
||||||
color="var(--spectrum-global-color-static-blue-200)"
|
color="var(--spectrum-global-color-static-blue-200)"
|
||||||
transition
|
transition
|
||||||
{zIndex}
|
{zIndex}
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
export let color
|
export let color
|
||||||
export let zIndex
|
export let zIndex
|
||||||
export let transition = false
|
export let transition = false
|
||||||
|
export let line = false
|
||||||
|
export let alignRight = false
|
||||||
|
|
||||||
|
$: flipped = top < 20
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -18,11 +22,12 @@
|
||||||
}}
|
}}
|
||||||
out:fade={{ duration: transition ? 130 : 0 }}
|
out:fade={{ duration: transition ? 130 : 0 }}
|
||||||
class="indicator"
|
class="indicator"
|
||||||
class:flipped={top < 20}
|
class:flipped
|
||||||
|
class:line
|
||||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
||||||
>
|
>
|
||||||
{#if text}
|
{#if text}
|
||||||
<div class="text" class:flipped={top < 20}>
|
<div class="text" class:flipped class:line class:right={alignRight}>
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -30,6 +35,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.indicator {
|
.indicator {
|
||||||
|
right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: var(--zIndex);
|
z-index: var(--zIndex);
|
||||||
border: 2px solid var(--color);
|
border: 2px solid var(--color);
|
||||||
|
@ -42,6 +48,9 @@
|
||||||
.indicator.flipped {
|
.indicator.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
.indicator.line {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
.text {
|
.text {
|
||||||
background-color: var(--color);
|
background-color: var(--color);
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -61,9 +70,18 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.text.line {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
.text.flipped {
|
.text.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
transform: translateY(0%);
|
transform: translateY(0%);
|
||||||
top: -2px;
|
top: -2px;
|
||||||
}
|
}
|
||||||
|
.text.right {
|
||||||
|
right: -2px;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let color
|
export let color
|
||||||
export let transition
|
export let transition
|
||||||
export let zIndex
|
export let zIndex
|
||||||
|
export let prefix = null
|
||||||
|
|
||||||
let indicators = []
|
let indicators = []
|
||||||
let interval
|
let interval
|
||||||
|
@ -51,6 +52,9 @@
|
||||||
const parents = document.getElementsByClassName(componentId)
|
const parents = document.getElementsByClassName(componentId)
|
||||||
if (parents.length) {
|
if (parents.length) {
|
||||||
text = parents[0].dataset.name
|
text = parents[0].dataset.name
|
||||||
|
if (prefix) {
|
||||||
|
text = `${prefix} ${text}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch reads to minimize reflow
|
// Batch reads to minimize reflow
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
let measured = false
|
let measured = false
|
||||||
|
|
||||||
$: definition = $builderStore.selectedComponentDefinition
|
$: definition = $builderStore.selectedComponentDefinition
|
||||||
$: showBar = definition?.showSettingsBar
|
$: showBar = definition?.showSettingsBar && !$builderStore.isDragging
|
||||||
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
|
|
|
@ -23,6 +23,7 @@ const createBuilderStore = () => {
|
||||||
theme: null,
|
theme: null,
|
||||||
customTheme: null,
|
customTheme: null,
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
|
isDragging: false,
|
||||||
}
|
}
|
||||||
const writableStore = writable(initialState)
|
const writableStore = writable(initialState)
|
||||||
const derivedStore = derived(writableStore, $state => {
|
const derivedStore = derived(writableStore, $state => {
|
||||||
|
@ -64,13 +65,24 @@ const createBuilderStore = () => {
|
||||||
dispatchEvent("preview-loaded")
|
dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
setSelectedPath: path => {
|
setSelectedPath: path => {
|
||||||
console.log("set to ")
|
|
||||||
console.log(path)
|
|
||||||
writableStore.update(state => {
|
writableStore.update(state => {
|
||||||
state.selectedPath = path
|
state.selectedPath = path
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
moveComponent: (componentId, destinationComponentId, mode) => {
|
||||||
|
dispatchEvent("move-component", {
|
||||||
|
componentId,
|
||||||
|
destinationComponentId,
|
||||||
|
mode,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setDragging: dragging => {
|
||||||
|
writableStore.update(state => {
|
||||||
|
state.isDragging = dragging
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...writableStore,
|
...writableStore,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
import { localStorageStore } from "../../../builder/src/builderStore/store/localStorage"
|
import { localStorageStore } from "builder/src/builderStore/store/localStorage"
|
||||||
import { appStore } from "./app"
|
import { appStore } from "./app"
|
||||||
|
|
||||||
const createStateStore = () => {
|
const createStateStore = () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { buildLuceneQuery, luceneQuery } from "./lucene"
|
import { buildLuceneQuery, luceneQuery } from "builder/src/helpers/lucene"
|
||||||
|
|
||||||
export const getActiveConditions = conditions => {
|
export const getActiveConditions = conditions => {
|
||||||
if (!conditions?.length) {
|
if (!conditions?.length) {
|
||||||
|
|
|
@ -1,179 +0,0 @@
|
||||||
/**
|
|
||||||
* Builds a lucene JSON query from the filter structure generated in the builder
|
|
||||||
* @param filter the builder filter structure
|
|
||||||
*/
|
|
||||||
export const buildLuceneQuery = filter => {
|
|
||||||
let query = {
|
|
||||||
string: {},
|
|
||||||
fuzzy: {},
|
|
||||||
range: {},
|
|
||||||
equal: {},
|
|
||||||
notEqual: {},
|
|
||||||
empty: {},
|
|
||||||
notEmpty: {},
|
|
||||||
contains: {},
|
|
||||||
notContains: {},
|
|
||||||
}
|
|
||||||
if (Array.isArray(filter)) {
|
|
||||||
filter.forEach(expression => {
|
|
||||||
let { operator, field, type, value } = expression
|
|
||||||
// Parse all values into correct types
|
|
||||||
if (type === "datetime" && value) {
|
|
||||||
value = new Date(value).toISOString()
|
|
||||||
}
|
|
||||||
if (type === "number") {
|
|
||||||
value = parseFloat(value)
|
|
||||||
}
|
|
||||||
if (type === "boolean") {
|
|
||||||
value = `${value}`?.toLowerCase() === "true"
|
|
||||||
}
|
|
||||||
if (operator.startsWith("range")) {
|
|
||||||
if (!query.range[field]) {
|
|
||||||
query.range[field] = {
|
|
||||||
low:
|
|
||||||
type === "number"
|
|
||||||
? Number.MIN_SAFE_INTEGER
|
|
||||||
: "0000-00-00T00:00:00.000Z",
|
|
||||||
high:
|
|
||||||
type === "number"
|
|
||||||
? Number.MAX_SAFE_INTEGER
|
|
||||||
: "9999-00-00T00:00:00.000Z",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (operator === "rangeLow" && value != null && value !== "") {
|
|
||||||
query.range[field].low = value
|
|
||||||
} else if (operator === "rangeHigh" && value != null && value !== "") {
|
|
||||||
query.range[field].high = value
|
|
||||||
}
|
|
||||||
} else if (query[operator]) {
|
|
||||||
if (type === "boolean") {
|
|
||||||
// Transform boolean filters to cope with null.
|
|
||||||
// "equals false" needs to be "not equals true"
|
|
||||||
// "not equals false" needs to be "equals true"
|
|
||||||
if (operator === "equal" && value === false) {
|
|
||||||
query.notEqual[field] = true
|
|
||||||
} else if (operator === "notEqual" && value === false) {
|
|
||||||
query.equal[field] = true
|
|
||||||
} else {
|
|
||||||
query[operator][field] = value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query[operator][field] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a client-side lucene search on an array of data
|
|
||||||
* @param docs the data
|
|
||||||
* @param query the JSON lucene query
|
|
||||||
*/
|
|
||||||
export const luceneQuery = (docs, query) => {
|
|
||||||
if (!query) {
|
|
||||||
return docs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterates over a set of filters and evaluates a fail function against a doc
|
|
||||||
const match = (type, failFn) => doc => {
|
|
||||||
const filters = Object.entries(query[type] || {})
|
|
||||||
for (let i = 0; i < filters.length; i++) {
|
|
||||||
if (failFn(filters[i][0], filters[i][1], doc)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process a string match (fails if the value does not start with the string)
|
|
||||||
const stringMatch = match("string", (key, value, doc) => {
|
|
||||||
return !doc[key] || !doc[key].startsWith(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
|
||||||
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
|
|
||||||
return !doc[key] || !doc[key].startsWith(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a range match
|
|
||||||
const rangeMatch = match("range", (key, value, doc) => {
|
|
||||||
return !doc[key] || doc[key] < value.low || doc[key] > value.high
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process an equal match (fails if the value is different)
|
|
||||||
const equalMatch = match("equal", (key, value, doc) => {
|
|
||||||
return value != null && value !== "" && doc[key] !== value
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a not-equal match (fails if the value is the same)
|
|
||||||
const notEqualMatch = match("notEqual", (key, value, doc) => {
|
|
||||||
return value != null && value !== "" && doc[key] === value
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process an empty match (fails if the value is not empty)
|
|
||||||
const emptyMatch = match("empty", (key, value, doc) => {
|
|
||||||
return doc[key] != null && doc[key] !== ""
|
|
||||||
})
|
|
||||||
|
|
||||||
// Process a not-empty match (fails is the value is empty)
|
|
||||||
const notEmptyMatch = match("notEmpty", (key, value, doc) => {
|
|
||||||
return doc[key] == null || doc[key] === ""
|
|
||||||
})
|
|
||||||
|
|
||||||
// Match a document against all criteria
|
|
||||||
const docMatch = doc => {
|
|
||||||
return (
|
|
||||||
stringMatch(doc) &&
|
|
||||||
fuzzyMatch(doc) &&
|
|
||||||
rangeMatch(doc) &&
|
|
||||||
equalMatch(doc) &&
|
|
||||||
notEqualMatch(doc) &&
|
|
||||||
emptyMatch(doc) &&
|
|
||||||
notEmptyMatch(doc)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all docs
|
|
||||||
return docs.filter(docMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs a client-side sort from the equivalent server-side lucene sort
|
|
||||||
* parameters.
|
|
||||||
* @param docs the data
|
|
||||||
* @param sort the sort column
|
|
||||||
* @param sortOrder the sort order ("ascending" or "descending")
|
|
||||||
* @param sortType the type of sort ("string" or "number")
|
|
||||||
*/
|
|
||||||
export const luceneSort = (docs, sort, sortOrder, sortType = "string") => {
|
|
||||||
if (!sort || !sortOrder || !sortType) {
|
|
||||||
return docs
|
|
||||||
}
|
|
||||||
const parse = sortType === "string" ? x => `${x}` : x => parseFloat(x)
|
|
||||||
return docs.slice().sort((a, b) => {
|
|
||||||
const colA = parse(a[sort])
|
|
||||||
const colB = parse(b[sort])
|
|
||||||
if (sortOrder === "Descending") {
|
|
||||||
return colA > colB ? -1 : 1
|
|
||||||
} else {
|
|
||||||
return colA > colB ? 1 : -1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Limits the specified docs to the specified number of rows from the equivalent
|
|
||||||
* server-side lucene limit parameters.
|
|
||||||
* @param docs the data
|
|
||||||
* @param limit the number of docs to limit to
|
|
||||||
*/
|
|
||||||
export const luceneLimit = (docs, limit) => {
|
|
||||||
const numLimit = parseFloat(limit)
|
|
||||||
if (isNaN(numLimit)) {
|
|
||||||
return docs
|
|
||||||
}
|
|
||||||
return docs.slice(0, numLimit)
|
|
||||||
}
|
|
|
@ -23,10 +23,14 @@ export const styleable = (node, styles = {}) => {
|
||||||
let applyHoverStyles
|
let applyHoverStyles
|
||||||
let selectComponent
|
let selectComponent
|
||||||
|
|
||||||
|
// Allow dragging if required
|
||||||
|
const parent = node.closest(".component")
|
||||||
|
if (parent && parent.classList.contains("draggable")) {
|
||||||
|
node.setAttribute("draggable", true)
|
||||||
|
}
|
||||||
|
|
||||||
// Creates event listeners and applies initial styles
|
// Creates event listeners and applies initial styles
|
||||||
const setupStyles = (newStyles = {}) => {
|
const setupStyles = (newStyles = {}) => {
|
||||||
// Use empty state styles as base styles if required, but let them, get
|
|
||||||
// overridden by any user specified styles
|
|
||||||
let baseStyles = {}
|
let baseStyles = {}
|
||||||
if (newStyles.empty) {
|
if (newStyles.empty) {
|
||||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
||||||
|
@ -45,7 +49,6 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Applies a style string to a DOM node
|
// Applies a style string to a DOM node
|
||||||
const applyStyles = styleString => {
|
const applyStyles = styleString => {
|
||||||
node.style = styleString
|
node.style = styleString
|
||||||
node.dataset.componentId = componentId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the "normal" style definition
|
// Applies the "normal" style definition
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "0.9.146",
|
"version": "0.9.147-alpha.0",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -64,9 +64,9 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^0.9.146",
|
"@budibase/auth": "^0.9.147-alpha.0",
|
||||||
"@budibase/client": "^0.9.146",
|
"@budibase/client": "^0.9.147-alpha.0",
|
||||||
"@budibase/string-templates": "^0.9.146",
|
"@budibase/string-templates": "^0.9.147-alpha.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
"@koa/router": "8.0.0",
|
"@koa/router": "8.0.0",
|
||||||
"@sendgrid/mail": "7.1.1",
|
"@sendgrid/mail": "7.1.1",
|
||||||
|
@ -96,6 +96,7 @@
|
||||||
"koa-session": "5.12.0",
|
"koa-session": "5.12.0",
|
||||||
"koa-static": "5.0.0",
|
"koa-static": "5.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"memorystream": "^0.3.1",
|
||||||
"mongodb": "3.6.3",
|
"mongodb": "3.6.3",
|
||||||
"mssql": "6.2.3",
|
"mssql": "6.2.3",
|
||||||
"mysql": "2.18.1",
|
"mysql": "2.18.1",
|
||||||
|
|
|
@ -37,7 +37,7 @@ async function init() {
|
||||||
const envFileJson = {
|
const envFileJson = {
|
||||||
PORT: 4001,
|
PORT: 4001,
|
||||||
MINIO_URL: "http://localhost:10000/",
|
MINIO_URL: "http://localhost:10000/",
|
||||||
COUCH_DB_URL: "http://@localhost:10000/db/",
|
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||||
REDIS_URL: "localhost:6379",
|
REDIS_URL: "localhost:6379",
|
||||||
WORKER_URL: "http://localhost:4002",
|
WORKER_URL: "http://localhost:4002",
|
||||||
INTERNAL_API_KEY: "budibase",
|
INTERNAL_API_KEY: "budibase",
|
||||||
|
@ -48,6 +48,7 @@ async function init() {
|
||||||
COUCH_DB_PASSWORD: "budibase",
|
COUCH_DB_PASSWORD: "budibase",
|
||||||
COUCH_DB_USER: "budibase",
|
COUCH_DB_USER: "budibase",
|
||||||
SELF_HOSTED: 1,
|
SELF_HOSTED: 1,
|
||||||
|
DISABLE_ACCOUNT_PORTAL: "",
|
||||||
MULTI_TENANCY: "",
|
MULTI_TENANCY: "",
|
||||||
}
|
}
|
||||||
let envFile = ""
|
let envFile = ""
|
||||||
|
|
|
@ -31,7 +31,7 @@ const {
|
||||||
getDeployedApps,
|
getDeployedApps,
|
||||||
removeAppFromUserRoles,
|
removeAppFromUserRoles,
|
||||||
} = require("../../utilities/workerRequests")
|
} = require("../../utilities/workerRequests")
|
||||||
const { clientLibraryPath } = require("../../utilities")
|
const { clientLibraryPath, stringToReadStream } = require("../../utilities")
|
||||||
const { getAllLocks } = require("../../utilities/redis")
|
const { getAllLocks } = require("../../utilities/redis")
|
||||||
const {
|
const {
|
||||||
updateClientLibrary,
|
updateClientLibrary,
|
||||||
|
@ -114,8 +114,13 @@ async function createInstance(template) {
|
||||||
|
|
||||||
// replicate the template data to the instance DB
|
// replicate the template data to the instance DB
|
||||||
// this is currently very hard to test, downloading and importing template files
|
// this is currently very hard to test, downloading and importing template files
|
||||||
|
if (template && template.templateString) {
|
||||||
|
const { ok } = await db.load(stringToReadStream(template.templateString))
|
||||||
|
if (!ok) {
|
||||||
|
throw "Error loading database dump from memory."
|
||||||
|
}
|
||||||
|
} else if (template && template.useTemplate === "true") {
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
if (template && template.useTemplate === "true") {
|
|
||||||
const { ok } = await db.load(await getTemplateStream(template))
|
const { ok } = await db.load(await getTemplateStream(template))
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
throw "Error loading database dump from template."
|
throw "Error loading database dump from template."
|
||||||
|
@ -191,10 +196,11 @@ exports.fetchAppPackage = async function (ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async function (ctx) {
|
exports.create = async function (ctx) {
|
||||||
const { useTemplate, templateKey } = ctx.request.body
|
const { useTemplate, templateKey, templateString } = ctx.request.body
|
||||||
const instanceConfig = {
|
const instanceConfig = {
|
||||||
useTemplate,
|
useTemplate,
|
||||||
key: templateKey,
|
key: templateKey,
|
||||||
|
templateString,
|
||||||
}
|
}
|
||||||
if (ctx.request.files && ctx.request.files.templateFile) {
|
if (ctx.request.files && ctx.request.files.templateFile) {
|
||||||
instanceConfig.file = ctx.request.files.templateFile
|
instanceConfig.file = ctx.request.files.templateFile
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
const env = require("../../environment")
|
||||||
|
const { getAllApps } = require("@budibase/auth/db")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
|
const {
|
||||||
|
exportDB,
|
||||||
|
sendTempFile,
|
||||||
|
readFileSync,
|
||||||
|
} = require("../../utilities/fileSystem")
|
||||||
|
const { stringToReadStream } = require("../../utilities")
|
||||||
|
const { getGlobalDBName, getGlobalDB } = require("@budibase/auth/tenancy")
|
||||||
|
const { create } = require("./application")
|
||||||
|
const { getDocParams, DocumentTypes, isDevAppID } = require("../../db/utils")
|
||||||
|
|
||||||
|
async function createApp(appName, appImport) {
|
||||||
|
const ctx = {
|
||||||
|
request: {
|
||||||
|
body: {
|
||||||
|
templateString: appImport,
|
||||||
|
name: appName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return create(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.exportApps = async ctx => {
|
||||||
|
if (env.SELF_HOSTED || !env.MULTI_TENANCY) {
|
||||||
|
ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.")
|
||||||
|
}
|
||||||
|
const apps = await getAllApps(CouchDB, { all: true })
|
||||||
|
const globalDBString = await exportDB(getGlobalDBName())
|
||||||
|
let allDBs = {
|
||||||
|
global: globalDBString,
|
||||||
|
}
|
||||||
|
for (let app of apps) {
|
||||||
|
// only export the dev apps as they will be the latest, the user can republish the apps
|
||||||
|
// in their self hosted environment
|
||||||
|
if (isDevAppID(app._id)) {
|
||||||
|
allDBs[app.name] = await exportDB(app._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filename = `cloud-export-${new Date().getTime()}.txt`
|
||||||
|
ctx.attachment(filename)
|
||||||
|
ctx.body = sendTempFile(JSON.stringify(allDBs))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllDocType(db, docType) {
|
||||||
|
const response = await db.allDocs(
|
||||||
|
getDocParams(docType, null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return response.rows.map(row => row.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importApps = async ctx => {
|
||||||
|
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
|
||||||
|
ctx.throw(400, "Importing only allowed in self hosted environments.")
|
||||||
|
}
|
||||||
|
const apps = await getAllApps(CouchDB, { all: true })
|
||||||
|
if (
|
||||||
|
apps.length !== 0 ||
|
||||||
|
!ctx.request.files ||
|
||||||
|
!ctx.request.files.importFile
|
||||||
|
) {
|
||||||
|
ctx.throw(
|
||||||
|
400,
|
||||||
|
"Import file is required and environment must be fresh to import apps."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const importFile = ctx.request.files.importFile
|
||||||
|
const importString = readFileSync(importFile.path)
|
||||||
|
const dbs = JSON.parse(importString)
|
||||||
|
const globalDbImport = dbs.global
|
||||||
|
// remove from the list of apps
|
||||||
|
delete dbs.global
|
||||||
|
const globalDb = getGlobalDB()
|
||||||
|
// load the global db first
|
||||||
|
await globalDb.load(stringToReadStream(globalDbImport))
|
||||||
|
for (let [appName, appImport] of Object.entries(dbs)) {
|
||||||
|
await createApp(appName, appImport)
|
||||||
|
}
|
||||||
|
// once apps are created clean up the global db
|
||||||
|
let users = await getAllDocType(globalDb, DocumentTypes.USER)
|
||||||
|
for (let user of users) {
|
||||||
|
delete user.tenantId
|
||||||
|
}
|
||||||
|
await globalDb.bulkDocs(users)
|
||||||
|
ctx.body = {
|
||||||
|
message: "Apps successfully imported.",
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,15 +41,10 @@ exports.fetch = async function (ctx) {
|
||||||
|
|
||||||
exports.buildSchemaFromDb = async function (ctx) {
|
exports.buildSchemaFromDb = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.appId)
|
const db = new CouchDB(ctx.appId)
|
||||||
const datasourceId = ctx.params.datasourceId
|
const datasource = await db.get(ctx.params.datasourceId)
|
||||||
const datasource = await db.get(datasourceId)
|
|
||||||
|
|
||||||
const Connector = integrations[datasource.source]
|
const tables = await buildSchemaHelper(datasource)
|
||||||
|
datasource.entities = tables
|
||||||
// Connect to the DB and build the schema
|
|
||||||
const connector = new Connector(datasource.config)
|
|
||||||
await connector.buildSchema(datasource._id, datasource.entities)
|
|
||||||
datasource.entities = connector.tables
|
|
||||||
|
|
||||||
const response = await db.put(datasource)
|
const response = await db.put(datasource)
|
||||||
datasource._rev = response.rev
|
datasource._rev = response.rev
|
||||||
|
@ -81,12 +76,18 @@ exports.update = async function (ctx) {
|
||||||
|
|
||||||
exports.save = async function (ctx) {
|
exports.save = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.appId)
|
const db = new CouchDB(ctx.appId)
|
||||||
const plus = ctx.request.body.plus
|
const plus = ctx.request.body.datasource.plus
|
||||||
|
const fetchSchema = ctx.request.body.fetchSchema
|
||||||
|
|
||||||
const datasource = {
|
const datasource = {
|
||||||
_id: generateDatasourceID({ plus }),
|
_id: generateDatasourceID({ plus }),
|
||||||
type: plus ? DocumentTypes.DATASOURCE_PLUS : DocumentTypes.DATASOURCE,
|
type: plus ? DocumentTypes.DATASOURCE_PLUS : DocumentTypes.DATASOURCE,
|
||||||
...ctx.request.body,
|
...ctx.request.body.datasource,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fetchSchema) {
|
||||||
|
let tables = await buildSchemaHelper(datasource)
|
||||||
|
datasource.entities = tables
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put(datasource)
|
const response = await db.put(datasource)
|
||||||
|
@ -133,3 +134,14 @@ exports.query = async function (ctx) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildSchemaHelper = async datasource => {
|
||||||
|
const Connector = integrations[datasource.source]
|
||||||
|
|
||||||
|
// Connect to the DB and build the schema
|
||||||
|
const connector = new Connector(datasource.config)
|
||||||
|
await connector.buildSchema(datasource._id, datasource.entities)
|
||||||
|
datasource.entities = connector.tables
|
||||||
|
|
||||||
|
return connector.tables
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ const {
|
||||||
generateRowID,
|
generateRowID,
|
||||||
DocumentTypes,
|
DocumentTypes,
|
||||||
InternalTables,
|
InternalTables,
|
||||||
generateMemoryViewID,
|
|
||||||
} = require("../../../db/utils")
|
} = require("../../../db/utils")
|
||||||
const userController = require("../user")
|
const userController = require("../user")
|
||||||
const {
|
const {
|
||||||
|
@ -20,7 +19,12 @@ const { fullSearch, paginatedSearch } = require("./internalSearch")
|
||||||
const { getGlobalUsersFromMetadata } = require("../../../utilities/global")
|
const { getGlobalUsersFromMetadata } = require("../../../utilities/global")
|
||||||
const inMemoryViews = require("../../../db/inMemoryView")
|
const inMemoryViews = require("../../../db/inMemoryView")
|
||||||
const env = require("../../../environment")
|
const env = require("../../../environment")
|
||||||
const { migrateToInMemoryView } = require("../view/utils")
|
const {
|
||||||
|
migrateToInMemoryView,
|
||||||
|
migrateToDesignView,
|
||||||
|
getFromDesignDoc,
|
||||||
|
getFromMemoryDoc,
|
||||||
|
} = require("../view/utils")
|
||||||
|
|
||||||
const CALCULATION_TYPES = {
|
const CALCULATION_TYPES = {
|
||||||
SUM: "sum",
|
SUM: "sum",
|
||||||
|
@ -74,33 +78,24 @@ async function getRawTableData(ctx, db, tableId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getView(db, viewName) {
|
async function getView(db, viewName) {
|
||||||
let viewInfo
|
let mainGetter = env.SELF_HOSTED ? getFromDesignDoc : getFromMemoryDoc
|
||||||
async function getFromDesignDoc() {
|
let secondaryGetter = env.SELF_HOSTED ? getFromMemoryDoc : getFromDesignDoc
|
||||||
const designDoc = await db.get("_design/database")
|
let migration = env.SELF_HOSTED ? migrateToDesignView : migrateToInMemoryView
|
||||||
viewInfo = designDoc.views[viewName]
|
let viewInfo,
|
||||||
return viewInfo
|
migrate = false
|
||||||
}
|
|
||||||
let migrate = false
|
|
||||||
if (env.SELF_HOSTED) {
|
|
||||||
viewInfo = await getFromDesignDoc()
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
viewInfo = await db.get(generateMemoryViewID(viewName))
|
viewInfo = await mainGetter(db, viewName)
|
||||||
if (viewInfo) {
|
|
||||||
viewInfo = viewInfo.view
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// check if it can be retrieved from design doc (needs migrated)
|
// check if it can be retrieved from design doc (needs migrated)
|
||||||
if (err.status !== 404) {
|
if (err.status !== 404) {
|
||||||
viewInfo = null
|
viewInfo = null
|
||||||
} else {
|
} else {
|
||||||
viewInfo = await getFromDesignDoc()
|
viewInfo = await secondaryGetter(db, viewName)
|
||||||
migrate = !!viewInfo
|
migrate = !!viewInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (migrate) {
|
if (migrate) {
|
||||||
await migrateToInMemoryView(db, viewName)
|
await migration(db, viewName)
|
||||||
}
|
}
|
||||||
if (!viewInfo) {
|
if (!viewInfo) {
|
||||||
throw "View does not exist."
|
throw "View does not exist."
|
||||||
|
@ -351,6 +346,11 @@ exports.bulkDestroy = async ctx => {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.search = async ctx => {
|
exports.search = async ctx => {
|
||||||
|
// Fetch the whole table when running in cypress, as search doesn't work
|
||||||
|
if (env.isCypress()) {
|
||||||
|
return { rows: await exports.fetch(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const { tableId } = ctx.params
|
const { tableId } = ctx.params
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
|
|
|
@ -107,3 +107,30 @@ exports.migrateToInMemoryView = async (db, viewName) => {
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
await exports.saveView(db, null, viewName, view)
|
await exports.saveView(db, null, viewName, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.migrateToDesignView = async (db, viewName) => {
|
||||||
|
let view = await db.get(generateMemoryViewID(viewName))
|
||||||
|
const designDoc = await db.get("_design/database")
|
||||||
|
designDoc.views[viewName] = view.view
|
||||||
|
await db.put(designDoc)
|
||||||
|
await db.remove(view._id, view._rev)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getFromDesignDoc = async (db, viewName) => {
|
||||||
|
const designDoc = await db.get("_design/database")
|
||||||
|
let view = designDoc.views[viewName]
|
||||||
|
if (view == null) {
|
||||||
|
throw { status: 404, message: "Unable to get view" }
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getFromMemoryDoc = async (db, viewName) => {
|
||||||
|
let view = await db.get(generateMemoryViewID(viewName))
|
||||||
|
if (view) {
|
||||||
|
view = view.view
|
||||||
|
} else {
|
||||||
|
throw { status: 404, message: "Unable to get view" }
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
const Router = require("@koa/router")
|
||||||
|
const controller = require("../controllers/cloud")
|
||||||
|
const authorized = require("../../middleware/authorized")
|
||||||
|
const { BUILDER } = require("@budibase/auth/permissions")
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
router
|
||||||
|
.get("/api/cloud/export", authorized(BUILDER), controller.exportApps)
|
||||||
|
// has to be public, only run if apps don't exist
|
||||||
|
.post("/api/cloud/import", controller.importApps)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -24,6 +24,7 @@ const hostingRoutes = require("./hosting")
|
||||||
const backupRoutes = require("./backup")
|
const backupRoutes = require("./backup")
|
||||||
const metadataRoutes = require("./metadata")
|
const metadataRoutes = require("./metadata")
|
||||||
const devRoutes = require("./dev")
|
const devRoutes = require("./dev")
|
||||||
|
const cloudRoutes = require("./cloud")
|
||||||
|
|
||||||
exports.mainRoutes = [
|
exports.mainRoutes = [
|
||||||
authRoutes,
|
authRoutes,
|
||||||
|
@ -49,6 +50,7 @@ exports.mainRoutes = [
|
||||||
backupRoutes,
|
backupRoutes,
|
||||||
metadataRoutes,
|
metadataRoutes,
|
||||||
devRoutes,
|
devRoutes,
|
||||||
|
cloudRoutes,
|
||||||
// these need to be handled last as they still use /api/:tableId
|
// these need to be handled last as they still use /api/:tableId
|
||||||
// this could be breaking as koa may recognise other routes as this
|
// this could be breaking as koa may recognise other routes as this
|
||||||
tableRoutes,
|
tableRoutes,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// mock out postgres for this
|
// mock out postgres for this
|
||||||
jest.mock("pg")
|
jest.mock("pg")
|
||||||
|
|
||||||
|
const { findLastKey } = require("lodash/fp")
|
||||||
const setup = require("./utilities")
|
const setup = require("./utilities")
|
||||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||||
const { basicQuery, basicDatasource } = setup.structures
|
const { basicQuery, basicDatasource } = setup.structures
|
||||||
|
@ -19,10 +20,10 @@ describe("/queries", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
async function createInvalidIntegration() {
|
async function createInvalidIntegration() {
|
||||||
const datasource = await config.createDatasource({
|
const datasource = await config.createDatasource({datasource: {
|
||||||
...basicDatasource(),
|
...basicDatasource().datasource,
|
||||||
source: "INVALID_INTEGRATION",
|
source: "INVALID_INTEGRATION",
|
||||||
})
|
}})
|
||||||
const query = await config.createQuery()
|
const query = await config.createQuery()
|
||||||
return { datasource, query }
|
return { datasource, query }
|
||||||
}
|
}
|
||||||
|
@ -183,11 +184,14 @@ describe("/queries", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail with invalid integration type", async () => {
|
it("should fail with invalid integration type", async () => {
|
||||||
const { query } = await createInvalidIntegration()
|
const { query, datasource } = await createInvalidIntegration()
|
||||||
await request
|
await request
|
||||||
.post(`/api/queries/${query._id}`)
|
.post(`/api/queries/${query._id}`)
|
||||||
.send({
|
.send({
|
||||||
|
datasourceId: datasource._id,
|
||||||
parameters: {},
|
parameters: {},
|
||||||
|
fields: {},
|
||||||
|
queryVerb: "read",
|
||||||
})
|
})
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect(400)
|
.expect(400)
|
||||||
|
|
|
@ -317,7 +317,7 @@ describe("/rows", () => {
|
||||||
await request
|
await request
|
||||||
.get(`/api/views/derp`)
|
.get(`/api/views/derp`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
.expect(400)
|
.expect(404)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should be able to run on a view", async () => {
|
it("should be able to run on a view", async () => {
|
||||||
|
|
|
@ -110,6 +110,8 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getDocParams = getDocParams
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets parameters for retrieving tables, this is a utility function for the getDocParams function.
|
* Gets parameters for retrieving tables, this is a utility function for the getDocParams function.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -13,6 +13,10 @@ function isDev() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCypress() {
|
||||||
|
return process.env.NODE_ENV === "cypress"
|
||||||
|
}
|
||||||
|
|
||||||
let LOADED = false
|
let LOADED = false
|
||||||
if (!LOADED && isDev() && !isTest()) {
|
if (!LOADED && isDev() && !isTest()) {
|
||||||
require("dotenv").config()
|
require("dotenv").config()
|
||||||
|
@ -40,6 +44,7 @@ module.exports = {
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
||||||
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
||||||
|
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||||
// minor
|
// minor
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
LOGGER: process.env.LOGGER,
|
LOGGER: process.env.LOGGER,
|
||||||
|
@ -61,6 +66,7 @@ module.exports = {
|
||||||
module.exports[key] = value
|
module.exports[key] = value
|
||||||
},
|
},
|
||||||
isTest,
|
isTest,
|
||||||
|
isCypress,
|
||||||
isDev,
|
isDev,
|
||||||
isProd: () => {
|
isProd: () => {
|
||||||
return !isDev()
|
return !isDev()
|
||||||
|
|
|
@ -85,10 +85,10 @@ module MongoDBModule {
|
||||||
// which method we want to call on the collection
|
// which method we want to call on the collection
|
||||||
switch (query.extra.actionTypes) {
|
switch (query.extra.actionTypes) {
|
||||||
case "insertOne": {
|
case "insertOne": {
|
||||||
return collection.insertOne(query.json)
|
return await collection.insertOne(query.json)
|
||||||
}
|
}
|
||||||
case "insertMany": {
|
case "insertMany": {
|
||||||
return collection.insertOne(query.json).toArray()
|
return await collection.insertOne(query.json).toArray()
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -112,19 +112,19 @@ module MongoDBModule {
|
||||||
|
|
||||||
switch (query.extra.actionTypes) {
|
switch (query.extra.actionTypes) {
|
||||||
case "find": {
|
case "find": {
|
||||||
return collection.find(query.json).toArray()
|
return await collection.find(query.json).toArray()
|
||||||
}
|
}
|
||||||
case "findOne": {
|
case "findOne": {
|
||||||
return collection.findOne(query.json)
|
return await collection.findOne(query.json)
|
||||||
}
|
}
|
||||||
case "findOneAndUpdate": {
|
case "findOneAndUpdate": {
|
||||||
return collection.findOneAndUpdate(query.json)
|
return await collection.findOneAndUpdate(query.json)
|
||||||
}
|
}
|
||||||
case "count": {
|
case "count": {
|
||||||
return collection.countDocuments(query.json)
|
return await collection.countDocuments(query.json)
|
||||||
}
|
}
|
||||||
case "distinct": {
|
case "distinct": {
|
||||||
return collection.distinct(query.json)
|
return await collection.distinct(query.json)
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -148,10 +148,10 @@ module MongoDBModule {
|
||||||
|
|
||||||
switch (query.extra.actionTypes) {
|
switch (query.extra.actionTypes) {
|
||||||
case "updateOne": {
|
case "updateOne": {
|
||||||
return collection.updateOne(query.json)
|
return await collection.updateOne(query.json)
|
||||||
}
|
}
|
||||||
case "updateMany": {
|
case "updateMany": {
|
||||||
return collection.updateMany(query.json).toArray()
|
return await collection.updateMany(query.json).toArray()
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -175,10 +175,10 @@ module MongoDBModule {
|
||||||
|
|
||||||
switch (query.extra.actionTypes) {
|
switch (query.extra.actionTypes) {
|
||||||
case "deleteOne": {
|
case "deleteOne": {
|
||||||
return collection.deleteOne(query.json)
|
return await collection.deleteOne(query.json)
|
||||||
}
|
}
|
||||||
case "deleteMany": {
|
case "deleteMany": {
|
||||||
return collection.deleteMany(query.json).toArray()
|
return await collection.deleteMany(query.json).toArray()
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
@ -6,6 +6,9 @@ jest.mock("../../environment", () => ({
|
||||||
isDev: () => true,
|
isDev: () => true,
|
||||||
_set: () => {},
|
_set: () => {},
|
||||||
}))
|
}))
|
||||||
|
jest.mock("@budibase/auth/tenancy", () => ({
|
||||||
|
getTenantId: () => "testing123"
|
||||||
|
}))
|
||||||
|
|
||||||
const usageQuotaMiddleware = require("../usageQuota")
|
const usageQuotaMiddleware = require("../usageQuota")
|
||||||
const usageQuota = require("../../utilities/usageQuota")
|
const usageQuota = require("../../utilities/usageQuota")
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
const usageQuota = require("../utilities/usageQuota")
|
const usageQuota = require("../utilities/usageQuota")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
const { getTenantId } = require("@budibase/auth/tenancy")
|
||||||
|
|
||||||
|
// tenants without limits
|
||||||
|
const EXCLUDED_TENANTS = ["bb", "default", "bbtest", "bbstaging"]
|
||||||
|
|
||||||
// currently only counting new writes and deletes
|
// currently only counting new writes and deletes
|
||||||
const METHOD_MAP = {
|
const METHOD_MAP = {
|
||||||
|
@ -28,8 +32,10 @@ function getProperty(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async (ctx, next) => {
|
module.exports = async (ctx, next) => {
|
||||||
|
const tenantId = getTenantId()
|
||||||
|
|
||||||
// if in development or a self hosted cloud usage quotas should not be executed
|
// if in development or a self hosted cloud usage quotas should not be executed
|
||||||
if (env.isDev() || env.SELF_HOSTED) {
|
if (env.isDev() || env.SELF_HOSTED || EXCLUDED_TENANTS.includes(tenantId)) {
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue