Merge branch 'develop' into remove-global-info-users-on-deletion
This commit is contained in:
commit
3fb83eabad
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"license": "AGPL-3.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
export let showConfirmButton = true
|
||||
export let showCloseIcon = true
|
||||
export let onConfirm = undefined
|
||||
export let onCancel = undefined
|
||||
export let disabled = false
|
||||
export let showDivider = true
|
||||
|
||||
|
@ -28,6 +29,14 @@
|
|||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function close() {
|
||||
loading = true
|
||||
if (!onCancel || (await onCancel()) !== false) {
|
||||
cancel()
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -65,7 +74,7 @@
|
|||
>
|
||||
<slot name="footer" />
|
||||
{#if showCancelButton}
|
||||
<Button group secondary on:click={cancel}>{cancelText}</Button>
|
||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||
{/if}
|
||||
{#if showConfirmButton}
|
||||
<Button
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
export let selectedRows = []
|
||||
export let editColumnTitle = "Edit"
|
||||
export let customRenderers = []
|
||||
export let disableSorting = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -63,7 +64,7 @@
|
|||
)
|
||||
|
||||
// Reset state when data changes
|
||||
$: data.length, reset()
|
||||
$: rows.length, reset()
|
||||
const reset = () => {
|
||||
nextScrollTop = 0
|
||||
scrollTop = 0
|
||||
|
@ -107,7 +108,7 @@
|
|||
}
|
||||
|
||||
const sortRows = (rows, sortColumn, sortOrder) => {
|
||||
if (!sortColumn || !sortOrder) {
|
||||
if (!sortColumn || !sortOrder || disableSorting) {
|
||||
return rows
|
||||
}
|
||||
return rows.slice().sort((a, b) => {
|
||||
|
@ -131,6 +132,7 @@
|
|||
sortColumn = fieldSchema.name
|
||||
sortOrder = "Descending"
|
||||
}
|
||||
dispatch("sort", { column: sortColumn, order: sortOrder })
|
||||
}
|
||||
|
||||
const getDisplayName = schema => {
|
||||
|
|
|
@ -6,7 +6,7 @@ context("Create a Table", () => {
|
|||
|
||||
it("should create a new Table", () => {
|
||||
cy.createTable("dog")
|
||||
|
||||
cy.wait(1000)
|
||||
// Check if Table exists
|
||||
cy.get(".table-title h1").should("have.text", "dog")
|
||||
})
|
||||
|
@ -36,7 +36,8 @@ context("Create a Table", () => {
|
|||
it("edits a row", () => {
|
||||
cy.contains("button", "Edit").click({ force: true })
|
||||
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("RoverUpdated").should("have.text", "RoverUpdated")
|
||||
})
|
||||
|
@ -62,7 +63,7 @@ context("Create a Table", () => {
|
|||
|
||||
it("deletes a table", () => {
|
||||
cy.get(".actions > :nth-child(1) > .icon > .spectrum-Icon > use")
|
||||
.first()
|
||||
.eq(1)
|
||||
.click({ force: true })
|
||||
cy.get(".spectrum-Menu > :nth-child(2)").click()
|
||||
cy.contains("Delete Table").click()
|
||||
|
|
|
@ -35,8 +35,11 @@ Cypress.Commands.add("createApp", name => {
|
|||
.within(() => {
|
||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||
cy.wait(7000)
|
||||
})
|
||||
.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.get(".nav-item.selected > .content").should("be.visible")
|
||||
})
|
||||
|
@ -69,11 +72,28 @@ Cypress.Commands.add("createTestTableWithData", () => {
|
|||
cy.addColumn("dog", "age", "Number")
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createTable", tableName => {
|
||||
Cypress.Commands.add("createInitialDatasource", tableName => {
|
||||
// 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("Create new table").click()
|
||||
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.wait(1000)
|
||||
cy.get("input").first().type(tableName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.146-alpha.1",
|
||||
"@budibase/client": "^0.9.146-alpha.1",
|
||||
"@budibase/bbui": "^0.9.146-alpha.3",
|
||||
"@budibase/client": "^0.9.146-alpha.3",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.1",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.3",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import FilterDrawer from "components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte"
|
||||
// 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 webhookModal
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { tables, views } from "stores/backend"
|
||||
|
||||
import { fade } from "svelte/transition"
|
||||
import { tables } from "stores/backend"
|
||||
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
||||
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
||||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||
|
@ -8,72 +8,118 @@
|
|||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||
import * as api from "./api"
|
||||
import TableFilterButton from "./buttons/TableFilterButton.svelte"
|
||||
import Table from "./Table.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import { fetchTableData } from "helpers/fetchTableData"
|
||||
import { Pagination } from "@budibase/bbui"
|
||||
|
||||
let hideAutocolumns = true
|
||||
let data = []
|
||||
let loading = false
|
||||
|
||||
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
||||
$: title = $tables.selected?.name
|
||||
$: schema = $tables.selected?.schema
|
||||
$: tableView = {
|
||||
schema,
|
||||
name: $views.selected?.name,
|
||||
}
|
||||
$: type = $tables.selected?.type
|
||||
$: isInternal = type !== "external"
|
||||
$: id = $tables.selected?._id
|
||||
$: search = searchTable(id)
|
||||
$: columnOptions = Object.keys($search.schema || {})
|
||||
|
||||
// Fetch rows for specified table
|
||||
$: {
|
||||
loading = true
|
||||
const loadingTableId = $tables.selected?._id
|
||||
api.fetchDataForTable($tables.selected?._id).then(rows => {
|
||||
loading = false
|
||||
// Fetches new data whenever the table changes
|
||||
const searchTable = tableId => {
|
||||
return fetchTableData({
|
||||
tableId,
|
||||
schema,
|
||||
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
|
||||
}
|
||||
// Fetch data whenever sorting option changes
|
||||
const onSort = e => {
|
||||
search.update({
|
||||
sortColumn: e.detail.column,
|
||||
sortOrder: e.detail.order,
|
||||
})
|
||||
}
|
||||
|
||||
data = rows || []
|
||||
// Fetch data whenever filters change
|
||||
const onFilter = e => {
|
||||
search.update({
|
||||
filters: e.detail,
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch data whenever schema changes
|
||||
const onUpdateColumns = () => {
|
||||
search.update({
|
||||
schema,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Table
|
||||
{title}
|
||||
{schema}
|
||||
tableId={$tables.selected?._id}
|
||||
{data}
|
||||
{type}
|
||||
allowEditing={true}
|
||||
bind:hideAutocolumns
|
||||
{loading}
|
||||
>
|
||||
{#if isInternal}
|
||||
<CreateColumnButton />
|
||||
{/if}
|
||||
{#if schema && Object.keys(schema).length > 0}
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
title={"Create row"}
|
||||
modalContentComponent={CreateEditRow}
|
||||
/>
|
||||
{/if}
|
||||
<div>
|
||||
<Table
|
||||
title={$tables.selected?.name}
|
||||
{schema}
|
||||
{type}
|
||||
tableId={id}
|
||||
data={$search.rows}
|
||||
bind:hideAutocolumns
|
||||
loading={$search.loading}
|
||||
on:sort={onSort}
|
||||
allowEditing
|
||||
disableSorting
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
on:updaterows={search.refresh}
|
||||
>
|
||||
{#if isInternal}
|
||||
<CreateViewButton />
|
||||
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
|
||||
{/if}
|
||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{#if schema && Object.keys(schema).length > 0}
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
on:updaterows={search.refresh}
|
||||
title={"Create row"}
|
||||
modalContentComponent={CreateEditRow}
|
||||
/>
|
||||
{/if}
|
||||
{#if isInternal}
|
||||
<CreateViewButton />
|
||||
{/if}
|
||||
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={$tables.selected?._id} />
|
||||
{#key id}
|
||||
<TableFilterButton {schema} on:change={onFilter} />
|
||||
{/key}
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={$tables.selected?._id} />
|
||||
{/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>
|
||||
import { fade } from "svelte/transition"
|
||||
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 Spinner from "components/common/Spinner.svelte"
|
||||
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||
|
@ -21,6 +20,7 @@
|
|||
export let hideAutocolumns
|
||||
export let rowCount
|
||||
export let type
|
||||
export let disableSorting = false
|
||||
|
||||
let selectedRows = []
|
||||
let editableColumn
|
||||
|
@ -98,47 +98,57 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="table-title">
|
||||
{#if title}
|
||||
<Heading size="S">{title}</Heading>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<div transition:fade>
|
||||
<Spinner size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
<Layout noPadding gap="S">
|
||||
<div>
|
||||
<div class="table-title">
|
||||
{#if title}
|
||||
<Heading size="S">{title}</Heading>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<div transition:fade|local>
|
||||
<Spinner size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="popovers">
|
||||
<slot />
|
||||
{#if !isUsersTable && selectedRows.length > 0}
|
||||
<DeleteRowsButton on:updaterows {selectedRows} {deleteRows} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="popovers">
|
||||
<slot />
|
||||
{#if !isUsersTable && selectedRows.length > 0}
|
||||
<DeleteRowsButton {selectedRows} {deleteRows} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#key tableId}
|
||||
<Table
|
||||
{data}
|
||||
{schema}
|
||||
{loading}
|
||||
{customRenderers}
|
||||
{rowCount}
|
||||
bind:selectedRows
|
||||
allowSelectRows={allowEditing && !isUsersTable}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing && isInternal}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
on:editcolumn={e => editColumn(e.detail)}
|
||||
on:editrow={e => editRow(e.detail)}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
/>
|
||||
{/key}
|
||||
{#key tableId}
|
||||
<div class="table-wrapper" in:fade={{ delay: 200, duration: 100 }}>
|
||||
<Table
|
||||
{data}
|
||||
{schema}
|
||||
{loading}
|
||||
{customRenderers}
|
||||
{rowCount}
|
||||
{disableSorting}
|
||||
bind:selectedRows
|
||||
allowSelectRows={allowEditing && !isUsersTable}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing && isInternal}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
on:editcolumn={e => editColumn(e.detail)}
|
||||
on:editrow={e => editRow(e.detail)}
|
||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||
on:sort
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={editRowModal}>
|
||||
<svelte:component this={editRowComponent} row={editableRow} />
|
||||
<svelte:component this={editRowComponent} on:updaterows row={editableRow} />
|
||||
</Modal>
|
||||
<Modal bind:this={editColumnModal}>
|
||||
<CreateEditColumn field={editableColumn} onClosed={editColumnModal.hide} />
|
||||
<CreateEditColumn
|
||||
field={editableColumn}
|
||||
on:updatecolumns
|
||||
onClosed={editColumnModal.hide}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
|
@ -152,6 +162,9 @@
|
|||
.table-title > div {
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
.table-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.popovers {
|
||||
display: flex;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import Table from "./Table.svelte"
|
||||
import CalculateButton from "./buttons/CalculateButton.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 ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||
|
@ -61,7 +61,7 @@
|
|||
allowEditing={!view?.calculation}
|
||||
bind:hideAutocolumns
|
||||
>
|
||||
<FilterButton {view} />
|
||||
<ViewFilterButton {view} />
|
||||
<CalculateButton {view} />
|
||||
{#if view.calculation}
|
||||
<GroupByButton {view} />
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
Create column
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditColumn />
|
||||
<CreateEditColumn on:updatecolumns />
|
||||
</Modal>
|
||||
|
|
|
@ -12,5 +12,5 @@
|
|||
{title}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<svelte:component this={modalContentComponent} />
|
||||
<svelte:component this={modalContentComponent} on:updaterows />
|
||||
</Modal>
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
||||
export let selectedRows
|
||||
export let deleteRows
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let modal
|
||||
|
||||
async function confirmDeletion() {
|
||||
await deleteRows()
|
||||
modal?.hide()
|
||||
dispatch("updaterows")
|
||||
}
|
||||
</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,
|
||||
Context,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables } from "stores/backend"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
|
@ -30,8 +31,9 @@
|
|||
const AUTO_TYPE = "auto"
|
||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
const LINK_TYPE = FIELDS.LINK.type
|
||||
let fieldDefinitions = cloneDeep(FIELDS)
|
||||
const dispatch = createEventDispatcher()
|
||||
const { hide } = getContext(Context.Modal)
|
||||
let fieldDefinitions = cloneDeep(FIELDS)
|
||||
|
||||
export let field = {
|
||||
type: "string",
|
||||
|
@ -81,12 +83,13 @@
|
|||
if (field.type === AUTO_TYPE) {
|
||||
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
||||
}
|
||||
tables.saveField({
|
||||
await tables.saveField({
|
||||
originalName,
|
||||
field,
|
||||
primaryDisplay,
|
||||
indexes,
|
||||
})
|
||||
dispatch("updatecolumns")
|
||||
}
|
||||
|
||||
function deleteColumn() {
|
||||
|
@ -99,6 +102,7 @@
|
|||
hide()
|
||||
deletion = false
|
||||
}
|
||||
dispatch("updatecolumns")
|
||||
}
|
||||
|
||||
function handleTypeChange(event) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables, rows } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
|
@ -12,6 +13,7 @@
|
|||
export let row = {}
|
||||
|
||||
let errors = []
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: creating = row?._id == null
|
||||
$: table = row.tableId
|
||||
|
@ -43,6 +45,7 @@
|
|||
|
||||
notifications.success("Row saved successfully.")
|
||||
rows.save(rowResponse)
|
||||
dispatch("updaterows")
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables, rows } from "stores/backend"
|
||||
import { roles } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -9,6 +10,7 @@
|
|||
|
||||
export let row = {}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let errors = []
|
||||
|
||||
$: creating = row?._id == null
|
||||
|
@ -71,6 +73,7 @@
|
|||
|
||||
notifications.success("User saved successfully")
|
||||
rows.save(rowResponse)
|
||||
dispatch("updaterows")
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
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 EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
@ -11,16 +11,27 @@
|
|||
import ICONS from "./icons"
|
||||
|
||||
let openDataSources = []
|
||||
$: enrichedDataSources = $datasources.list.map(datasource => ({
|
||||
...datasource,
|
||||
open:
|
||||
openDataSources.includes(datasource._id) ||
|
||||
containsActiveTable(datasource),
|
||||
selected: $datasources.selected === datasource._id,
|
||||
}))
|
||||
$: enrichedDataSources = $datasources.list.map(datasource => {
|
||||
const selected = $datasources.selected === datasource._id
|
||||
const open = openDataSources.includes(datasource._id)
|
||||
const containsSelected = containsActiveEntity(datasource)
|
||||
return {
|
||||
...datasource,
|
||||
selected,
|
||||
open: selected || open || containsSelected,
|
||||
}
|
||||
})
|
||||
$: 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) {
|
||||
toggleNode(datasource)
|
||||
openNode(datasource)
|
||||
datasources.select(datasource._id)
|
||||
$goto(`./datasource/${datasource._id}`)
|
||||
}
|
||||
|
@ -30,12 +41,22 @@
|
|||
$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) {
|
||||
const isOpen = openDataSources.includes(datasource._id)
|
||||
if (isOpen) {
|
||||
openDataSources = openDataSources.filter(id => datasource._id !== id)
|
||||
closeNode(datasource)
|
||||
} else {
|
||||
openDataSources = [...openDataSources, datasource._id]
|
||||
openNode(datasource)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,16 +65,35 @@
|
|||
queries.fetch()
|
||||
})
|
||||
|
||||
const containsActiveTable = datasource => {
|
||||
const activeTableId = get(tables).selected?._id
|
||||
const containsActiveEntity = datasource => {
|
||||
// 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) {
|
||||
return false
|
||||
}
|
||||
let tableOptions = datasource.entities
|
||||
if (!Array.isArray(tableOptions)) {
|
||||
tableOptions = Object.values(tableOptions)
|
||||
|
||||
// Get a list of table options
|
||||
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>
|
||||
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
<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 { capitalise } from "helpers"
|
||||
|
||||
export let integration
|
||||
export let schema
|
||||
let addButton
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<Layout gap="S">
|
||||
{#each Object.keys(schema) as configKey}
|
||||
{#if schema[configKey].type === "object"}
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
<div class="form-row ssl">
|
||||
<Label>{capitalise(configKey)}</Label>
|
||||
<Button secondary thin outline on:click={addButton.addEntry()}
|
||||
>Add</Button
|
||||
>
|
||||
</div>
|
||||
<KeyValueBuilder
|
||||
bind:this={addButton}
|
||||
defaults={schema[configKey].default}
|
||||
bind:object={integration[configKey]}
|
||||
noAddButton={true}
|
||||
/>
|
||||
{:else if schema[configKey].type === "boolean"}
|
||||
<div class="form-row">
|
||||
|
@ -42,4 +50,11 @@
|
|||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row.ssl {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20%;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,74 +1,160 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { datasources } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { Input, Label, ModalContent, Modal, Context } from "@budibase/bbui"
|
||||
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
|
||||
import { ModalContent, Modal, Body, Layout, Detail } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import ICONS from "../icons"
|
||||
import api from "builderStore/api"
|
||||
import { IntegrationNames } from "constants"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { getContext } from "svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
|
||||
const modalContext = getContext(Context.Modal)
|
||||
export let modal
|
||||
let integrations = []
|
||||
let integration = {}
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
|
||||
let tableModal
|
||||
let name
|
||||
let error = ""
|
||||
let integration
|
||||
const INTERNAL = "BUDIBASE"
|
||||
|
||||
$: 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
|
||||
function selectIntegration(integrationType) {
|
||||
const selected = integrations[integrationType]
|
||||
|
||||
// build the schema
|
||||
const config = {}
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
config[key] = selected.datasource[key].default
|
||||
}
|
||||
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,
|
||||
integration = {
|
||||
type: integrationType,
|
||||
plus: selected.plus,
|
||||
config,
|
||||
plus,
|
||||
})
|
||||
notifications.success(`Datasource ${name} created successfully.`)
|
||||
analytics.captureEvent(Events.DATASOURCE.CREATED, { name, type })
|
||||
schema: selected.datasource,
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to new datasource
|
||||
$goto(`./datasource/${response._id}`)
|
||||
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>
|
||||
|
||||
<Modal bind:this={tableModal} on:hide={modalContext.hide}>
|
||||
<CreateTableModal bind:name />
|
||||
<Modal bind:this={internalTableModal}>
|
||||
<CreateTableModal />
|
||||
</Modal>
|
||||
<ModalContent
|
||||
title="Create Datasource"
|
||||
size="L"
|
||||
confirmText="Create"
|
||||
onConfirm={saveDatasource}
|
||||
disabled={error || !name || !integration?.type}
|
||||
>
|
||||
<Input
|
||||
data-cy="datasource-name-input"
|
||||
label="Datasource Name"
|
||||
on:input={checkValid}
|
||||
bind:value={name}
|
||||
{error}
|
||||
/>
|
||||
<Label>Datasource Type</Label>
|
||||
<TableIntegrationMenu bind:integration />
|
||||
</ModalContent>
|
||||
|
||||
<Modal bind:this={externalDatasourceModal}>
|
||||
<DatasourceConfigModal {integration} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
disabled={!Object.keys(integration).length}
|
||||
title="Data"
|
||||
confirmText="Continue"
|
||||
showCancelButton={false}
|
||||
size="M"
|
||||
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"
|
||||
/>
|
||||
|
||||
<span class="icon-spacing">
|
||||
<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>
|
|
@ -12,7 +12,7 @@
|
|||
import { dndzone } from "svelte-dnd-action"
|
||||
import { generate } from "shortid"
|
||||
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 { getComponentForSettingType } from "./componentSettings"
|
||||
import PropertyControl from "./PropertyControl.svelte"
|
||||
|
|
|
@ -13,18 +13,20 @@
|
|||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
||||
import { generate } from "shortid"
|
||||
import { getValidOperatorsForType, OperatorOptions } from "helpers/lucene"
|
||||
import { getValidOperatorsForType, OperatorOptions } from "constants/lucene"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
export let bindings = []
|
||||
export let panel = BindingPanel
|
||||
export let allowBindings = true
|
||||
|
||||
const BannedTypes = ["link", "attachment", "formula"]
|
||||
|
||||
$: fieldOptions = (schemaFields ?? [])
|
||||
.filter(field => !BannedTypes.includes(field.type))
|
||||
.map(field => field.name)
|
||||
$: valueTypeOptions = allowBindings ? ["Value", "Binding"] : ["Value"]
|
||||
|
||||
const addFilter = () => {
|
||||
filters = [
|
||||
|
@ -93,7 +95,7 @@
|
|||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
{#if !filters?.length}
|
||||
Add your first filter column.
|
||||
Add your first filter expression.
|
||||
{:else}
|
||||
Results are filtered to only those which match all of the following
|
||||
constraints.
|
||||
|
@ -117,7 +119,7 @@
|
|||
/>
|
||||
<Select
|
||||
disabled={filter.noValue || !filter.field}
|
||||
options={["Value", "Binding"]}
|
||||
options={valueTypeOptions}
|
||||
bind:value={filter.valueType}
|
||||
placeholder={null}
|
||||
/>
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: internalTable = dataSource?.type === "table"
|
||||
|
||||
const saveFilter = async () => {
|
||||
dispatch("change", tempValue)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
export let defaults
|
||||
export let object = defaults || {}
|
||||
export let readOnly
|
||||
export let noAddButton
|
||||
|
||||
let fields = Object.entries(object).map(([name, value]) => ({ name, value }))
|
||||
|
||||
|
@ -12,7 +13,7 @@
|
|||
{}
|
||||
)
|
||||
|
||||
function addEntry() {
|
||||
export function addEntry() {
|
||||
fields = [...fields, {}]
|
||||
}
|
||||
|
||||
|
@ -32,7 +33,7 @@
|
|||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if !readOnly}
|
||||
{#if !readOnly && !noAddButton}
|
||||
<div>
|
||||
<Button secondary thin outline on:click={addEntry}>Add</Button>
|
||||
</div>
|
||||
|
|
|
@ -15,6 +15,20 @@ export const AppStatus = {
|
|||
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
|
||||
export const UNEDITABLE_USER_FIELDS = [
|
||||
"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: {
|
||||
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",
|
||||
},
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
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,
|
||||
]
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
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/page/dist/index-vars.css"
|
||||
import "./global.css"
|
||||
|
||||
import { suppressWarnings } from "./helpers/warnings"
|
||||
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
|
||||
import App from "./App.svelte"
|
||||
|
||||
// Init spectrum icons
|
||||
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({
|
||||
target: document.getElementById("app"),
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
<script>
|
||||
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 DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
|
||||
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
|
||||
|
||||
let selected = "Sources"
|
||||
|
||||
let modal
|
||||
|
||||
$: isExternal =
|
||||
$params.selectedDatasource &&
|
||||
$params.selectedDatasource !== BUDIBASE_INTERNAL_DB
|
||||
|
@ -23,9 +25,7 @@
|
|||
<Tab title="Sources">
|
||||
<div class="tab-content-padding">
|
||||
<DatasourceNavigator />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateDatasourceModal />
|
||||
</Modal>
|
||||
<CreateDatasourceModal bind:modal />
|
||||
</div>
|
||||
</Tab>
|
||||
</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 />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { params } from "@roxi/routify"
|
||||
import { datasources } from "stores/backend"
|
||||
|
||||
if ($params.selectedDatasource) {
|
||||
if ($params.selectedDatasource && !$params.query) {
|
||||
const datasource = $datasources.list.find(
|
||||
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 />
|
||||
|
|
|
@ -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>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("./table")
|
||||
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")
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<CreateDatasourceModal bind:modal />
|
||||
<!-- routify:options index=false -->
|
||||
|
|
|
@ -67,7 +67,7 @@ export function createDatasourcesStore() {
|
|||
})
|
||||
return json
|
||||
},
|
||||
save: async datasource => {
|
||||
save: async (datasource, fetchSchema = false) => {
|
||||
let response
|
||||
if (datasource._id) {
|
||||
response = await api.put(
|
||||
|
@ -75,7 +75,10 @@ export function createDatasourcesStore() {
|
|||
datasource
|
||||
)
|
||||
} else {
|
||||
response = await api.post("/api/datasources", datasource)
|
||||
response = await api.post("/api/datasources", {
|
||||
datasource: datasource,
|
||||
fetchSchema,
|
||||
})
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { datasources, integrations, tables } from "./"
|
||||
import { datasources, integrations, tables, views } from "./"
|
||||
import api from "builderStore/api"
|
||||
|
||||
export function createQueriesStore() {
|
||||
|
@ -55,10 +55,9 @@ export function createQueriesStore() {
|
|||
},
|
||||
select: query => {
|
||||
update(state => ({ ...state, selected: query._id }))
|
||||
tables.update(state => ({
|
||||
...state,
|
||||
selected: null,
|
||||
}))
|
||||
views.unselect()
|
||||
tables.unselect()
|
||||
datasources.unselect()
|
||||
},
|
||||
unselect: () => {
|
||||
update(state => ({ ...state, selected: null }))
|
||||
|
|
|
@ -95,7 +95,13 @@ export function createTablesStore() {
|
|||
selected: {},
|
||||
}))
|
||||
},
|
||||
saveField: ({ originalName, field, primaryDisplay = false, indexes }) => {
|
||||
saveField: async ({
|
||||
originalName,
|
||||
field,
|
||||
primaryDisplay = false,
|
||||
indexes,
|
||||
}) => {
|
||||
let promise
|
||||
update(state => {
|
||||
// delete the original if renaming
|
||||
// need to handle if the column had no name, empty string
|
||||
|
@ -126,9 +132,12 @@ export function createTablesStore() {
|
|||
...state.draft.schema,
|
||||
[field.name]: cloneDeep(field),
|
||||
}
|
||||
save(state.draft)
|
||||
promise = save(state.draft)
|
||||
return state
|
||||
})
|
||||
if (promise) {
|
||||
await promise
|
||||
}
|
||||
},
|
||||
deleteField: field => {
|
||||
update(state => {
|
||||
|
|
|
@ -16,6 +16,7 @@ export function createViewsStore() {
|
|||
...state,
|
||||
selected: view,
|
||||
}))
|
||||
tables.unselect()
|
||||
queries.unselect()
|
||||
datasources.unselect()
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.146-alpha.1",
|
||||
"@budibase/bbui": "^0.9.146-alpha.3",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.1",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.3",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -58,6 +58,10 @@ export default {
|
|||
find: "sdk",
|
||||
replacement: path.resolve("./src/sdk"),
|
||||
},
|
||||
{
|
||||
find: "builder",
|
||||
replacement: path.resolve("../builder"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
svelte({
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||
import ErrorSVG from "../../../builder/assets/error.svg"
|
||||
import ErrorSVG from "builder/assets/error.svg"
|
||||
|
||||
// Provide contexts
|
||||
setContext("sdk", SDK)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
luceneQuery,
|
||||
luceneSort,
|
||||
luceneLimit,
|
||||
} from "utils/lucene"
|
||||
} from "builder/src/helpers/lucene"
|
||||
import Placeholder from "./Placeholder.svelte"
|
||||
|
||||
export let dataSource
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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"
|
||||
|
||||
const createStateStore = () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { buildLuceneQuery, luceneQuery } from "./lucene"
|
||||
import { buildLuceneQuery, luceneQuery } from "builder/src/helpers/lucene"
|
||||
|
||||
export const getActiveConditions = conditions => {
|
||||
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)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -64,9 +64,9 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^0.9.146-alpha.1",
|
||||
"@budibase/client": "^0.9.146-alpha.1",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.1",
|
||||
"@budibase/auth": "^0.9.146-alpha.3",
|
||||
"@budibase/client": "^0.9.146-alpha.3",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.3",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
"@koa/router": "8.0.0",
|
||||
"@sendgrid/mail": "7.1.1",
|
||||
|
|
|
@ -41,15 +41,10 @@ exports.fetch = async function (ctx) {
|
|||
|
||||
exports.buildSchemaFromDb = async function (ctx) {
|
||||
const db = new CouchDB(ctx.appId)
|
||||
const datasourceId = ctx.params.datasourceId
|
||||
const datasource = await db.get(datasourceId)
|
||||
const datasource = await db.get(ctx.params.datasourceId)
|
||||
|
||||
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
|
||||
const tables = await buildSchemaHelper(datasource)
|
||||
datasource.entities = tables
|
||||
|
||||
const response = await db.put(datasource)
|
||||
datasource._rev = response.rev
|
||||
|
@ -81,12 +76,18 @@ exports.update = async function (ctx) {
|
|||
|
||||
exports.save = async function (ctx) {
|
||||
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 = {
|
||||
_id: generateDatasourceID({ plus }),
|
||||
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)
|
||||
|
@ -133,3 +134,14 @@ exports.query = async function (ctx) {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -351,6 +351,11 @@ exports.bulkDestroy = 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 { tableId } = ctx.params
|
||||
const db = new CouchDB(appId)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// mock out postgres for this
|
||||
jest.mock("pg")
|
||||
|
||||
const { findLastKey } = require("lodash/fp")
|
||||
const setup = require("./utilities")
|
||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||
const { basicQuery, basicDatasource } = setup.structures
|
||||
|
@ -19,10 +20,10 @@ describe("/queries", () => {
|
|||
})
|
||||
|
||||
async function createInvalidIntegration() {
|
||||
const datasource = await config.createDatasource({
|
||||
...basicDatasource(),
|
||||
const datasource = await config.createDatasource({datasource: {
|
||||
...basicDatasource().datasource,
|
||||
source: "INVALID_INTEGRATION",
|
||||
})
|
||||
}})
|
||||
const query = await config.createQuery()
|
||||
return { datasource, query }
|
||||
}
|
||||
|
@ -183,11 +184,14 @@ describe("/queries", () => {
|
|||
})
|
||||
|
||||
it("should fail with invalid integration type", async () => {
|
||||
const { query } = await createInvalidIntegration()
|
||||
const { query, datasource } = await createInvalidIntegration()
|
||||
await request
|
||||
.post(`/api/queries/${query._id}`)
|
||||
.send({
|
||||
datasourceId: datasource._id,
|
||||
parameters: {},
|
||||
fields: {},
|
||||
queryVerb: "read",
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
.expect(400)
|
||||
|
|
|
@ -13,6 +13,10 @@ function isDev() {
|
|||
)
|
||||
}
|
||||
|
||||
function isCypress() {
|
||||
return process.env.NODE_ENV === "cypress"
|
||||
}
|
||||
|
||||
let LOADED = false
|
||||
if (!LOADED && isDev() && !isTest()) {
|
||||
require("dotenv").config()
|
||||
|
@ -61,6 +65,7 @@ module.exports = {
|
|||
module.exports[key] = value
|
||||
},
|
||||
isTest,
|
||||
isCypress,
|
||||
isDev,
|
||||
isProd: () => {
|
||||
return !isDev()
|
||||
|
|
|
@ -6,6 +6,9 @@ jest.mock("../../environment", () => ({
|
|||
isDev: () => true,
|
||||
_set: () => {},
|
||||
}))
|
||||
jest.mock("@budibase/auth/tenancy", () => ({
|
||||
getTenantId: () => "testing123"
|
||||
}))
|
||||
|
||||
const usageQuotaMiddleware = require("../usageQuota")
|
||||
const usageQuota = require("../../utilities/usageQuota")
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
const CouchDB = require("../db")
|
||||
const usageQuota = require("../utilities/usageQuota")
|
||||
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
|
||||
const METHOD_MAP = {
|
||||
|
@ -28,8 +32,10 @@ function getProperty(url) {
|
|||
}
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
const tenantId = getTenantId()
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
|
|
|
@ -70,10 +70,12 @@ exports.basicRole = () => {
|
|||
|
||||
exports.basicDatasource = () => {
|
||||
return {
|
||||
type: "datasource",
|
||||
name: "Test",
|
||||
source: "POSTGRES",
|
||||
config: {},
|
||||
datasource: {
|
||||
type: "datasource",
|
||||
name: "Test",
|
||||
source: "POSTGRES",
|
||||
config: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.9.146-alpha.1",
|
||||
"version": "0.9.146-alpha.3",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -25,8 +25,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^0.9.146-alpha.1",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.1",
|
||||
"@budibase/auth": "^0.9.146-alpha.3",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.3",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
"aws-sdk": "^2.811.0",
|
||||
|
|
|
@ -60,7 +60,7 @@ async function saveUser(
|
|||
// check budibase users in other tenants
|
||||
if (env.MULTI_TENANCY) {
|
||||
dbUser = await getTenantUser(email)
|
||||
if (dbUser != null) {
|
||||
if (dbUser != null && dbUser.tenantId !== tenantId) {
|
||||
throw `Email address ${email} already in use.`
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ async function saveUser(
|
|||
// check root account users in account portal
|
||||
if (!env.SELF_HOSTED) {
|
||||
const account = await accounts.getAccount(email)
|
||||
if (account && account.verified) {
|
||||
if (account && account.verified && account.tenantId !== tenantId) {
|
||||
throw `Email address ${email} already in use.`
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue