1
0
Fork 0
mirror of synced 2024-06-22 04:10:54 +12:00

separation of datasources and queries

This commit is contained in:
Martin McKeaveney 2020-12-18 18:19:43 +00:00
commit 29b51bc6c2
113 changed files with 2915 additions and 1147 deletions

View file

@ -1,5 +1,5 @@
{
"version": "0.3.8",
"version": "0.4.2",
"npmClient": "yarn",
"packages": [
"packages/*"

View file

@ -115,22 +115,22 @@ Cypress.Commands.add("createUser", (email, password, role) => {
// Create User
cy.contains("Users").click()
cy.contains("Create New Row").click()
cy.contains("Create New User").click()
cy.get(".modal").within(() => {
cy.get("input")
.first()
.type(password)
.type(email)
cy.get("input")
.eq(1)
.type(email)
.type(password)
cy.get("select")
.first()
.select(role)
// Save
cy.get(".buttons")
.contains("Create Row")
.contains("Create User")
.click()
})
})

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.3.8",
"version": "0.4.2",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -64,7 +64,7 @@
},
"dependencies": {
"@budibase/bbui": "^1.52.2",
"@budibase/client": "^0.3.8",
"@budibase/client": "^0.4.2",
"@budibase/colorpicker": "^1.0.1",
"@budibase/svelte-ag-grid": "^0.0.16",
"@fortawesome/fontawesome-free": "^5.14.0",

View file

@ -2,9 +2,9 @@ import { getFrontendStore } from "./store/frontend"
import { getBackendUiStore } from "./store/backend"
import { getAutomationStore } from "./store/automation/"
import { getThemeStore } from "./store/theme"
import { derived } from "svelte/store"
import { derived, writable } from "svelte/store"
import analytics from "analytics"
import { LAYOUT_NAMES } from "../constants"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
import { makePropsSafe } from "components/userInterface/assetParsing/createProps"
export const store = getFrontendStore()
@ -13,18 +13,12 @@ export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
export const currentAsset = derived(store, $store => {
const layout = $store.layouts
? $store.layouts.find(layout => layout._id === $store.currentAssetId)
: null
if (layout) return layout
const screen = $store.screens
? $store.screens.find(screen => screen._id === $store.currentAssetId)
: null
if (screen) return screen
const type = $store.currentFrontEndType
if (type === FrontendTypes.SCREEN) {
return $store.screens.find(screen => screen._id === $store.selectedScreenId)
} else if (type === FrontendTypes.LAYOUT) {
return $store.layouts.find(layout => layout._id === $store.selectedLayoutId)
}
return null
})
@ -59,8 +53,14 @@ export const selectedComponent = derived(
}
)
export const currentAssetName = derived(store, () => {
return currentAsset.name
export const currentAssetId = derived(store, $store => {
return $store.currentFrontEndType === FrontendTypes.SCREEN
? $store.selectedScreenId
: $store.selectedLayoutId
})
export const currentAssetName = derived(currentAsset, $currentAsset => {
return $currentAsset?.name
})
// leave this as before for consistency
@ -74,6 +74,8 @@ export const mainLayout = derived(store, $store => {
)
})
export const selectedAccessRole = writable("BASIC")
export const initialise = async () => {
try {
await analytics.activate()

View file

@ -1,11 +1,14 @@
import { writable, get } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import api from "../api"
import { backendUiStore } from ".."
const INITIAL_BACKEND_UI_STATE = {
tables: [],
views: [],
users: [],
roles: [],
datasources: [],
selectedDatabase: {},
selectedTable: {},
draftTable: {},
@ -20,9 +23,12 @@ export const getBackendUiStore = () => {
select: async db => {
const tablesResponse = await api.get(`/api/tables`)
const tables = await tablesResponse.json()
const datasourcesResponse = await api.get(`/api/datasources`)
const datasources = await datasourcesResponse.json()
store.update(state => {
state.selectedDatabase = db
state.tables = tables
state.datasources = datasources
return state
})
},
@ -44,6 +50,69 @@ export const getBackendUiStore = () => {
return state
}),
},
datasources: {
fetch: async () => {
const response = await api.get(`/api/datasources`)
const json = await response.json()
store.update(state => {
state.datasources = json
return state
})
return json
},
select: async datasource => {
store.update(state => {
state.selectedDatasourceId = datasource._id
return state
})
},
save: async datasource => {
const response = await api.post("/api/datasources", datasource)
const json = await response.json()
store.update(state => {
const currentIdx = state.datasources.findIndex(
ds => ds._id === json._id
)
if (currentIdx >= 0) {
state.datasources.splice(currentIdx, 1, json)
} else {
state.datasources.push(json)
}
state.datasources = state.datasources
return state
})
},
saveQuery: async (datasourceId, query) => {
const response = await api.post(
`/api/datasources/${datasourceId}/queries`,
query
)
const json = await response.json()
store.update(state => {
const currentIdx = state.datasources.findIndex(
ds => ds._id === json._id
)
if (currentIdx >= 0) {
state.datasources.splice(currentIdx, 1, json)
} else {
state.datasources.push(json)
}
state.datasources = state.datasources
return state
})
},
},
queries: {
select: queryId =>
store.update(state => {
state.selectedQueryId = queryId
return state
}),
},
tables: {
fetch: async () => {
const tablesResponse = await api.get(`/api/tables`)
@ -177,6 +246,26 @@ export const getBackendUiStore = () => {
return state
}),
},
roles: {
fetch: async () => {
const response = await api.get("/api/roles")
const roles = await response.json()
store.update(state => {
state.roles = roles
return state
})
},
delete: async role => {
const response = await api.delete(`/api/roles/${role._id}/${role._rev}`)
await store.actions.roles.fetch()
return response
},
save: async role => {
const response = await api.post("/api/roles", role)
await store.actions.roles.fetch()
return response
},
},
}
return store

View file

@ -3,7 +3,6 @@ import { cloneDeep } from "lodash/fp"
import {
createProps,
getBuiltin,
makePropsSafe,
} from "components/userInterface/assetParsing/createProps"
import {
allScreens,
@ -11,6 +10,7 @@ import {
currentAsset,
mainLayout,
selectedComponent,
selectedAccessRole,
} from "builderStore"
import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
import api from "../api"
@ -32,7 +32,8 @@ const INITIAL_FRONTEND_STATE = {
screens: [],
components: [],
currentFrontEndType: "none",
currentAssetId: "",
selectedScreenId: "",
selectedLayoutId: "",
selectedComponentId: "",
errors: [],
hasAppPackage: false,
@ -83,28 +84,31 @@ export const getFrontendStore = () => {
},
},
screens: {
select: async screenId => {
let promise
select: screenId => {
store.update(state => {
const screen = get(allScreens).find(screen => screen._id === screenId)
let screens = get(allScreens)
let screen =
screens.find(screen => screen._id === screenId) || screens[0]
if (!screen) return state
state.currentFrontEndType = FrontendTypes.SCREEN
state.currentAssetId = screenId
state.currentView = "detail"
// Update role to the screen's role setting so that it will always
// be visible
selectedAccessRole.set(screen.routing.roleId)
promise = store.actions.screens.regenerateCss(screen)
state.currentFrontEndType = FrontendTypes.SCREEN
state.selectedScreenId = screen._id
state.currentView = "detail"
state.selectedComponentId = screen.props?._id
return state
})
await promise
},
create: async screen => {
screen = await store.actions.screens.save(screen)
store.update(state => {
state.currentAssetId = screen._id
state.selectedScreenId = screen._id
state.selectedComponentId = screen.props._id
state.currentFrontEndType = FrontendTypes.SCREEN
selectedAccessRole.set(screen.routing.roleId)
return state
})
return screen
@ -113,6 +117,7 @@ export const getFrontendStore = () => {
const creatingNewScreen = screen._id === undefined
const response = await api.post(`/api/screens`, screen)
screen = await response.json()
await store.actions.routing.fetch()
store.update(state => {
const foundScreen = state.screens.findIndex(
@ -122,28 +127,14 @@ export const getFrontendStore = () => {
state.screens.splice(foundScreen, 1)
}
state.screens.push(screen)
if (creatingNewScreen) {
const safeProps = makePropsSafe(
state.components[screen.props._component],
screen.props
)
state.selectedComponentId = safeProps._id
screen.props = safeProps
}
return state
})
return screen
},
regenerateCss: async asset => {
const response = await api.post("/api/css/generate", asset)
asset._css = (await response.json())?.css
},
regenerateCssForCurrentScreen: async () => {
const asset = get(currentAsset)
if (asset) {
await store.actions.screens.regenerateCss(asset)
if (creatingNewScreen) {
store.actions.screens.select(screen._id)
}
return screen
},
delete: async screens => {
const screensToDelete = Array.isArray(screens) ? screens : [screens]
@ -159,8 +150,8 @@ export const getFrontendStore = () => {
`/api/screens/${screenToDelete._id}/${screenToDelete._rev}`
)
)
if (screenToDelete._id === state.currentAssetId) {
state.currentAssetId = ""
if (screenToDelete._id === state.selectedScreenId) {
state.selectedScreenId = null
}
}
return state
@ -181,50 +172,44 @@ export const getFrontendStore = () => {
},
},
layouts: {
select: async layoutId => {
select: layoutId => {
store.update(state => {
const layout = store.actions.layouts.find(layoutId)
const layout =
store.actions.layouts.find(layoutId) || get(store).layouts[0]
if (!layout) return
state.currentFrontEndType = FrontendTypes.LAYOUT
state.currentView = "detail"
state.currentAssetId = layout._id
state.selectedLayoutId = layout._id
state.selectedComponentId = layout.props?._id
return state
})
let cssPromises = []
cssPromises.push(store.actions.screens.regenerateCssForCurrentScreen())
for (let screen of get(allScreens)) {
cssPromises.push(store.actions.screens.regenerateCss(screen))
}
await Promise.all(cssPromises)
},
save: async layout => {
const layoutToSave = cloneDeep(layout)
delete layoutToSave._css
const creatingNewLayout = layoutToSave._id === undefined
const response = await api.post(`/api/layouts`, layoutToSave)
const json = await response.json()
const savedLayout = await response.json()
store.update(state => {
const layoutIdx = state.layouts.findIndex(
stateLayout => stateLayout._id === json._id
stateLayout => stateLayout._id === savedLayout._id
)
if (layoutIdx >= 0) {
// update existing layout
state.layouts.splice(layoutIdx, 1, json)
state.layouts.splice(layoutIdx, 1, savedLayout)
} else {
// save new layout
state.layouts.push(json)
state.layouts.push(savedLayout)
}
state.currentAssetId = json._id
return state
})
// Select layout if creating a new one
if (creatingNewLayout) {
store.actions.layouts.select(savedLayout._id)
}
return savedLayout
},
find: layoutId => {
if (!layoutId) {
@ -237,16 +222,17 @@ export const getFrontendStore = () => {
const response = await api.delete(
`/api/layouts/${layoutToDelete._id}/${layoutToDelete._rev}`
)
if (response.status !== 200) {
const json = await response.json()
throw new Error(json.message)
}
store.update(state => {
state.layouts = state.layouts.filter(
layout => layout._id !== layoutToDelete._id
)
if (layoutToDelete._id === state.selectedLayoutId) {
state.selectedLayoutId = get(mainLayout)._id
}
return state
})
},
@ -372,7 +358,6 @@ export const getFrontendStore = () => {
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
promises.push(store.actions.preview.saveSelected())
store.actions.components.select(componentToPaste)
@ -390,8 +375,6 @@ export const getFrontendStore = () => {
}
selected._styles[type][name] = value
promises.push(store.actions.screens.regenerateCssForCurrentScreen())
// save without messing with the store
promises.push(store.actions.preview.saveSelected())
return state
@ -476,13 +459,8 @@ export const getFrontendStore = () => {
}).props
}
// Save layout and regenerate all CSS because otherwise weird things happen
// Save layout
nav._children = [...nav._children, newLink]
state.currentAssetId = layout._id
promises.push(store.actions.screens.regenerateCss(layout))
for (let screen of get(allScreens)) {
promises.push(store.actions.screens.regenerateCss(screen))
}
promises.push(store.actions.layouts.save(layout))
}
return state

View file

@ -4,12 +4,17 @@
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
import CreateViewButton from "./buttons/CreateViewButton.svelte"
import ExportButton from "./buttons/ExportButton.svelte"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import * as api from "./api"
import Table from "./Table.svelte"
import { TableNames } from "constants"
import CreateEditUser from "./modals/CreateEditUser.svelte"
import CreateEditRow from "./modals/CreateEditRow.svelte"
let data = []
let loading = false
$: isUsersTable = $backendUiStore.selectedTable?._id === TableNames.USERS
$: title = $backendUiStore.selectedTable.name
$: schema = $backendUiStore.selectedTable.schema
$: tableView = {
@ -29,11 +34,22 @@
}
</script>
<Table {title} {schema} {data} allowEditing={true} {loading}>
<Table
{title}
{schema}
tableId={$backendUiStore.selectedTable?._id}
{data}
allowEditing={true}
{loading}>
<CreateColumnButton />
{#if schema && Object.keys(schema).length > 0}
<CreateRowButton />
<CreateRowButton
title={isUsersTable ? 'Create New User' : 'Create New Row'}
modalContentComponent={isUsersTable ? CreateEditUser : CreateEditRow} />
<CreateViewButton />
<ExportButton view={tableView} />
{/if}
{#if isUsersTable}
<EditRolesButton />
{/if}
</Table>

View file

@ -1,32 +1,63 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import * as api from "./api"
import Table from "./Table.svelte"
import EditIntegrationConfigButton from "./buttons/EditIntegrationConfigButton.svelte"
import CreateQueryButton from "components/backend/DataTable/buttons/CreateQueryButton.svelte"
export let datasourceId
export let query = {}
let data = []
let loading = false
let error = false
$: table = $backendUiStore.selectedTable
$: title = table.name
$: schema = table.schema
$: tableView = {
schema,
name: $backendUiStore.selectedView.name,
$: datasourceId = $params.selectedDatasource
// TODO: refactor
// $: query = $backendUiStore.datasources.find(
// ds => ds._id === $params.selectedDatasource
// ).queries[$params.query]
$: title = query.name
$: schema = query.schema
async function fetchData() {
try {
loading = true
const rows = await api.fetchDataForQuery(
$params.selectedDatasource,
$params.query
)
data = rows || []
error = false
} catch (err) {
console.log(err)
error = `${query}: Query error. (${err.message}). This could be a problem with your datasource configuration.`
notifier.danger(error)
} finally {
loading = false
}
}
// Fetch rows for specified table
// Fetch rows for specified query
$: {
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
loading = true
api.fetchDataForView($backendUiStore.selectedView).then(rows => {
data = rows || []
loading = false
})
}
fetchData()
}
</script>
{#if error}
<div class="errors">{error}</div>
{/if}
<Table {title} {schema} {data} {loading}>
<EditIntegrationConfigButton {table} />
<CreateQueryButton {query} />
</Table>
<style>
.errors {
color: var(--red);
background: var(--red-light);
padding: var(--spacing-m);
border-radius: var(--border-radius-m);
margin-bottom: var(--spacing-m);
}
</style>

View file

@ -7,20 +7,16 @@
Toggle,
RichText,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let meta
export let creating
export let value = meta.type === "boolean" ? false : ""
export let readonly
$: type = meta.type
$: label = capitalise(meta.name)
$: editingUser =
!creating && $backendUiStore.selectedTable?._id === TableNames.USERS
</script>
{#if type === 'options'}
@ -53,5 +49,5 @@
data-cy="{meta.name}-input"
{type}
bind:value
disabled={editingUser} />
disabled={readonly} />
{/if}

View file

@ -7,10 +7,15 @@
import { notifier } from "builderStore/store/notifications"
import Spinner from "components/common/Spinner.svelte"
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
import { getRenderer, editRowRenderer } from "./cells/cellRenderers"
import {
getRenderer,
editRowRenderer,
userRowRenderer,
} from "./cells/cellRenderers"
import TableLoadingOverlay from "./TableLoadingOverlay"
import TableHeader from "./TableHeader"
import "@budibase/svelte-ag-grid/dist/index.css"
import { TableNames } from "constants"
export let schema = {}
export let data = []
@ -42,7 +47,18 @@
animateRows: true,
}
$: isUsersTable = tableId === TableNames.USERS
$: {
if (isUsersTable) {
schema.email.displayFieldName = "Email"
schema.roleId.displayFieldName = "Role"
}
}
$: {
// Reset selection every time data changes
selectedRows = []
let result = []
if (allowEditing) {
result = [
@ -57,23 +73,34 @@
suppressMenu: true,
minWidth: 114,
width: 114,
cellRenderer: editRowRenderer,
cellRenderer: isUsersTable ? userRowRenderer : editRowRenderer,
},
]
}
Object.keys(schema || {}).forEach((key, idx) => {
const canEditColumn = key => {
if (!allowEditing) {
return false
}
return !(isUsersTable && ["email", "roleId"].includes(key))
}
Object.entries(schema || {}).forEach(([key, value]) => {
result.push({
headerCheckboxSelection: false,
headerComponent: TableHeader,
headerComponentParams: {
field: schema[key],
editable: allowEditing,
editable: canEditColumn(key),
},
headerName: key,
headerName: value.displayFieldName || key,
field: key,
sortable: true,
cellRenderer: getRenderer(schema[key], true),
cellRenderer: getRenderer({
schema: schema[key],
editable: true,
isUsersTable,
}),
cellRendererParams: {
selectRelationship,
},

View file

@ -23,5 +23,22 @@ export async function fetchDataForView(view) {
const FETCH_ROWS_URL = `/api/views/${view.name}`
const response = await api.get(FETCH_ROWS_URL)
return await response.json()
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
return json
}
export async function fetchDataForQuery(datasourceId, queryId) {
const FETCH_QUERY_URL = `/api/datasources/${datasourceId}/queries/${queryId}`
const response = await api.get(FETCH_QUERY_URL)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
return json
}

View file

@ -0,0 +1,47 @@
<script>
import { goto } from "@sveltech/routify"
import {
DropdownMenu,
TextButton as Button,
Icon,
Label,
Modal,
ModalContent,
TextArea,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte"
import CreateEditQuery from "components/backend/DataTable/modals/CreateEditQuery.svelte"
export let datasource
let modal
let query = {}
let fields = []
async function saveQuery() {
try {
await backendUiStore.actions.datasources.saveQuery(datasource._id, query)
} catch (err) {
console.error(err)
// TODO: notifier
}
}
</script>
<div>
<Button text small on:click={modal.show}>
<Icon name="filter" />
Create Query
</Button>
</div>
<Modal bind:this={modal}>
<ModalContent
confirmText="Save"
cancelText="Cancel"
onConfirm={saveQuery}
title="Create New Query">
<CreateEditQuery {datasource} bind:query />
</ModalContent>
</Modal>

View file

@ -1,6 +1,9 @@
<script>
import { TextButton as Button, Icon, Modal } from "@budibase/bbui"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
import CreateEditRow from "../modals/CreateEditRow.svelte"
export let modalContentComponent = CreateEditRow
export let title = "Create New Row"
let modal
</script>
@ -8,9 +11,9 @@
<div>
<Button text small on:click={modal.show}>
<Icon name="addrow" />
Create New Row
{title}
</Button>
</div>
<Modal bind:this={modal}>
<CreateEditRowModal />
<svelte:component this={modalContentComponent} />
</Modal>

View file

@ -0,0 +1,41 @@
<script>
import {
DropdownMenu,
TextButton as Button,
Icon,
Modal,
ModalContent,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import EditIntegrationConfig from "../modals/EditIntegrationConfig.svelte"
export let table
let modal
// TODO: revisit
async function saveTable() {
const SAVE_TABLE_URL = `/api/tables`
const response = await api.post(SAVE_TABLE_URL, table)
const savedTable = await response.json()
await backendUiStore.actions.tables.fetch()
backendUiStore.actions.tables.select(savedTable)
}
</script>
<div>
<Button text small on:click={modal.show}>
<Icon name="edit" />
Configure Schema
</Button>
</div>
<Modal bind:this={modal}>
<ModalContent
confirmText="Save"
cancelText="Cancel"
onConfirm={saveTable}
title={'Datasource Configuration'}>
<EditIntegrationConfig onClosed={modal.hide} bind:table />
</ModalContent>
</Modal>

View file

@ -0,0 +1,23 @@
<script>
import { TextButton as Button, Modal } from "@budibase/bbui"
import EditRolesModal from "../modals/EditRoles.svelte"
let modal
</script>
<div>
<Button text small on:click={modal.show}>
<i class="ri-lock-line" />
Edit Roles
</Button>
</div>
<Modal bind:this={modal}>
<EditRolesModal />
</Modal>
<style>
i {
margin-right: var(--spacing-xs);
font-size: var(--font-size-s);
}
</style>

View file

@ -0,0 +1,10 @@
<script>
import { backendUiStore } from "builderStore"
export let roleId
$: role = $backendUiStore.roles.find(role => role._id === roleId)
$: roleName = role?.name ?? "Unknown role"
</script>
<div>{roleName}</div>

View file

@ -1,16 +1,25 @@
import AttachmentList from "./AttachmentCell.svelte"
import EditRow from "../modals/EditRow.svelte"
import CreateEditUser from "../modals/CreateEditUser.svelte"
import DeleteRow from "../modals/DeleteRow.svelte"
import RelationshipDisplay from "./RelationshipCell.svelte"
import RoleCell from "./RoleCell.svelte"
const renderers = {
attachment: attachmentRenderer,
link: linkedRowRenderer,
}
export function getRenderer(schema, editable) {
export function getRenderer({ schema, editable, isUsersTable }) {
const rendererParams = {
options: schema.options,
constraints: schema.constraints,
editable,
}
if (renderers[schema.type]) {
return renderers[schema.type](schema.options, schema.constraints, editable)
return renderers[schema.type](rendererParams)
} else if (isUsersTable && schema.name === "roleId") {
return roleRenderer(rendererParams)
} else {
return false
}
@ -45,15 +54,31 @@ export function editRowRenderer(params) {
return container
}
/* eslint-disable no-unused-vars */
function attachmentRenderer(options, constraints, editable) {
export function userRowRenderer(params) {
const container = document.createElement("div")
container.style.height = "100%"
container.style.display = "flex"
container.style.alignItems = "center"
new EditRow({
target: container,
props: {
row: params.data,
modalContentComponent: CreateEditUser,
},
})
return container
}
function attachmentRenderer() {
return params => {
const container = document.createElement("div")
container.style.height = "100%"
container.style.display = "flex"
container.style.alignItems = "center"
const attachmentInstance = new AttachmentList({
new AttachmentList({
target: container,
props: {
files: params.value || [],
@ -64,7 +89,6 @@ function attachmentRenderer(options, constraints, editable) {
}
}
/* eslint-disable no-unused-vars */
function linkedRowRenderer() {
return params => {
let container = document.createElement("div")
@ -84,3 +108,21 @@ function linkedRowRenderer() {
return container
}
}
function roleRenderer() {
return params => {
let container = document.createElement("div")
container.style.display = "grid"
container.style.height = "100%"
container.style.alignItems = "center"
new RoleCell({
target: container,
props: {
roleId: params.value,
},
})
return container
}
}

View file

@ -0,0 +1,190 @@
<script>
import { onMount } from "svelte"
import {
Select,
Button,
Label,
Input,
TextArea,
Heading,
Spacer,
Switcher,
} from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import api from "builderStore/api"
import { FIELDS } from "constants/backend"
import IntegrationQueryEditor from "components/integration/index.svelte"
import { backendUiStore } from "builderStore"
const PREVIEW_HEADINGS = [
{
title: "Preview",
key: "PREVIEW",
},
{
title: "Schema",
key: "SCHEMA",
},
]
export let datasource
export let query
export let fields = []
let config = {}
let queryType
let previewTab = "PREVIEW"
let preview
$: query.schema = fields.reduce(
(acc, next) => ({
...acc,
[next.name]: {
name: next.name,
type: "string",
},
}),
{}
)
function newField() {
fields = [...fields, {}]
}
function deleteField(idx) {
fields.splice(idx, 1)
fields = fields
}
async function fetchQueryConfig() {
try {
const response = await api.get(`/api/integrations/${datasource.source}`)
const json = await response.json()
config = json.query
} catch (err) {
// TODO: Error fetching integration config
// notifier.danger()
console.error(err)
}
}
async function previewQuery() {
try {
const response = await api.post(`/api/datasources/queries/preview`, {
type: datasource.source,
config: datasource.config,
query: query.queryString,
})
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
preview = json[0] || {}
// TODO: refactor
fields = Object.keys(preview).map(field => ({
name: field,
type: "STRING",
}))
} catch (err) {
notifier.danger(`Query Error: ${err.message}`)
console.error(err)
}
}
onMount(() => {
fetchQueryConfig()
})
</script>
<section>
<div class="config">
<h6>Datasource Type</h6>
<span>{datasource.source}</span>
<Spacer medium />
<Label extraSmall grey>Query Name</Label>
<Input type="text" thin bind:value={query.name} />
<Spacer medium />
<Label extraSmall grey>Query Type</Label>
<Select secondary bind:value={queryType}>
<option value={''}>Select an option</option>
{#each Object.keys(config) as queryType}
<option value={queryType}>{queryType}</option>
{/each}
</Select>
<Spacer medium />
<IntegrationQueryEditor {queryType} bind:query={query.queryString} />
<Spacer small />
<Button thin secondary on:click={previewQuery}>Preview Query</Button>
<Spacer small />
{#if preview}
<Switcher headings={PREVIEW_HEADINGS} bind:value={previewTab}>
{#if previewTab === 'PREVIEW'}
<pre class="preview">{JSON.stringify(preview, undefined, 2)}</pre>
{:else if previewTab === 'SCHEMA'}
{#each fields as field, idx}
<div class="field">
<Input thin type={'text'} bind:value={field.name} />
<Select secondary thin bind:value={field.type}>
<option value={''}>Select an option</option>
<option value={'STRING'}>Text</option>
<option value={'NUMBER'}>Number</option>
<option value={'BOOLEAN'}>Boolean</option>
<option value={'DATETIME'}>Datetime</option>
</Select>
<i
class="ri-close-circle-line delete"
on:click={() => deleteField(idx)} />
</div>
{/each}
<Button thin secondary on:click={newField}>Add Field</Button>
{/if}
</Switcher>
{/if}
</div>
</section>
<style>
.field {
display: grid;
grid-gap: 10px;
grid-template-columns: 1fr 1fr 50px;
margin-bottom: var(--spacing-m);
}
h6 {
font-family: var(--font-sans);
font-weight: 600;
text-rendering: var(--text-render);
color: var(--ink);
font-size: var(--heading-font-size-xs);
color: var(--ink);
margin-bottom: var(--spacing-xs);
margin-top: var(--spacing-xs);
}
.config {
margin-bottom: var(--spacing-s);
}
.delete {
align-self: center;
cursor: pointer;
}
.preview {
width: 800px;
overflow-wrap: break-word;
white-space: pre-wrap;
}
</style>

View file

@ -1,6 +1,5 @@
<script>
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
@ -40,15 +39,9 @@
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#if creating && table._id === TableNames.USERS}
<RowFieldControl
{creating}
meta={{ name: 'password', type: 'password' }}
bind:value={row.password} />
{/if}
{#each tableSchema as [key, meta]}
<div>
<RowFieldControl {meta} bind:value={row[key]} {creating} />
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{/each}
</ModalContent>

View file

@ -0,0 +1,85 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as backendApi from "../api"
import { ModalContent, Select } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let row = {}
let errors = []
$: creating = row?._id == null
$: table = row.tableId
? $backendUiStore.tables.find(table => table._id === row?.tableId)
: $backendUiStore.selectedTable
$: tableSchema = getUserSchema(table)
$: customSchemaKeys = getCustomSchemaKeys(tableSchema)
const getUserSchema = table => {
let schema = table?.schema ?? {}
if (schema.username) {
schema.username.name = "Username"
}
return schema
}
const getCustomSchemaKeys = schema => {
let customSchema = { ...schema }
delete customSchema["email"]
delete customSchema["roleId"]
return Object.entries(customSchema)
}
const saveRow = async () => {
const rowResponse = await backendApi.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
if (Array.isArray(rowResponse.errors)) {
errors = rowResponse.errors.map(error => ({ message: error }))
} else {
errors = Object.entries(rowResponse.errors)
.map(([key, error]) => ({ dataPath: key, message: error }))
.flat()
}
return false
}
notifier.success("User saved successfully.")
backendUiStore.actions.rows.save(rowResponse)
}
</script>
<ModalContent
title={creating ? 'Create User' : 'Edit User'}
confirmText={creating ? 'Create User' : 'Save User'}
onConfirm={saveRow}>
<ErrorsBox {errors} />
<RowFieldControl
meta={{ ...tableSchema.email, name: 'Email' }}
bind:value={row.email}
readonly={!creating} />
<RowFieldControl
meta={{ name: 'password', type: 'password' }}
bind:value={row.password} />
<!-- Defer rendering this select until roles load, otherwise the initial
selection is always undefined -->
<Select
thin
secondary
label="Role"
data-cy="roleId-select"
bind:value={row.roleId}>
<option value="">Choose an option</option>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
{#each customSchemaKeys as [key, meta]}
<RowFieldControl {meta} bind:value={row[key]} {creating} />
{/each}
</ModalContent>

View file

@ -7,11 +7,14 @@
Heading,
Spacer,
} from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import { FIELDS } from "constants/backend"
import { backendUiStore } from "builderStore"
import * as api from "../api"
export let table
let smartSchemaRow
let fields = Object.keys(table.schema).map(field => ({
name: field,
type: table.schema[field].type.toUpperCase(),
@ -34,11 +37,29 @@
fields.splice(idx, 1)
fields = fields
}
async function smartSchema() {
try {
const rows = await api.fetchDataForView($backendUiStore.selectedView)
const first = rows[0]
smartSchemaRow = first
fields = Object.keys(first).map(key => ({
// TODO: Smarter type mapping
name: key,
type: "STRING",
}))
} catch (err) {
notifier.danger("Error determining schema. Please enter fields manually.")
}
}
</script>
<section>
<div class="config">
<h6>Schema</h6>
{#if smartSchemaRow}
<pre>{JSON.stringify(smartSchemaRow, undefined, 2)}</pre>
{/if}
{#each fields as field, idx}
<div class="field">
<Input thin type={'text'} bind:value={field.name} />
@ -55,6 +76,7 @@
</div>
{/each}
<Button thin secondary on:click={newField}>Add Field</Button>
<Button thin primary on:click={smartSchema}>Smart Schema</Button>
</div>
<div class="config">
@ -102,5 +124,6 @@
.delete {
align-self: center;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,132 @@
<script>
import { ModalContent, Select, Input, Button } from "@budibase/bbui"
import { onMount } from "svelte"
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import { backendUiStore } from "builderStore"
let permissions = []
let selectedRole = {}
let errors = []
$: selectedRoleId = selectedRole._id
$: otherRoles = $backendUiStore.roles.filter(
role => role._id !== selectedRoleId
)
$: isCreating = selectedRoleId == null || selectedRoleId === ""
const fetchPermissions = async () => {
const permissionsResponse = await api.get("/api/permissions")
permissions = await permissionsResponse.json()
}
// Changes the selected role
const changeRole = event => {
const id = event?.target?.value
const role = $backendUiStore.roles.find(role => role._id === id)
if (role) {
selectedRole = {
...role,
inherits: role.inherits ?? "",
permissionId: role.permissionId ?? "",
}
} else {
selectedRole = { _id: "", inherits: "", permissionId: "" }
}
errors = []
}
// Saves or creates the selected role
const saveRole = async () => {
errors = []
// Clean up empty strings
const keys = ["_id", "inherits", "permissionId"]
keys.forEach(key => {
if (selectedRole[key] === "") {
delete selectedRole[key]
}
})
// Validation
if (!selectedRole.name || selectedRole.name.trim() === "") {
errors.push({ message: "Please enter a role name" })
}
if (!selectedRole.permissionId) {
errors.push({ message: "Please choose permissions" })
}
if (errors.length) {
return false
}
// Save/create the role
const response = await backendUiStore.actions.roles.save(selectedRole)
if (response.status === 200) {
notifier.success("Role saved successfully.")
} else {
notifier.danger("Error saving role.")
return false
}
}
// Deletes the selected role
const deleteRole = async () => {
const response = await backendUiStore.actions.roles.delete(selectedRole)
if (response.status === 200) {
changeRole()
notifier.success("Role deleted successfully.")
} else {
notifier.danger("Error deleting role.")
}
}
onMount(fetchPermissions)
</script>
<ModalContent
title="Edit Roles"
confirmText={isCreating ? 'Create' : 'Save'}
onConfirm={saveRole}>
{#if errors.length}
<ErrorsBox {errors} />
{/if}
<Select
thin
secondary
label="Role"
value={selectedRoleId}
on:change={changeRole}>
<option value="">Create new role</option>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
{#if selectedRole}
<Input label="Name" bind:value={selectedRole.name} thin />
<Select
thin
secondary
label="Inherits Role"
bind:value={selectedRole.inherits}>
<option value="">None</option>
{#each otherRoles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
<Select
thin
secondary
label="Permissions"
bind:value={selectedRole.permissionId}>
<option value="">Choose permissions</option>
{#each permissions as permission}
<option value={permission._id}>{permission.name}</option>
{/each}
</Select>
{/if}
<div slot="footer">
{#if !isCreating}
<Button red on:click={deleteRole}>Delete</Button>
{/if}
</div>
</ModalContent>

View file

@ -1,8 +1,9 @@
<script>
import { Modal, Button } from "@budibase/bbui"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
import CreateEditRow from "../modals/CreateEditRow.svelte"
export let row
export let modalContentComponent = CreateEditRow
let modal
@ -14,5 +15,5 @@
<Button data-cy="edit-row" secondary small on:click={showModal}>Edit</Button>
<Modal bind:this={modal}>
<CreateEditRowModal {row} />
<svelte:component this={modalContentComponent} {row} />
</Modal>

View file

@ -0,0 +1,85 @@
<script>
import { onMount } from "svelte"
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import CreateDatasourceModal from "./modals/CreateDatasourceModal.svelte"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
import { Modal, Switcher } from "@budibase/bbui"
import NavItem from "components/common/NavItem.svelte"
let modal
$: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name
function selectDatasource(datasource) {
// You can't actually select a datasource, just edit it
backendUiStore.actions.datasources.select(datasource)
$goto(`./datasource/${datasource._id}`)
}
function onClickQuery(datasourceId, queryId) {
if ($backendUiStore.selectedQueryId === queryId) {
return
}
backendUiStore.actions.queries.select(queryId)
$goto(`./datasource/${datasourceId}/${queryId}`)
}
onMount(() => {
backendUiStore.actions.datasources.fetch()
})
</script>
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="title">
<i
data-cy="new-datasource"
on:click={modal.show}
class="ri-add-circle-fill" />
</div>
<div class="hierarchy-items-container">
{#each $backendUiStore.datasources as datasource, idx}
<NavItem
border={idx > 0}
icon={'ri-database-2-line'}
text={datasource.name}
selected={$backendUiStore.selectedDatasourceId === datasource._id}
on:click={() => selectDatasource(datasource)}>
<EditDatasourcePopover {datasource} />
</NavItem>
{#each Object.keys(datasource.queries) as queryId}
<NavItem
indentLevel={1}
icon="ri-eye-line"
text={datasource.queries[queryId].name}
selected={selectedView === queryId}
on:click={() => onClickQuery(datasource._id, queryId)}>
<!-- <EditViewPopover
view={{ name: viewName, ...table.views[viewName] }} /> -->
</NavItem>
{/each}
{/each}
</div>
{/if}
<Modal bind:this={modal}>
<CreateDatasourceModal />
</Modal>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.title i {
font-size: 20px;
}
.title i:hover {
cursor: pointer;
color: var(--blue);
}
</style>

View file

@ -0,0 +1,55 @@
<script>
export let icon
export let className
export let title
export let selected
export let indented
</script>
<div
data-cy="table-nav-item"
class:indented
class:selected
on:click
class={className}>
<i class={icon} />
<span>{title}</span>
<slot />
</div>
<style>
.indented {
grid-template-columns: 46px 1fr 20px;
}
.indented i {
justify-self: end;
}
div {
padding: var(--spacing-s) var(--spacing-m);
border-radius: var(--border-radius-m);
display: grid;
grid-template-columns: 20px 1fr 20px;
align-items: center;
transition: 0.3s background-color;
color: var(--ink);
font-weight: 400;
font-size: 14px;
margin-bottom: var(--spacing-xs);
grid-gap: var(--spacing-s);
}
.selected {
background-color: var(--grey-2);
}
div:hover {
background-color: var(--grey-1);
cursor: pointer;
}
i {
color: var(--grey-7);
font-size: 20px;
}
</style>

View file

@ -1,5 +1,5 @@
<script>
import { Input, TextArea } from "@budibase/bbui"
import { Input, TextArea, Spacer } from "@budibase/bbui"
export let integration
</script>
@ -11,5 +11,6 @@
type={configKey.type}
label={configKey}
bind:value={integration[configKey]} />
<Spacer medium />
{/each}
</form>

View file

@ -1,6 +1,6 @@
<script>
import { onMount } from "svelte"
import { Input, TextArea } from "@budibase/bbui"
import { Input, TextArea, Spacer } from "@budibase/bbui"
import api from "builderStore/api"
const INTEGRATION_ICON_MAP = {
@ -14,8 +14,7 @@
let integrations = []
async function fetchIntegrations() {
const INTEGRATIONS_URL = `/api/integrations`
const response = await api.get(INTEGRATIONS_URL)
const response = await api.get("/api/integrations")
const json = await response.json()
integrations = json
return json
@ -31,12 +30,21 @@
{#each Object.keys(integrations) as integrationType}
<div
class="integration hoverable"
class:selected={integration.type === integrationType}
on:click={() => {
selectedIntegration = integrations[integrationType]
integration.type = integrationType
selectedIntegration = integrations[integrationType].datasource
integration = { type: integrationType, ...Object.keys(selectedIntegration).reduce(
(acc, next) => {
return {
...acc,
[next]: selectedIntegration[next].default,
}
},
{}
) }
}}>
<span>{integrationType}</span>
<i class="ri-database-2-line" />
<span>{integrationType}</span>
</div>
{/each}
</div>
@ -48,6 +56,7 @@
type={selectedIntegration[configKey].type}
label={configKey}
bind:value={integration[configKey]} />
<Spacer medium />
{/each}
{/if}
</section>
@ -60,6 +69,7 @@
.integration-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: var(--spacing-m);
}
.integration {
@ -76,7 +86,8 @@
margin-bottom: var(--spacing-xs);
}
.integration:hover {
.integration:hover,
.selected {
background-color: var(--grey-3);
}
</style>

View file

@ -0,0 +1,57 @@
<script>
import { goto, params } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import analytics from "analytics"
let modal
let error = ""
let name
let source
let integration
let datasource
function checkValid(evt) {
const datasourceName = evt.target.value
if ($backendUiStore.datasources?.some(datasource => datasource.name === datasourceName)) {
error = `Datasource with name ${datasourceName} already exists. Please choose another name.`
return
}
error = ""
}
async function saveDatasource() {
const { type, ...config } = integration
// Create datasource
await backendUiStore.actions.datasources.save({
name,
source: type,
config
})
notifier.success(`Datasource ${name} created successfully.`)
analytics.captureEvent("Datasource Created", { name })
// Navigate to new datasource
$goto(`./datasource/${datasource._id}`)
}
</script>
<ModalContent
title="Create Datasource"
confirmText="Create"
onConfirm={saveDatasource}
disabled={error || !name}>
<Input
data-cy="datasource-name-input"
thin
label="Datasource Name"
on:input={checkValid}
bind:value={name}
{error} />
<Label grey extraSmall>Create Integrated Table from External Source</Label>
<TableIntegrationMenu bind:integration />
</ModalContent>

View file

@ -0,0 +1,57 @@
<script>
import { goto, params } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import analytics from "analytics"
let modal
let error = ""
let name
let source
let integration
let datasource
function checkValid(evt) {
const datasourceName = evt.target.value
if ($backendUiStore.datasources?.some(datasource => datasource.name === datasourceName)) {
error = `Datasource with name ${tableName} already exists. Please choose another name.`
return
}
error = ""
}
async function saveDatasource() {
const { type, ...config } = integration
// Create datasource
await backendUiStore.actions.datasources.save({
name,
source: type,
config
})
notifier.success(`Datasource ${name} created successfully.`)
analytics.captureEvent("Datasource Created", { name })
// Navigate to new datasource
$goto(`./datasource/${datasource._id}`)
}
</script>
<ModalContent
title="Create Datasource"
confirmText="Create"
onConfirm={saveDatasource}
disabled={error || !name}>
<Input
data-cy="datasource-name-input"
thin
label="Datasource Name"
on:input={checkValid}
bind:value={name}
{error} />
<Label grey extraSmall>Create Integrated Table from External Source</Label>
<TableIntegrationMenu bind:integration />
</ModalContent>

View file

@ -0,0 +1,89 @@
<script>
import { backendUiStore, store, allScreens } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let datasource
let anchor
let dropdown
let confirmDeleteDialog
let error = ""
let originalName = datasource.name
let willBeDeleted
function hideEditor() {
dropdown?.hide()
}
function showModal() {
hideEditor()
confirmDeleteDialog.show()
}
async function deleteDatasource() {
// TODO: update the store correctly
await backendUiStore.actions.datasources.delete(datasource)
// await backendUiStore.actions.datasources.fetch()
notifier.success("Datasource deleted")
hideEditor()
}
</script>
<div on:click|stopPropagation>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
<DropdownContainer>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={showModal}
data-cy="delete-datasource" />
</DropdownContainer>
</DropdownMenu>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Datasource"
onOk={deleteDatasource}
title="Confirm Deletion">
Are you sure you wish to delete the datasource
<i>{datasource.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
.actions {
padding: var(--spacing-xl);
display: grid;
grid-gap: var(--spacing-xl);
min-width: 400px;
}
h5 {
margin: 0;
font-weight: 500;
}
footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-m);
}
</style>

View file

@ -2,11 +2,10 @@
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { TableNames } from "constants"
import ListItem from "./ListItem.svelte"
import CreateTableModal from "./modals/CreateTableModal.svelte"
import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte"
import { Modal } from "@budibase/bbui"
import { Modal, Switcher } from "@budibase/bbui"
import NavItem from "components/common/NavItem.svelte"
let modal
@ -37,7 +36,6 @@
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="title">
<h1>Tables</h1>
<i data-cy="new-table" on:click={modal.show} class="ri-add-circle-fill" />
</div>
<div class="hierarchy-items-container">
@ -48,7 +46,9 @@
text={table.name}
selected={selectedView === `all_${table._id}`}
on:click={() => selectTable(table)}>
<EditTablePopover {table} />
{#if table._id !== TableNames.USERS}
<EditTablePopover {table} />
{/if}
</NavItem>
{#each Object.keys(table.views || {}) as viewName}
<NavItem

View file

@ -2,9 +2,8 @@
import { goto, params } from "@sveltech/routify"
import { backendUiStore, store } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { Input, Label, ModalContent } from "@budibase/bbui"
import { Input, Label, ModalContent, Button, Spacer } from "@budibase/bbui"
import TableDataImport from "../TableDataImport.svelte"
import TableIntegrationMenu from "../TableIntegrationMenu/index.svelte"
import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates"
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
@ -22,6 +21,7 @@
let dataImport
let integration
let error = ""
let externalDataSource = false
function checkValid(evt) {
const tableName = evt.target.value
@ -36,8 +36,7 @@
let newTable = {
name,
schema: dataImport.schema || {},
dataImport,
integration
dataImport
}
// Only set primary display if defined
@ -87,11 +86,6 @@
bind:value={name}
{error} />
<div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />
</div>
<div>
<Label grey extraSmall>Create Integrated Table from External Source</Label>
<TableIntegrationMenu bind:integration />
</div>
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
<TableDataImport bind:dataImport />
</ModalContent>

View file

@ -3,7 +3,6 @@
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import IntegrationConfigForm from "../TableIntegrationMenu//IntegrationConfigForm.svelte"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
export let table
@ -79,9 +78,6 @@
bind:value={table.name}
on:input={checkValid}
{error} />
{#if table.integration?.type}
<IntegrationConfigForm integration={table.integration} />
{/if}
<footer>
<Button secondary on:click={hideEditor}>Cancel</Button>
<Button primary disabled={error} on:click={save}>Save</Button>

View file

@ -5,9 +5,9 @@
</script>
{#if hasErrors}
<div class="container bb__alert bb__alert--danger">
<div class="container">
{#each errors as error}
<div class="error">{error.dataPath} {error.message}</div>
<div class="error">{error.dataPath || ''} {error.message}</div>
{/each}
</div>
{/if}
@ -17,6 +17,8 @@
border-radius: var(--border-radius-m);
margin: 0;
padding: var(--spacing-m);
background-color: rgba(241, 165, 165, 0.2);
color: var(--red);
}
.error {

View file

@ -61,7 +61,7 @@
}
.nav-item:hover,
.nav-item.selected {
border-radius: var(--border-radius-m);
border-radius: var(--border-radius-s);
}
.content {

View file

@ -0,0 +1,14 @@
<script>
import { TextArea } from "@budibase/bbui"
const QueryTypes = {
SQL: "sql",
}
export let queryType
export let query
</script>
{#if queryType === QueryTypes.SQL}
<TextArea thin label="Query" bind:value={query} />
{/if}

View file

@ -4,7 +4,7 @@
import api from "builderStore/api"
async function updateApplication(data) {
const response = await api.put(`/api/${$store.appId}`, data)
const response = await api.put(`/api/applications/${$store.appId}`, data)
const app = await response.json()
store.update(state => {
state = {

View file

@ -84,6 +84,7 @@
overflow: hidden;
margin: auto;
height: 100%;
background-color: white;
}
.component-container iframe {
border: 0;

View file

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
import { store, currentAsset } from "builderStore"
import { store, currentAssetId } from "builderStore"
import { getComponentDefinition } from "builderStore/storeUtils"
import { DropEffect, DropPosition } from "./dragDropStore"
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
@ -22,7 +22,7 @@
const path = store.actions.components.findRoute(component)
// Go to correct URL
$goto(`./${$store.currentAssetId}/${path}`)
$goto(`./${$currentAssetId}/${path}`)
}
const dragstart = component => e => {
@ -71,7 +71,7 @@
on:drop={dragDropStore.actions.drop}
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
withArrow
indentLevel={level + 3}
indentLevel={level + 1}
selected={$store.selectedComponentId === component._id}>
<ComponentDropdownMenu {component} />
</NavItem>

View file

@ -1,5 +1,4 @@
<script>
import { writable } from "svelte/store"
import { goto } from "@sveltech/routify"
import { store, selectedComponent, currentAsset } from "builderStore"
import instantiateStore from "./dragDropStore"
@ -20,6 +19,7 @@
export let route
export let path
export let indent
export let border
$: selectedScreen = $currentAsset
@ -34,6 +34,7 @@
icon="ri-folder-line"
text={path}
opened={true}
{border}
withArrow={route.subpaths} />
{#each Object.entries(route.subpaths) as [url, subpath]}
@ -41,8 +42,8 @@
<NavItem
icon="ri-artboard-2-line"
indentLevel={indent || 1}
selected={$store.currentAssetId === screenId}
opened={$store.currentAssetId === screenId}
selected={$store.selectedScreenId === screenId}
opened={$store.selectedScreenId === screenId}
text={ROUTE_NAME_MAP[url]?.[role] || url}
withArrow={route.subpaths}
on:click={() => changeScreen(screenId)}>
@ -50,6 +51,7 @@
</NavItem>
{#if selectedScreen?._id === screenId}
<ComponentTree
level={1}
components={selectedScreen.props._children}
currentComponent={$selectedComponent}
{dragDropStore} />

View file

@ -1,11 +1,76 @@
<script>
import { goto } from "@sveltech/routify"
import { store } from "builderStore"
import { store, selectedAccessRole } from "builderStore"
import PathTree from "./PathTree.svelte"
let routes = {}
$: paths = Object.keys(routes || {}).sort()
$: {
const allRoutes = $store.routes
const sortedPaths = Object.keys(allRoutes || {}).sort()
const selectedRoleId = $selectedAccessRole
const selectedScreenId = $store.selectedScreenId
let found = false
let firstValidScreenId
let filteredRoutes = {}
let screenRoleId
// Filter all routes down to only those which match the current role
sortedPaths.forEach(path => {
const config = allRoutes[path]
Object.entries(config.subpaths).forEach(([subpath, pathConfig]) => {
Object.entries(pathConfig.screens).forEach(([roleId, screenId]) => {
if (screenId === selectedScreenId) {
screenRoleId = roleId
found = roleId === selectedRoleId
}
if (roleId === selectedRoleId) {
if (!firstValidScreenId) {
firstValidScreenId = screenId
}
if (!filteredRoutes[path]) {
filteredRoutes[path] = { subpaths: {} }
}
filteredRoutes[path].subpaths[subpath] = {
screens: {
[selectedRoleId]: screenId,
},
}
}
})
})
})
routes = filteredRoutes
// Select the correct role for the current screen ID
if (!found && screenRoleId) {
selectedAccessRole.set(screenRoleId)
}
// If the selected screen isn't in this filtered list, select the first one
else if (!found && firstValidScreenId) {
store.actions.screens.select(firstValidScreenId)
}
}
</script>
<div class="root">
{#each Object.keys($store.routes || {}) as path}
<PathTree {path} route={$store.routes[path]} />
{#each paths as path, idx}
<PathTree border={idx > 0} {path} route={routes[path]} />
{/each}
{#if !paths.length}
<div class="empty">
There aren't any screens configured with this access role.
</div>
{/if}
</div>
<style>
div.empty {
font-size: var(--font-size-xs);
color: var(--grey-5);
padding-top: var(--spacing-xs);
}
</style>

View file

@ -6,6 +6,7 @@
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
import SettingsView from "./SettingsView.svelte"
import { setWith } from "lodash"
let flattenedPanel = flattenComponents(panelStructure.categories)
let categories = [
@ -69,7 +70,7 @@
) {
selectedAsset.props._instanceName = value
} else {
selectedAsset[name] = value
setWith(selectedAsset, name.split("."), value, Object)
}
return state
})

View file

@ -1,6 +1,11 @@
<script>
import { goto, url } from "@sveltech/routify"
import { store, currentAssetName, selectedComponent } from "builderStore"
import { goto } from "@sveltech/routify"
import {
store,
currentAssetName,
selectedComponent,
currentAssetId,
} from "builderStore"
import components from "./temporaryPanelStructure.js"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
@ -27,7 +32,7 @@
const onComponentChosen = component => {
store.actions.components.create(component._component, component.presetProps)
const path = store.actions.components.findRoute($selectedComponent)
$goto(`./${$store.currentAssetId}/${path}`)
$goto(`./${$currentAssetId}/${path}`)
close()
}
</script>

View file

@ -1,13 +1,19 @@
<script>
import { onMount } from "svelte"
import { goto, params, url } from "@sveltech/routify"
import { store, currentAsset, selectedComponent } from "builderStore"
import {
store,
allScreens,
currentAsset,
backendUiStore,
selectedAccessRole,
} from "builderStore"
import { FrontendTypes } from "constants"
import ComponentNavigationTree from "components/userInterface/ComponentNavigationTree/index.svelte"
import Layout from "components/userInterface/Layout.svelte"
import NewScreenModal from "components/userInterface/NewScreenModal.svelte"
import NewLayoutModal from "components/userInterface/NewLayoutModal.svelte"
import { Modal, Switcher } from "@budibase/bbui"
import { Modal, Switcher, Select } from "@budibase/bbui"
const tabs = [
{
@ -24,11 +30,38 @@
let routes = {}
let tab = $params.assetType
function navigate({ detail }) {
if (!detail) return
const navigate = ({ detail }) => {
if (!detail) {
return
}
$goto(`../${detail.heading.key}`)
}
const updateAccessRole = event => {
const role = event.target.value
// Select a valid screen with this new role - otherwise we'll not be
// able to change role at all because ComponentNavigationTree will kick us
// back the current role again because the same screen ID is still selected
const firstValidScreenId = $allScreens.find(
screen => screen.routing.roleId === role
)?._id
if (firstValidScreenId) {
store.actions.screens.select(firstValidScreenId)
}
// Otherwise clear the selected screen ID so that the first new valid screen
// can be selected by ComponentNavigationTree
else {
store.update(state => {
state.selectedScreenId = null
return state
})
}
selectedAccessRole.set(role)
}
onMount(() => {
store.actions.routing.fetch()
})
@ -41,11 +74,21 @@
on:click={modal.show}
data-cy="new-screen"
class="ri-add-circle-fill" />
{#if $currentAsset}
<div class="nav-items-container">
<ComponentNavigationTree />
</div>
{/if}
<div class="role-select">
<Select
extraThin
secondary
on:change={updateAccessRole}
value={$selectedAccessRole}
label="Filter by Access">
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
</div>
<div class="nav-items-container">
<ComponentNavigationTree />
</div>
<Modal bind:this={modal}>
<NewScreenModal />
</Modal>
@ -54,8 +97,8 @@
on:click={modal.show}
data-cy="new-layout"
class="ri-add-circle-fill" />
{#each $store.layouts as layout (layout._id)}
<Layout {layout} />
{#each $store.layouts as layout, idx (layout._id)}
<Layout {layout} border={idx > 0} />
{/each}
<Modal bind:this={modal}>
<NewLayoutModal />
@ -82,4 +125,8 @@
cursor: pointer;
color: var(--blue);
}
.role-select {
margin-bottom: var(--spacing-m);
}
</style>

View file

@ -10,6 +10,7 @@
import { writable } from "svelte/store"
export let layout
export let border
let confirmDeleteDialog
let componentToDelete = ""
@ -23,17 +24,17 @@
</script>
<NavItem
border={false}
{border}
icon="ri-layout-3-line"
text={layout.name}
withArrow
selected={$store.currentAssetId === layout._id}
opened={$store.currentAssetId === layout._id}
selected={$store.selectedLayoutId === layout._id}
opened={$store.selectedLayoutId === layout._id}
on:click={selectLayout}>
<LayoutDropdownMenu {layout} />
</NavItem>
{#if $store.currentAssetId === layout._id && layout.props?._children}
{#if $store.selectedLayoutId === layout._id && layout.props?._children}
<ComponentTree
components={layout.props._children}
currentComponent={$selectedComponent}

View file

@ -1,19 +1,15 @@
<script>
import { goto } from "@sveltech/routify"
import api from "builderStore/api"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore, allScreens } from "builderStore"
import { store } from "builderStore"
import { Input, ModalContent } from "@budibase/bbui"
import analytics from "analytics"
const CONTAINER = "@budibase/standard-components/container"
let name = ""
async function save() {
try {
await store.actions.layouts.save({ name })
$goto(`./${$store.currentAssetId}`)
const layout = await store.actions.layouts.save({ name })
$goto(`./${layout._id}`)
notifier.success(`Layout ${name} created successfully`)
} catch (err) {
notifier.danger(`Error creating layout ${name}.`)

View file

@ -1,17 +1,11 @@
<script>
import { goto } from "@sveltech/routify"
import { store, backendUiStore, allScreens } from "builderStore"
import {
Input,
Button,
Spacer,
Select,
ModalContent,
Toggle,
} from "@budibase/bbui"
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui"
import getTemplates from "builderStore/store/screenTemplates"
import { some } from "lodash/fp"
import analytics from "analytics"
import { onMount } from "svelte"
import api from "builderStore/api"
const CONTAINER = "@budibase/standard-components/container"
@ -21,15 +15,13 @@
let templateIndex
let draftScreen
let createLink = true
let roleId = "BASIC"
$: templates = getTemplates($store, $backendUiStore.tables)
$: route = !route && $allScreens.length === 0 ? "*" : route
$: baseComponents = Object.values($store.components)
.filter(componentDefinition => componentDefinition.baseComponent)
.map(c => c._component)
$: {
if (templates && templateIndex === undefined) {
templateIndex = 0
@ -56,10 +48,10 @@
const save = async () => {
if (!route) {
routeError = "Url is required"
routeError = "URL is required"
} else {
if (routeNameExists(route)) {
routeError = "This url is already taken"
if (routeExists(route, roleId)) {
routeError = "This URL is already taken for this access role"
} else {
routeError = ""
}
@ -69,8 +61,7 @@
draftScreen.props._instanceName = name
draftScreen.props._component = baseComponent
// TODO: need to fix this up correctly
draftScreen.routing = { route, roleId: "ADMIN" }
draftScreen.routing = { route, roleId }
const createdScreen = await store.actions.screens.create(draftScreen)
if (createLink) {
@ -85,12 +76,14 @@
})
}
$goto(`./screen/${createdScreen._id}`)
$goto(`./${createdScreen._id}`)
}
const routeNameExists = route => {
const routeExists = (route, roleId) => {
return $allScreens.some(
screen => screen.routing.route.toLowerCase() === route.toLowerCase()
screen =>
screen.routing.route.toLowerCase() === route.toLowerCase() &&
screen.routing.roleId === roleId
)
}
@ -113,14 +106,16 @@
{/each}
{/if}
</Select>
<Input label="Name" bind:value={name} />
<Input
label="Url"
error={routeError}
bind:value={route}
on:change={routeChanged} />
<Select label="Access" bind:value={roleId} secondary>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>
<Toggle text="Create link in navigation bar" bind:checked={createLink} />
</ModalContent>

View file

@ -0,0 +1,15 @@
<script>
import { Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
export let value
let roles = []
</script>
<Select bind:value extraThin secondary on:change>
<option value="">Choose an option</option>
{#each $backendUiStore.roles as role}
<option value={role._id}>{role.name}</option>
{/each}
</Select>

View file

@ -1,8 +1,10 @@
<script>
import { get } from "lodash"
import { isEmpty } from "lodash/fp"
import { FrontendTypes } from "constants"
import PropertyControl from "./PropertyControl.svelte"
import LayoutSelect from "./LayoutSelect.svelte"
import RoleSelect from "./RoleSelect.svelte"
import Input from "./PropertyPanelControls/Input.svelte"
import { excludeProps } from "./propertyCategories.js"
import { store, allScreens, currentAsset } from "builderStore"
@ -16,7 +18,13 @@
export let displayNameField = false
export let assetInstance
let assetProps = ["title", "description", "route", "layoutId"]
let assetProps = [
"title",
"description",
"routing.route",
"layoutId",
"routing.roleId",
]
let duplicateName = false
const propExistsOnComponentDef = prop =>
@ -28,7 +36,8 @@
const screenDefinition = [
{ key: "description", label: "Description", control: Input },
{ key: "route", label: "Route", control: Input },
{ key: "routing.route", label: "Route", control: Input },
{ key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
@ -92,7 +101,7 @@
control={def.control}
label={def.label}
key={def.key}
value={assetInstance[def.key]}
value={get(assetInstance, def.key)}
onChange={onScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}

View file

@ -31,6 +31,16 @@
return [...acc, ...viewsArr]
}, [])
$: queries = $backendUiStore.datasources.reduce((acc, cur) => {
let queriesArr = Object.entries(cur.queries).map(([key, value]) => ({
label: value.name,
name: value.name,
...value,
type: "query",
}))
return [...acc, ...queriesArr]
}, [])
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.selectedComponentId,
components: $store.components,
@ -56,7 +66,7 @@
class="dropdownbutton"
bind:this={anchorRight}
on:click={dropdownRight.show}>
<span>{value.label ? value.label : 'Table / View'}</span>
<span>{value.label ? value.label : 'Table / View / Query'}</span>
<Icon name="arrowdown" />
</div>
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
@ -99,6 +109,20 @@
</li>
{/each}
</ul>
<hr />
<div class="title">
<Heading extraSmall>Queries</Heading>
</div>
<ul>
{#each queries as query}
<li
class:selected={value === query}
on:click={() => handleSelected(query)}>
{query.label}
</li>
{/each}
</ul>
</div>
</DropdownMenu>

View file

@ -20,6 +20,7 @@
backendUiStore.actions.reset()
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
await backendUiStore.actions.roles.fetch()
return pkg
} else {
throw new Error(pkg)
@ -217,5 +218,6 @@
position: absolute;
bottom: var(--spacing-m);
left: var(--spacing-m);
z-index: 1;
}
</style>

View file

@ -1,11 +1,32 @@
<script>
import { Switcher } from "@budibase/bbui"
import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
import DatasourceNavigator from "components/backend/DatasourceNavigator/DatasourceNavigator.svelte"
const tabs = [
{
title: "Tables",
key: "table",
},
{
title: "Data Sources",
key: "datasource",
},
]
let tab = "table"
</script>
<!-- routify:options index=0 -->
<div class="root">
<div class="nav">
<TableNavigator />
<Switcher headings={tabs} bind:value={tab}>
{#if tab === 'table'}
<TableNavigator />
{:else if tab === 'datasource'}
<DatasourceNavigator />
{/if}
</Switcher>
</div>
<div class="content">
<slot />

View file

@ -0,0 +1,16 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
$: datasourceId = $params.selectedDatasource
// TODO: refactor
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === $params.selectedDatasource
)
$: query = datasource.queries[$params.query]
</script>
{#if $backendUiStore.selectedDatabase._id && query}
<ExternalDataSourceTable {query} {datasourceId} />
{/if}

View file

@ -0,0 +1,15 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
if ($params.selectedDatasourceId) {
const datasource = $backendUiStore.datasources.find(
m => m._id === $params.selectedDatasource
)
if (datasource) {
backendUiStore.actions.datasources.select(datasource)
}
}
</script>
<slot />

View file

@ -0,0 +1,43 @@
<script>
import { Button, Spacer } from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import CreateQueryButton from "components/backend/DataTable/buttons/CreateQueryButton.svelte"
$: datasource = $backendUiStore.datasources.find(
ds => ds._id === $backendUiStore.selectedDatasourceId
)
async function saveDatasource() {
// Create datasource
await backendUiStore.actions.datasources.save(datasource)
notifier.success(`Datasource ${name} saved successfully.`)
}
</script>
{#if datasource}
<CreateQueryButton {datasource} />
<section>
<h4>{datasource.name}: Configuration</h4>
<IntegrationConfigForm integration={datasource.config} />
<Spacer medium />
<footer>
<Button primary wide disabled={false} on:click={saveDatasource}>
Save
</Button>
</footer>
</section>
{/if}
<style>
h4 {
margin-top: var(--spacing-xl);
margin-bottom: var(--spacing-s);
}
section {
background: white;
border-radius: var(--border-radius-m);
padding: var(--spacing-xl);
}
</style>

View file

@ -0,0 +1,18 @@
<script>
import { backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
onMount(async () => {
// navigate to first datasource in list, if not already selected
if (
!$leftover &&
$backendUiStore.datasources.length > 0 &&
!$backendUiStore.selectedDatasourceId
) {
$goto(`./${$backendUiStore.datasources[0]._id}`)
}
})
</script>
<slot />

View file

@ -1,17 +1,12 @@
<script>
import TableDataTable from "components/backend/DataTable/DataTable.svelte"
import ExternalDataSourceTable from "components/backend/DataTable/ExternalDataSourceTable.svelte"
import { backendUiStore } from "builderStore"
$: selectedTable = $backendUiStore.selectedTable
</script>
{#if $backendUiStore.selectedDatabase._id && selectedTable.name}
{#if selectedTable.integration?.type}
<ExternalDataSourceTable />
{:else}
<TableDataTable />
{/if}
<TableDataTable />
{:else}
<i>Create your first table to start building</i>
{/if}

View file

@ -26,11 +26,12 @@
// There are leftover stuff, like IDs, so navigate the components and find the ID and select it.
if ($leftover) {
// Get the correct screen children.
const assetChildren = assetList.find(
asset =>
asset._id === $params.asset ||
asset._id === decodeURIComponent($params.asset)
).props._children
const assetChildren =
assetList.find(
asset =>
asset._id === $params.asset ||
asset._id === decodeURIComponent($params.asset)
)?.props._children ?? []
findComponent(componentIds, assetChildren)
}
// }

View file

@ -1,11 +1,10 @@
<script>
import { store, backendUiStore } from "builderStore"
import { store, backendUiStore, currentAsset } from "builderStore"
import { onMount } from "svelte"
import { FrontendTypes } from "constants"
import CurrentItemPreview from "components/userInterface/AppPreview"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import { last } from "lodash/fp"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
$: instance = $store.appInstance
@ -36,7 +35,7 @@
</div>
<div class="preview-pane">
{#if $store.currentAssetId && $store.currentAssetId.length > 0}
{#if $currentAsset}
<ComponentSelectionList />
<div class="preview-content">
<CurrentItemPreview />

View file

@ -5,12 +5,32 @@
// Go to first layout
if ($params.assetType === FrontendTypes.LAYOUT) {
$goto(`../${$store.layouts[0]?._id}`)
// Try to use previously selected layout first
let id
if (
$store.selectedLayoutId &&
$store.layouts.find(layout => layout._id === $store.selectedLayoutId)
) {
id = $store.selectedLayoutId
} else {
id = $store.layouts[0]?._id
}
$goto(`../${id}`)
}
// Go to first screen
if ($params.assetType === FrontendTypes.SCREEN) {
$goto(`../${$allScreens[0]?._id}`)
// Try to use previously selected layout first
let id
if (
$store.selectedScreenId &&
$allScreens.find(screen => screen._id === $store.selectedScreenId)
) {
id = $store.selectedScreenId
} else {
id = $allScreens[0]?._id
}
$goto(`../${id}`)
}
</script>

View file

@ -854,6 +854,16 @@
svelte-portal "^1.0.0"
turndown "^7.0.0"
"@budibase/client@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.4.2.tgz#cb146681377f96ca907234606cdfa9f66db2139e"
integrity sha512-3KjkSMFc8mYMw48oYhfszJHgG03P9XS8+bRlAsPtT0m5RP8GF7jxWNDDrpl80pbi1NA1D+QmMo5SjLeCAO1Y+Q==
dependencies:
deep-equal "^2.0.1"
mustache "^4.0.1"
regexparam "^1.3.0"
svelte-spa-router "^3.0.5"
"@budibase/colorpicker@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
@ -1646,6 +1656,11 @@ array-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@ -1713,6 +1728,13 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies:
array-filter "^1.0.0"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -2876,6 +2898,27 @@ deep-equal@^1.0.1:
object-keys "^1.1.1"
regexp.prototype.flags "^1.2.0"
deep-equal@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9"
integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==
dependencies:
call-bind "^1.0.0"
es-get-iterator "^1.1.1"
get-intrinsic "^1.0.1"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.1.1"
isarray "^2.0.5"
object-is "^1.1.4"
object-keys "^1.1.1"
object.assign "^4.1.2"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.3"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@ -3084,7 +3127,7 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2:
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-abstract@^1.18.0-next.1:
es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1:
version "1.18.0-next.1"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68"
integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==
@ -3102,6 +3145,20 @@ es-abstract@^1.18.0-next.1:
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-get-iterator@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.1.tgz#b93ddd867af16d5118e00881396533c1c6647ad9"
integrity sha512-qorBw8Y7B15DVLaJWy6WdEV/ZkieBcu6QCq/xzWzGOKJqgG1j754vXRfZ3NY7HSShneqU43mPB4OkQBTkvHhFw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.1"
has-symbols "^1.0.1"
is-arguments "^1.0.4"
is-map "^2.0.1"
is-set "^2.0.1"
is-string "^1.0.5"
isarray "^2.0.5"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@ -3485,7 +3542,7 @@ for-in@^1.0.2:
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
foreach@~2.0.1:
foreach@^2.0.5, foreach@~2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
@ -3575,7 +3632,7 @@ get-caller-file@^2.0.1:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.0.0:
get-intrinsic@^1.0.0, get-intrinsic@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.1.tgz#94a9768fcbdd0595a1c9273aacf4c89d075631be"
integrity sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==
@ -3951,6 +4008,11 @@ is-arrayish@^0.2.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
is-bigint@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2"
integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@ -3958,6 +4020,13 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0"
integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==
dependencies:
call-bind "^1.0.0"
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@ -3996,7 +4065,7 @@ is-data-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-date-object@^1.0.1:
is-date-object@^1.0.1, is-date-object@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
@ -4073,6 +4142,11 @@ is-installed-globally@^0.3.2:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-map@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@ -4083,6 +4157,11 @@ is-negative-zero@^2.0.0:
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
is-number-object@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -4148,6 +4227,11 @@ is-regex@^1.0.4, is-regex@^1.1.1:
dependencies:
has-symbols "^1.0.1"
is-set@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@ -4158,18 +4242,44 @@ is-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
is-symbol@^1.0.2:
is-string@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-symbol@^1.0.2, is-symbol@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
dependencies:
has-symbols "^1.0.1"
is-typed-array@^1.1.3:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.4.tgz#1f66f34a283a3c94a4335434661ca53fff801120"
integrity sha512-ILaRgn4zaSrVNXNGtON6iFNotXW3hAPF3+0fB1usg2jFlWqo5fEDdmJkz0zBfoi7Dgskr8Khi2xZ8cXqZEfXNA==
dependencies:
available-typed-arrays "^1.0.2"
call-bind "^1.0.0"
es-abstract "^1.18.0-next.1"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -4195,6 +4305,11 @@ isarray@1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isbuffer@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b"
@ -5492,6 +5607,14 @@ object-is@^1.0.1:
define-properties "^1.1.3"
es-abstract "^1.18.0-next.1"
object-is@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068"
integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==
dependencies:
call-bind "^1.0.0"
define-properties "^1.1.3"
object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@ -5518,7 +5641,7 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
object.assign@^4.1.0, object.assign@^4.1.1:
object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@ -6098,7 +6221,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp.prototype.flags@^1.2.0:
regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
@ -6106,6 +6229,11 @@ regexp.prototype.flags@^1.2.0:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
regexparam@1.3.0, regexparam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
regexpu-core@^4.7.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6"
@ -6590,6 +6718,14 @@ shortid@^2.2.15:
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
dependencies:
es-abstract "^1.18.0-next.0"
object-inspect "^1.8.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -7006,6 +7142,13 @@ svelte-portal@^1.0.0:
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
svelte-spa-router@^3.0.5:
version "3.1.0"
resolved "https://registry.yarnpkg.com/svelte-spa-router/-/svelte-spa-router-3.1.0.tgz#a929f0def7e12c41f32bc356f91685aeadcd75bf"
integrity sha512-jlM/xwjn57mylr+pzHYCOOy+IPQauT46gOucNGTBu6jHcFXu3F+oaojN4PXC1LYizRGxFB6QA0qnYbZnRfX7Sg==
dependencies:
regexparam "1.3.0"
svelte@^3.30.0:
version "3.30.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.30.0.tgz#cbde341e96bf34f4ac73c8f14f8a014e03bfb7d6"
@ -7435,11 +7578,45 @@ whatwg-url@^8.0.0:
tr46 "^2.0.2"
webidl-conversions "^6.1.0"
which-boxed-primitive@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
dependencies:
is-bigint "^1.0.1"
is-boolean-object "^1.1.0"
is-number-object "^1.0.4"
is-string "^1.0.5"
is-symbol "^1.0.3"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-typed-array@^1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff"
integrity sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==
dependencies:
available-typed-arrays "^1.0.2"
call-bind "^1.0.0"
es-abstract "^1.18.0-next.1"
foreach "^2.0.5"
function-bind "^1.1.1"
has-symbols "^1.0.1"
is-typed-array "^1.1.3"
which@^1.2.9, which@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"

View file

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.3.8",
"version": "0.4.2",
"license": "MPL-2.0",
"main": "dist/budibase-client.js",
"module": "dist/budibase-client.js",
@ -15,7 +15,7 @@
"svelte-spa-router": "^3.0.5"
},
"devDependencies": {
"@budibase/standard-components": "^0.3.8",
"@budibase/standard-components": "^0.4.2",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"fs-extra": "^8.1.0",
@ -23,8 +23,8 @@
"rollup": "^2.33.2",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-svelte": "^6.1.1",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-svelte": "^6.1.1",
"rollup-plugin-terser": "^4.0.4",
"svelte": "^3.30.0",
"svelte-jester": "^1.0.6"

View file

@ -2,6 +2,7 @@ import { fetchTableData } from "./tables"
import { fetchViewData } from "./views"
import { fetchRelationshipData } from "./relationships"
import { enrichRows } from "./rows"
import { fetchDataForQuery } from "../../../builder/src/components/backend/DataTable/api"
/**
* Fetches all rows for a particular Budibase data source.
@ -18,6 +19,8 @@ export const fetchDatasource = async (datasource, dataContext) => {
rows = await fetchTableData(tableId)
} else if (type === "view") {
rows = await fetchViewData(datasource)
} else if (type === "query") {
rows = await fetchDataForQuery(datasource)
} else if (type === "link") {
const row = dataContext[datasource.providerId]
rows = await fetchRelationshipData({
@ -26,7 +29,6 @@ export const fetchDatasource = async (datasource, dataContext) => {
fieldName,
})
}
// Enrich rows
return await enrichRows(rows, tableId)
}

View file

@ -3,7 +3,7 @@
import { setContext, onMount } from "svelte"
import Component from "./Component.svelte"
import SDK from "../sdk"
import { createDataStore, routeStore, screenStore } from "../store"
import { createDataStore, initialise, screenStore } from "../store"
// Provide contexts
setContext("sdk", SDK)
@ -14,13 +14,11 @@
// Load app config
onMount(async () => {
await routeStore.actions.fetchRoutes()
await screenStore.actions.fetchScreens()
await initialise()
loaded = true
})
</script>
{#if loaded && $screenStore.activeLayout}
<!-- // TODO: need to get the active screen as well -->
<Component definition={$screenStore.activeLayout.props} />
{/if}

View file

@ -7,7 +7,15 @@
const { styleable } = getContext("sdk")
const component = getContext("component")
$: routerConfig = getRouterConfig($routeStore.routes)
// Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config
// changes
$: configs = [
{
routes: getRouterConfig($routeStore.routes),
id: $routeStore.routeSessionId,
},
]
const getRouterConfig = routes => {
let config = {}
@ -25,11 +33,11 @@
}
</script>
{#if routerConfig}
{#each configs as config (config.id)}
<div use:styleable={$component.styles}>
<Router on:routeLoading={onRouteLoading} routes={routerConfig} />
<Router on:routeLoading={onRouteLoading} routes={config.routes} />
</div>
{/if}
{/each}
<style>
div {

View file

@ -1,18 +1,29 @@
import * as API from "../api"
import { getAppId } from "../utils/getAppId"
import { writable } from "svelte/store"
import { initialise } from "./initialise"
import { routeStore } from "./routes"
const createAuthStore = () => {
const store = writable("")
const goToDefaultRoute = () => {
// Setting the active route forces an update of the active screen ID,
// even if we're on the same URL
routeStore.actions.setActiveRoute("/")
// Navigating updates the URL to reflect this route
routeStore.actions.navigate("/")
}
const logIn = async ({ email, password }) => {
const user = await API.logIn({ email, password })
if (!user.error) {
store.set(user.token)
location.reload()
await initialise()
goToDefaultRoute()
}
}
const logOut = () => {
const logOut = async () => {
store.set("")
const appId = getAppId()
if (appId) {
@ -20,7 +31,8 @@ const createAuthStore = () => {
window.document.cookie = `budibase:${appId}:${environment}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`
}
}
location.reload()
await initialise()
goToDefaultRoute()
}
return {

View file

@ -6,3 +6,6 @@ export { bindingStore } from "./binding"
// Data stores are layered and duplicated, so it is not a singleton
export { createDataStore, dataStore } from "./data"
// Initialises an app by loading screens and routes
export { initialise } from "./initialise"

View file

@ -0,0 +1,7 @@
import { routeStore } from "./routes"
import { screenStore } from "./screens"
export async function initialise() {
await routeStore.actions.fetchRoutes()
await screenStore.actions.fetchScreens()
}

View file

@ -7,6 +7,7 @@ const createRouteStore = () => {
routes: [],
routeParams: {},
activeRoute: null,
routeSessionId: Math.random(),
}
const store = writable(initialState)
@ -21,8 +22,15 @@ const createRouteStore = () => {
})
})
})
// Sort route by paths so that the router matches correctly
routes.sort((a, b) => {
return a.path > b.path ? -1 : 1
})
store.update(state => {
state.routes = routes
state.routeSessionId = Math.random()
return state
})
}

View file

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.3.8",
"version": "0.4.2",
"description": "Budibase Web Server",
"main": "src/electron.js",
"repository": {
@ -21,7 +21,6 @@
"maintainer": "Budibase",
"icon": "./build/icons/",
"target": [
"AppImage",
"deb"
],
"category": "Development"
@ -49,8 +48,8 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/client": "^0.3.8",
"@elastic/elasticsearch": "^7.10.0",
"@budibase/client": "^0.4.2",
"@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1",
"@sentry/node": "^5.19.2",

View file

@ -32,7 +32,6 @@ const {
} = require("../../constants/screens")
const { cloneDeep } = require("lodash/fp")
const { recurseMustache } = require("../../utilities/mustache")
const { generateAssetCss } = require("../../utilities/builder/generateCss")
const { USERS_TABLE_SCHEMA } = require("../../constants")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -131,12 +130,6 @@ exports.fetchAppPackage = async function(ctx) {
const application = await db.get(ctx.params.appId)
const [layouts, screens] = await Promise.all([getLayouts(db), getScreens(db)])
for (let layout of layouts) {
layout._css = generateAssetCss([layout.props])
}
for (let screen of screens) {
screen._css = generateAssetCss([screen.props])
}
ctx.body = {
application,
screens,
@ -230,10 +223,6 @@ const createEmptyAppPackage = async (ctx, app) => {
screensAndLayouts.push(loginScreen)
await db.bulkDocs(screensAndLayouts)
// at the end add CSS to all the structures
for (let asset of screensAndLayouts) {
asset._css = generateAssetCss([asset.props])
}
await compileStaticAssets(app._id, screensAndLayouts)
await compileStaticAssets(app._id)
return newAppFolder
}

View file

@ -34,7 +34,6 @@ exports.authenticate = async ctx => {
userId: dbUser._id,
roleId: dbUser.roleId,
version: app.version,
permissions: dbUser.permissions || [],
}
// if in cloud add the user api key
if (env.CLOUD) {

View file

@ -0,0 +1,133 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const {
generateDatasourceID,
getDatasourceParams,
generateQueryID,
} = require("../../db/utils")
const { integrations } = require("../../integrations")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const datasources = (
await database.allDocs(
getDatasourceParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
ctx.body = datasources
}
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
// TODO: validate the config against the integration type
// if (!somethingIsntValid) {
// // ctx.throw(400, "email and Password Required.")
// }
const datasource = {
_id: generateDatasourceID(),
type: "datasource",
queries: {},
...ctx.request.body,
}
try {
const response = await db.post(datasource)
datasource._rev = response.rev
ctx.status = 200
ctx.message = "Datasource created successfully."
ctx.body = datasource
} catch (err) {
ctx.throw(err.status, err)
}
}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const user = ctx.request.body
const dbUser = await db.get(ctx.request.body._id)
if (user.password) {
user.password = await bcrypt.hash(user.password)
} else {
delete user.password
}
const newData = { ...dbUser, ...user }
const response = await db.put(newData)
user._rev = response.rev
ctx.status = 200
ctx.message = `User ${ctx.request.body.email} updated successfully.`
ctx.body = response
}
exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
await database.destroy(ctx.params.datasourceId)
ctx.message = `Datasource deleted.`
ctx.status = 200
}
exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const datasource = await database.get(ctx.params.datasourceId)
ctx.body = datasource
}
exports.saveQuery = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const query = ctx.request.body
//
// {
// type: "",
// query: "",
// otherStuff: ""
// }
const datasource = await db.get(ctx.params.datasourceId)
const queryId = generateQueryID()
datasource.queries[queryId] = query
const response = await db.put(datasource)
datasource._rev = response.rev
ctx.body = datasource
ctx.message = `Query ${query.name} saved successfully.`
}
exports.previewQuery = async function(ctx) {
const { type, config, query } = ctx.request.body
const Integration = integrations[type]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
ctx.body = await new Integration(config, query).query()
}
exports.executeQuery = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const datasource = await db.get(ctx.params.datasourceId)
const query = datasource.queries[ctx.params.queryId]
const Integration = integrations[datasource.source]
if (!Integration) {
ctx.throw(400, "Integration type does not exist.")
return
}
ctx.body = await new Integration(datasource.config, query.queryString).query()
}

View file

@ -7,8 +7,6 @@ const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
const PouchDB = require("../../../db")
const env = require("../../../environment")
const EXCLUDED_DIRECTORIES = ["css"]
/**
* Finalises the deployment, updating the quota for the user API key
* The verification process returns the levels to update to.
@ -140,15 +138,9 @@ exports.uploadAppAssets = async function({ appId, bucket, accountId }) {
let uploads = []
// Upload HTML, CSS and JS of the web app
// Upload HTML and JS of the web app
walkDir(appAssetsPath, function(filePath) {
const filePathParts = filePath.split("/")
const publicIndex = filePathParts.indexOf("public")
const directory = filePathParts[publicIndex + 1]
// don't include these top level directories
if (EXCLUDED_DIRECTORIES.indexOf(directory) !== -1) {
return
}
const appAssetUpload = prepareUploadForS3({
file: {
path: filePath,

View file

@ -6,3 +6,8 @@ exports.fetch = async function(ctx) {
ctx.status = 200
ctx.body = definitions
}
exports.find = async function(ctx) {
ctx.status = 200
ctx.body = definitions[ctx.params.type]
}

View file

@ -0,0 +1,6 @@
const { BUILTIN_PERMISSIONS } = require("../../utilities/security/permissions")
exports.fetch = async function(ctx) {
// TODO: need to build out custom permissions
ctx.body = Object.values(BUILTIN_PERMISSIONS)
}

View file

@ -0,0 +1,39 @@
// const CouchDB = require("../../../db")
// const { generateQueryID } = require("../../db/utils")
// const viewTemplate = require("./viewBuilder")
// exports.save = async ctx => {
// const db = new CouchDB(ctx.user.appId)
// const { datasourceId, query } = ctx.request.body
// const datasource = await db.get(datasourceId)
// const queryId = generateQueryID()
// datasource.queries[queryId] = query
// const response = await db.put(datasource)
// ctx.body = query
// ctx.message = `View ${viewToSave.name} saved successfully.`
// }
// exports.destroy = async ctx => {
// const db = new CouchDB(ctx.user.appId)
// const designDoc = await db.get("_design/database")
// const viewName = decodeURI(ctx.params.viewName)
// const view = designDoc.views[viewName]
// delete designDoc.views[viewName]
// await db.put(designDoc)
// const table = await db.get(view.meta.tableId)
// delete table.views[viewName]
// await db.put(table)
// ctx.body = view
// ctx.message = `View ${ctx.params.viewName} saved successfully.`
// }

View file

@ -4,7 +4,40 @@ const {
Role,
getRole,
} = require("../../utilities/security/roles")
const { generateRoleID, getRoleParams } = require("../../db/utils")
const {
generateRoleID,
getRoleParams,
getUserParams,
ViewNames,
} = require("../../db/utils")
const UpdateRolesOptions = {
CREATED: "created",
REMOVED: "removed",
}
async function updateRolesOnUserTable(db, roleId, updateOption) {
const table = await db.get(ViewNames.USERS)
const schema = table.schema
const remove = updateOption === UpdateRolesOptions.REMOVED
let updated = false
for (let prop of Object.keys(schema)) {
if (prop === "roleId") {
updated = true
const constraints = schema[prop].constraints
const indexOf = constraints.inclusion.indexOf(roleId)
if (remove && indexOf !== -1) {
constraints.inclusion.splice(indexOf, 1)
} else if (!remove && indexOf === -1) {
constraints.inclusion.push(roleId)
}
break
}
}
if (updated) {
await db.put(table)
}
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
@ -15,7 +48,13 @@ exports.fetch = async function(ctx) {
)
const customRoles = body.rows.map(row => row.doc)
const staticRoles = [BUILTIN_ROLES.ADMIN, BUILTIN_ROLES.POWER]
// exclude internal roles like builder
const staticRoles = [
BUILTIN_ROLES.ADMIN,
BUILTIN_ROLES.POWER,
BUILTIN_ROLES.BASIC,
BUILTIN_ROLES.PUBLIC,
]
ctx.body = [...staticRoles, ...customRoles]
}
@ -25,13 +64,18 @@ exports.find = async function(ctx) {
exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
let id = ctx.request.body._id || generateRoleID()
const role = new Role(id, ctx.request.body.name, ctx.request.body.inherits)
let { _id, name, inherits, permissionId } = ctx.request.body
if (!_id) {
_id = generateRoleID()
}
const role = new Role(_id, name)
.addPermission(permissionId)
.addInheritance(inherits)
if (ctx.request.body._rev) {
role._rev = ctx.request.body._rev
}
const result = await db.put(role)
await updateRolesOnUserTable(db, _id, UpdateRolesOptions.CREATED)
role._rev = result.rev
ctx.body = role
ctx.message = `Role '${role.name}' created successfully.`
@ -39,7 +83,26 @@ exports.save = async function(ctx) {
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
await db.remove(ctx.params.roleId, ctx.params.rev)
ctx.message = `Role ${ctx.params.id} deleted successfully`
const roleId = ctx.params.roleId
// first check no users actively attached to role
const users = (
await db.allDocs(
getUserParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
const usersWithRole = users.filter(user => user.roleId === roleId)
if (usersWithRole.length !== 0) {
ctx.throw("Cannot delete role when it is in use.")
}
await db.remove(roleId, ctx.params.rev)
await updateRolesOnUserTable(
db,
ctx.params.roleId,
UpdateRolesOptions.REMOVED
)
ctx.message = `Role ${ctx.params.roleId} deleted successfully`
ctx.status = 200
}

View file

@ -9,8 +9,8 @@ const {
ViewNames,
} = require("../../db/utils")
const usersController = require("./user")
const { cloneDeep } = require("lodash")
const { integrations } = require("../../integrations")
const { coerceRowValues } = require("../../utilities")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
@ -30,6 +30,28 @@ validateJs.extend(validateJs.validators.datetime, {
},
})
async function findRow(db, appId, tableId, rowId) {
let row
if (tableId === ViewNames.USERS) {
let ctx = {
params: {
userId: rowId,
},
user: {
appId,
},
}
await usersController.find(ctx)
row = ctx.body
} else {
row = await db.get(rowId)
}
if (row.tableId !== tableId) {
throw "Supplied tableId does not match the rows tableId"
}
return row
}
exports.patch = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
@ -65,6 +87,13 @@ exports.patch = async function(ctx) {
tableId: row.tableId,
table,
})
// Creation of a new user goes to the user controller
if (row.tableId === ViewNames.USERS) {
await usersController.update(ctx)
return
}
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
@ -81,19 +110,25 @@ exports.save = async function(ctx) {
let row = ctx.request.body
row.tableId = ctx.params.tableId
// TODO: find usage of this and break out into own endpoint
if (ctx.request.body.type === "delete") {
await bulkDelete(ctx)
ctx.body = ctx.request.body.rows
return
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
if (existingRow) {
ctx.params.id = row._id
await exports.patch(ctx)
return
}
if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId)
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
const table = await db.get(row.tableId)
row = coerceRowValues(row, table)
@ -122,39 +157,22 @@ exports.save = async function(ctx) {
})
// Creation of a new user goes to the user controller
if (!existingRow && row.tableId === ViewNames.USERS) {
try {
await usersController.create(ctx)
} catch (err) {
ctx.body = { errors: [err.message] }
}
return
}
if (existingRow) {
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} updated successfully.`
if (row.tableId === ViewNames.USERS) {
await usersController.create(ctx)
return
}
row.type = "row"
const response = await db.post(row)
const response = await db.put(row)
row._rev = response.rev
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} created successfully`
ctx.message = `${table.name} saved successfully`
}
exports.fetchView = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const { calculation, group, field } = ctx.query
const viewName = ctx.params.viewName
// if this is a table view being looked for just transfer to that
@ -164,6 +182,8 @@ exports.fetchView = async function(ctx) {
return
}
const db = new CouchDB(appId)
const { calculation, group, field } = ctx.query
const response = await db.query(`database/${viewName}`, {
include_docs: !calculation,
group,
@ -198,50 +218,33 @@ exports.fetchView = async function(ctx) {
exports.fetchTableRows = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const table = await db.get(ctx.params.tableId)
if (table.integration && table.integration.type) {
const Integration = integrations[table.integration.type]
ctx.body = await new Integration(table.integration).query()
return
// special case for users, fetch through the user controller
let rows
if (ctx.params.tableId === ViewNames.USERS) {
await usersController.fetch(ctx)
rows = ctx.body
} else {
const db = new CouchDB(appId)
const response = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
rows = response.rows.map(row => row.doc)
}
const response = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
ctx.body = response.rows.map(row => row.doc)
ctx.body = await linkRows.attachLinkInfo(
appId,
response.rows.map(row => row.doc)
)
}
exports.search = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const response = await db.allDocs({
include_docs: true,
...ctx.request.body,
})
ctx.body = await linkRows.attachLinkInfo(
appId,
response.rows.map(row => row.doc)
)
ctx.body = await linkRows.attachLinkInfo(appId, rows)
}
exports.find = async function(ctx) {
const appId = ctx.user.appId
const db = new CouchDB(appId)
const row = await db.get(ctx.params.rowId)
if (row.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId does not match the rows tableId")
return
try {
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId)
ctx.body = await linkRows.attachLinkInfo(appId, row)
} catch (err) {
ctx.throw(400, err)
}
ctx.body = await linkRows.attachLinkInfo(appId, row)
}
exports.destroy = async function(ctx) {
@ -307,7 +310,10 @@ exports.fetchEnrichedRow = async function(ctx) {
return
}
// need table to work out where links go in row
const [table, row] = await Promise.all([db.get(tableId), db.get(rowId)])
let [table, row] = await Promise.all([
db.get(tableId),
findRow(db, appId, tableId, rowId),
])
// get the link docs
const linkVals = await linkRows.getLinkDocuments({
appId,
@ -337,68 +343,6 @@ exports.fetchEnrichedRow = async function(ctx) {
ctx.status = 200
}
function coerceRowValues(record, table) {
const row = cloneDeep(record)
for (let [key, value] of Object.entries(row)) {
const field = table.schema[key]
if (!field) continue
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
row[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return row
}
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
longform: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}
async function bulkDelete(ctx) {
const appId = ctx.user.appId
const { rows } = ctx.request.body

View file

@ -1,8 +1,6 @@
const CouchDB = require("../../db")
const { getScreenParams, generateScreenID } = require("../../db/utils")
const { AccessController } = require("../../utilities/security/roles")
const { generateAssetCss } = require("../../utilities/builder/generateCss")
const compileStaticAssets = require("../../utilities/builder/compileStaticAssets")
exports.fetch = async ctx => {
const appId = ctx.user.appId
@ -32,10 +30,6 @@ exports.save = async ctx => {
}
const response = await db.put(screen)
// update CSS so client doesn't need to make a call directly after
screen._css = generateAssetCss([screen.props])
await compileStaticAssets(appId, screen)
ctx.message = `Screen ${screen.name} saved.`
ctx.body = {
...screen,

View file

@ -16,19 +16,10 @@ const CouchDB = require("../../../db")
const setBuilderToken = require("../../../utilities/builder/setBuilderToken")
const fileProcessor = require("../../../utilities/fileProcessor")
const env = require("../../../environment")
const { generateAssetCss } = require("../../../utilities/builder/generateCss")
const compileStaticAssets = require("../../../utilities/builder/compileStaticAssets")
// this was the version before we started versioning the component library
const COMP_LIB_BASE_APP_VERSION = "0.2.5"
exports.generateCss = async function(ctx) {
const structure = ctx.request.body
structure._css = generateAssetCss([structure.props])
await compileStaticAssets(ctx.appId, structure)
ctx.body = { css: structure._css }
}
exports.serveBuilder = async function(ctx) {
let builderPath = resolve(__dirname, "../../../../builder")
if (ctx.file === "index.html") {

View file

@ -22,13 +22,7 @@
<title>{title}</title>
<link rel="icon" type="image/png" href={favicon} />
<link rel="stylesheet" href={publicPath('bundle.css')} />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Roboto+Mono" />
<style>
html,
body {

View file

@ -1,40 +1,45 @@
const CouchDB = require("../../db")
const bcrypt = require("../../utilities/bcrypt")
const { generateUserID, getUserParams, ViewNames } = require("../../db/utils")
const { BUILTIN_ROLE_ID_ARRAY } = require("../../utilities/security/roles")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../utilities/security/permissions")
const { getRole } = require("../../utilities/security/roles")
exports.fetch = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const data = await database.allDocs(
getUserParams("", {
include_docs: true,
})
)
ctx.body = data.rows.map(row => row.doc)
const users = (
await database.allDocs(
getUserParams(null, {
include_docs: true,
})
)
).rows.map(row => row.doc)
// user hashed password shouldn't ever be returned
for (let user of users) {
delete user.password
}
ctx.body = users
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const { email, password, roleId, permissions } = ctx.request.body
const { email, password, roleId } = ctx.request.body
if (!email || !password) {
ctx.throw(400, "email and Password Required.")
}
const role = await checkRole(db, roleId)
const role = await getRole(ctx.user.appId, roleId)
if (!role) ctx.throw(400, "Invalid Role")
const hashedPassword = await bcrypt.hash(password)
const user = {
...ctx.request.body,
// these must all be after the object spread, make sure
// any values are overwritten, generateUserID will always
// generate the same ID for the user as it is not UUID based
_id: generateUserID(email),
email,
password: await bcrypt.hash(password),
type: "user",
roleId,
permissions: permissions || [BUILTIN_PERMISSION_NAMES.POWER],
password: hashedPassword,
tableId: ViewNames.USERS,
}
@ -59,7 +64,12 @@ exports.create = async function(ctx) {
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.appId)
const user = ctx.request.body
const dbUser = db.get(ctx.request.body._id)
const dbUser = await db.get(ctx.request.body._id)
if (user.password) {
user.password = await bcrypt.hash(user.password)
} else {
delete user.password
}
const newData = { ...dbUser, ...user }
const response = await db.put(newData)
@ -79,22 +89,12 @@ exports.destroy = async function(ctx) {
exports.find = async function(ctx) {
const database = new CouchDB(ctx.user.appId)
const user = await database.get(generateUserID(ctx.params.email))
ctx.body = {
email: user.email,
name: user.name,
_rev: user._rev,
let lookup = ctx.params.email
? generateUserID(ctx.params.email)
: ctx.params.userId
const user = await database.get(lookup)
if (user) {
delete user.password
}
}
const checkRole = async (db, roleId) => {
if (!roleId) return
if (BUILTIN_ROLE_ID_ARRAY.indexOf(roleId) !== -1) {
return {
_id: roleId,
name: roleId,
permissions: [],
}
}
return await db.get(roleId)
ctx.body = user
}

View file

@ -0,0 +1,41 @@
const Router = require("@koa/router")
const datasourceController = require("../controllers/datasource")
const authorized = require("../../middleware/authorized")
const {
BUILDER,
PermissionLevels,
PermissionTypes,
} = require("../../utilities/security/permissions")
const router = Router()
router
.get("/api/datasources", authorized(BUILDER), datasourceController.fetch)
.get(
"/api/datasources/:id",
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
datasourceController.find
)
.post("/api/datasources", authorized(BUILDER), datasourceController.save)
.post(
"/api/datasources/:datasourceId/queries",
authorized(BUILDER),
datasourceController.saveQuery
)
.post(
"/api/datasources/queries/preview",
authorized(BUILDER),
datasourceController.previewQuery
)
.get(
"/api/datasources/:datasourceId/queries/:queryId",
authorized(BUILDER),
datasourceController.executeQuery
)
.delete(
"/api/datasources/:datasourceId/:revId",
authorized(BUILDER),
datasourceController.destroy
)
module.exports = router

View file

@ -17,6 +17,9 @@ const templatesRoutes = require("./templates")
const analyticsRoutes = require("./analytics")
const routingRoutes = require("./routing")
const integrationRoutes = require("./integration")
const permissionRoutes = require("./permission")
const datasourceRoutes = require("./datasource")
// const queryRoutes = require("./query")
exports.mainRoutes = [
deployRoutes,
@ -34,6 +37,9 @@ exports.mainRoutes = [
webhookRoutes,
routingRoutes,
integrationRoutes,
permissionRoutes,
datasourceRoutes,
// queryRoutes,
// these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this
tableRoutes,

View file

@ -5,6 +5,8 @@ const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router.get("/api/integrations", authorized(BUILDER), controller.fetch)
router
.get("/api/integrations", authorized(BUILDER), controller.fetch)
.get("/api/integrations/:type", authorized(BUILDER), controller.find)
module.exports = router

View file

@ -0,0 +1,10 @@
const Router = require("@koa/router")
const controller = require("../controllers/permission")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const router = Router()
router.get("/api/permissions", authorized(BUILDER), controller.fetch)
module.exports = router

View file

@ -0,0 +1,28 @@
// const Router = require("@koa/router")
// const queryController = require("../controllers/query")
// const authorized = require("../../middleware/authorized")
// const { BUILDER } = require("../../utilities/security/permissions")
// const router = Router()
// // TODO: send down the datasource ID as well
// router
// // .get("/api/queries", authorized(BUILDER), queryController.fetch)
// // .get(
// // "/api/datasources/:datasourceId/queries/:id",
// // authorized(PermissionTypes.TABLE, PermissionLevels.READ),
// // queryController.find
// // )
// .post(
// "/api/datasources/:datasourceId/queries",
// authorized(BUILDER),
// queryController.save
// )
// .delete(
// "/api/datasources/:datasourceId/queries/:queryId/:revId",
// authorized(BUILDER),
// queryController.destroy
// )
// module.exports = router

View file

@ -2,11 +2,27 @@ const Router = require("@koa/router")
const controller = require("../controllers/role")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const Joi = require("joi")
const joiValidator = require("../../middleware/joi-validator")
const {
BUILTIN_PERMISSION_IDS,
} = require("../../utilities/security/permissions")
const router = Router()
function generateValidator() {
// prettier-ignore
return joiValidator.body(Joi.object({
_id: Joi.string().optional(),
_rev: Joi.string().optional(),
name: Joi.string().required(),
permissionId: Joi.string().valid(...Object.values(BUILTIN_PERMISSION_IDS)).required(),
inherits: Joi.string().optional(),
}).unknown(true))
}
router
.post("/api/roles", authorized(BUILDER), controller.save)
.post("/api/roles", authorized(BUILDER), generateValidator(), controller.save)
.get("/api/roles", authorized(BUILDER), controller.fetch)
.get("/api/roles/:roleId", authorized(BUILDER), controller.find)
.delete("/api/roles/:roleId/:rev", authorized(BUILDER), controller.destroy)

View file

@ -25,7 +25,6 @@ router
authorized(PermissionTypes.TABLE, PermissionLevels.READ),
rowController.find
)
.post("/api/rows/search", rowController.search)
.post(
"/api/:tableId/rows",
authorized(PermissionTypes.TABLE, PermissionLevels.WRITE),

View file

@ -10,7 +10,6 @@ const router = Router()
function generateSaveValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
_css: Joi.string().allow(""),
name: Joi.string().required(),
routing: Joi.object({
route: Joi.string().required(),

View file

@ -5,22 +5,6 @@ const env = require("../../environment")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/security/permissions")
const usage = require("../../middleware/usageQuota")
const joiValidator = require("../../middleware/joi-validator")
const Joi = require("joi")
function generateCssValidator() {
return joiValidator.body(
Joi.object({
_id: Joi.string().required(),
_rev: Joi.string().required(),
props: Joi.object()
.required()
.unknown(true),
})
.required()
.unknown(true)
)
}
const router = Router()
@ -40,12 +24,6 @@ if (env.NODE_ENV !== "production") {
}
router
.post(
"/api/css/generate",
authorized(BUILDER),
generateCssValidator(),
controller.generateCss
)
.post(
"/api/attachments/process",
authorized(BUILDER),

View file

@ -1,9 +1,6 @@
const CouchDB = require("../../../db")
const supertest = require("supertest")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions")
const packageJson = require("../../../../package")
const jwt = require("jsonwebtoken")
const env = require("../../../environment")
@ -131,49 +128,7 @@ exports.createUser = async (
return res.body
}
const createUserWithOnePermission = async (request, appId, permName) => {
let permissions = [permName]
return await createUserWithPermissions(
request,
appId,
permissions,
"onePermOnlyUser"
)
}
const createUserWithAdminPermissions = async (request, appId) => {
let permissions = [BUILTIN_PERMISSION_NAMES.ADMIN]
return await createUserWithPermissions(
request,
appId,
permissions,
"adminUser"
)
}
const createUserWithAllPermissionExceptOne = async (
request,
appId,
permName
) => {
let permissions = [permName]
return await createUserWithPermissions(
request,
appId,
permissions,
"allPermsExceptOneUser"
)
}
const createUserWithPermissions = async (
request,
appId,
permissions,
email
) => {
const createUserWithRole = async (request, appId, roleId, email) => {
const password = `password_${email}`
await request
.post(`/api/users`)
@ -181,8 +136,7 @@ const createUserWithPermissions = async (
.send({
email,
password,
roleId: BUILTIN_ROLE_IDS.POWER,
permissions,
roleId,
})
const anonUser = {
@ -215,23 +169,29 @@ exports.testPermissionsForEndpoint = async ({
url,
body,
appId,
permName1,
permName2,
passRole,
failRole,
}) => {
const headers = await createUserWithOnePermission(request, appId, permName1)
await createRequest(request, method, url, body)
.set(headers)
.expect(200)
const noPermsHeaders = await createUserWithAllPermissionExceptOne(
const passHeader = await createUserWithRole(
request,
appId,
permName2
passRole,
"passUser@budibase.com"
)
await createRequest(request, method, url, body)
.set(noPermsHeaders)
.set(passHeader)
.expect(200)
const failHeader = await createUserWithRole(
request,
appId,
failRole,
"failUser@budibase.com"
)
await createRequest(request, method, url, body)
.set(failHeader)
.expect(403)
}
@ -242,7 +202,12 @@ exports.builderEndpointShouldBlockNormalUsers = async ({
body,
appId,
}) => {
const headers = await createUserWithAdminPermissions(request, appId)
const headers = await createUserWithRole(
request,
appId,
BUILTIN_ROLE_IDS.BASIC,
"basicUser@budibase.com"
)
await createRequest(request, method, url, body)
.set(headers)

View file

@ -1,46 +0,0 @@
const { generateAssetCss, generateCss } = require("../../../utilities/builder/generateCss")
describe("generate_css", () => {
it("Check how array styles are output", () => {
expect(generateCss({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0 10 0 15;")
})
it("Check handling of an array with empty string values", () => {
expect(generateCss({ padding: ["", "", "", ""] })).toBe("")
})
it("Check handling of an empty array", () => {
expect(generateCss({ margin: [] })).toBe("")
})
it("Check handling of valid font property", () => {
expect(generateCss({ "font-size": "10px" })).toBe("font-size: 10px;")
})
})
describe("generate_screen_css", () => {
const normalComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: { "font-size": "16px" }, hover: {}, active: {}, selected: {} } }
it("Test generation of normal css styles", () => {
expect(generateAssetCss([normalComponent])).toBe(".header-123-456 {\nfont-size: 16px;\n}")
})
const hoverComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {"font-size": "16px"}, active: {}, selected: {} } }
it("Test generation of hover css styles", () => {
expect(generateAssetCss([hoverComponent])).toBe(".header-123-456:hover {\nfont-size: 16px;\n}")
})
const selectedComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: { "font-size": "16px" } } }
it("Test generation of selection css styles", () => {
expect(generateAssetCss([selectedComponent])).toBe(".header-123-456::selection {\nfont-size: 16px;\n}")
})
const emptyComponent = { _id: "123-456", _component: "@standard-components/header", _children: [], _styles: { normal: {}, hover: {}, active: {}, selected: {} } }
it("Testing handling of empty component styles", () => {
expect(generateAssetCss([emptyComponent])).toBe("")
})
})

View file

@ -1,15 +1,20 @@
const {
const {
createApplication,
createTable,
createView,
supertest,
defaultHeaders
defaultHeaders,
} = require("./couchTestUtils")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const {
BUILTIN_ROLE_IDS,
} = require("../../../utilities/security/roles")
BUILTIN_PERMISSION_IDS,
} = require("../../../utilities/security/permissions")
const roleBody = { name: "user", inherits: BUILTIN_ROLE_IDS.BASIC }
const roleBody = {
name: "NewRole",
inherits: BUILTIN_ROLE_IDS.BASIC,
permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY,
}
describe("/roles", () => {
let server
@ -19,8 +24,8 @@ describe("/roles", () => {
let view
beforeAll(async () => {
({ request, server } = await supertest())
});
;({ request, server } = await supertest())
})
afterAll(() => {
server.close()
@ -34,30 +39,29 @@ describe("/roles", () => {
})
describe("create", () => {
it("returns a success message when role is successfully created", async () => {
const res = await request
.post(`/api/roles`)
.send(roleBody)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.res.statusMessage).toEqual("Role 'user' created successfully.")
expect(res.res.statusMessage).toEqual(
"Role 'NewRole' created successfully."
)
expect(res.body._id).toBeDefined()
expect(res.body._rev).toBeDefined()
})
});
})
describe("fetch", () => {
it("should list custom roles, plus 2 default roles", async () => {
const createRes = await request
.post(`/api/roles`)
.send(roleBody)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
const customRole = createRes.body
@ -65,33 +69,37 @@ describe("/roles", () => {
const res = await request
.get(`/api/roles`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toBe(3)
expect(res.body.length).toBe(5)
const adminRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.ADMIN)
expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER)
expect(adminRole).toBeDefined()
expect(adminRole.inherits).toEqual(BUILTIN_ROLE_IDS.POWER)
expect(adminRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.ADMIN)
const powerUserRole = res.body.find(r => r._id === BUILTIN_ROLE_IDS.POWER)
expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(powerUserRole).toBeDefined()
expect(powerUserRole.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(powerUserRole.permissionId).toEqual(BUILTIN_PERMISSION_IDS.POWER)
const customRoleFetched = res.body.find(r => r._id === customRole._id)
expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(customRoleFetched).toBeDefined()
expect(customRoleFetched.inherits).toEqual(BUILTIN_ROLE_IDS.BASIC)
expect(customRoleFetched.permissionId).toEqual(
BUILTIN_PERMISSION_IDS.READ_ONLY
)
})
});
})
describe("destroy", () => {
it("should delete custom roles", async () => {
const createRes = await request
.post(`/api/roles`)
.send({ name: "user" })
.send({ name: "user", permissionId: BUILTIN_PERMISSION_IDS.READ_ONLY })
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
const customRole = createRes.body
@ -107,4 +115,4 @@ describe("/roles", () => {
.expect(404)
})
})
});
})

View file

@ -51,11 +51,9 @@ describe("/rows", () => {
describe("save, load, update, delete", () => {
it("returns a success message when the row is created", async () => {
const res = await createRow()
expect(res.res.statusMessage).toEqual(`${table.name} created successfully`)
expect(res.res.statusMessage).toEqual(`${table.name} saved successfully`)
expect(res.body.name).toEqual("Test Contact")
expect(res.body._rev).toBeDefined()
})
@ -118,30 +116,6 @@ describe("/rows", () => {
expect(res.body.find(r => r.name === row.name)).toBeDefined()
})
it("lists rows when queried by their ID", async () => {
const newRow = {
tableId: table._id,
name: "Second Contact",
status: "new"
}
const row = await createRow()
const secondRow = await createRow(newRow)
const rowIds = [row.body._id, secondRow.body._id]
const res = await request
.post(`/api/rows/search`)
.set(defaultHeaders(appId))
.send({
keys: rowIds
})
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.length).toBe(2)
expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(rowIds))
})
it("load should return 404 when row does not exist", async () => {
await createRow()
await request

View file

@ -5,12 +5,14 @@ const {
createUser,
testPermissionsForEndpoint,
} = require("./couchTestUtils")
const {
BUILTIN_PERMISSION_NAMES,
} = require("../../../utilities/security/permissions")
const {
BUILTIN_ROLE_IDS,
} = require("../../../utilities/security/roles")
const { BUILTIN_ROLE_IDS } = require("../../../utilities/security/roles")
const { cloneDeep } = require("lodash/fp")
const baseBody = {
email: "bill@bill.com",
password: "yeeooo",
roleId: BUILTIN_ROLE_IDS.POWER,
}
describe("/users", () => {
let request
@ -19,13 +21,13 @@ describe("/users", () => {
let appId
beforeAll(async () => {
({ request, server } = await supertest(server))
});
;({ request, server } = await supertest(server))
})
beforeEach(async () => {
app = await createApplication(request)
appId = app.instance._id
});
})
afterAll(() => {
server.close()
@ -39,9 +41,9 @@ describe("/users", () => {
const res = await request
.get(`/api/users`)
.set(defaultHeaders(appId))
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toBe(2)
expect(res.body.find(u => u.email === "brenda@brenda.com")).toBeDefined()
expect(res.body.find(u => u.email === "pam@pam.com")).toBeDefined()
@ -54,37 +56,39 @@ describe("/users", () => {
method: "GET",
url: `/api/users`,
appId: appId,
permName1: BUILTIN_PERMISSION_NAMES.POWER,
permName2: BUILTIN_PERMISSION_NAMES.WRITE,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
})
describe("create", () => {
it("returns a success message when a user is successfully created", async () => {
const body = cloneDeep(baseBody)
body.email = "bill@budibase.com"
const res = await request
.post(`/api/users`)
.set(defaultHeaders(appId))
.send({ email: "bill@bill.com", password: "bills_password", roleId: BUILTIN_ROLE_IDS.POWER })
.send(body)
.expect(200)
.expect('Content-Type', /json/)
.expect("Content-Type", /json/)
expect(res.res.statusMessage).toEqual("User created successfully.");
expect(res.res.statusMessage).toEqual("User created successfully.")
expect(res.body._id).toBeUndefined()
})
it("should apply authorization to endpoint", async () => {
const body = cloneDeep(baseBody)
body.email = "brandNewUser@user.com"
await testPermissionsForEndpoint({
request,
method: "POST",
body: { email: "brandNewUser@user.com", password: "yeeooo", roleId: BUILTIN_ROLE_IDS.POWER },
body,
url: `/api/users`,
appId: appId,
permName1: BUILTIN_PERMISSION_NAMES.ADMIN,
permName2: BUILTIN_PERMISSION_NAMES.POWER,
passRole: BUILTIN_ROLE_IDS.ADMIN,
failRole: BUILTIN_ROLE_IDS.PUBLIC,
})
})
});
})
})

View file

@ -32,7 +32,7 @@ const USERS_TABLE_SCHEMA = {
constraints: {
type: "string",
presence: false,
inclusion: Object.keys(BUILTIN_ROLE_IDS),
inclusion: Object.values(BUILTIN_ROLE_IDS),
},
},
},

View file

@ -15,6 +15,8 @@ const DocumentTypes = {
INSTANCE: "inst",
LAYOUT: "layout",
SCREEN: "screen",
DATASOURCE: "datasource",
QUERY: "query",
}
const ViewNames = {
@ -102,15 +104,11 @@ exports.generateRowID = tableId => {
* Gets parameters for retrieving users, this is a utility function for the getDocParams function.
*/
exports.getUserParams = (email = "", otherProps = {}) => {
return getDocParams(
DocumentTypes.ROW,
`${ViewNames.USERS}${SEPARATOR}${DocumentTypes.USER}${SEPARATOR}${email}`,
otherProps
)
return exports.getRowParams(ViewNames.USERS, email, otherProps)
}
/**
* Generates a new user ID based on the passed in username.
* Generates a new user ID based on the passed in email.
* @param {string} email The email which the ID is going to be built up of.
* @returns {string} The new user ID which the user doc can be stored under.
*/
@ -227,3 +225,33 @@ exports.generateWebhookID = () => {
exports.getWebhookParams = (webhookId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.WEBHOOK, webhookId, otherProps)
}
/**
* Generates a new datasource ID.
* @returns {string} The new datasource ID which the webhook doc can be stored under.
*/
exports.generateDatasourceID = () => {
return `${DocumentTypes.DATASOURCE}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving a datasource, this is a utility function for the getDocParams function.
*/
exports.getDatasourceParams = (datasourceId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.DATASOURCE, datasourceId, otherProps)
}
/**
* Generates a new query ID.
* @returns {string} The new query ID which the query doc can be stored under.
*/
exports.generateQueryID = () => {
return `${DocumentTypes.QUERY}${SEPARATOR}${newid()}`
}
/**
* Gets parameters for retrieving a query, this is a utility function for the getDocParams function.
*/
exports.getQueryParams = (queryId = null, otherProps = {}) => {
return getDocParams(DocumentTypes.QUERY, queryId, otherProps)
}

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