separation of datasources and queries
This commit is contained in:
commit
63ad74b660
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.3.8",
|
||||
"version": "0.4.2",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
.nav-item:hover,
|
||||
.nav-item.selected {
|
||||
border-radius: var(--border-radius-m);
|
||||
border-radius: var(--border-radius-s);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
|
|
@ -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}
|
|
@ -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 = {
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
overflow: hidden;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
}
|
||||
.component-container iframe {
|
||||
border: 0;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}.`)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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}
|
|
@ -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 />
|
|
@ -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>
|
|
@ -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 />
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
// }
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { routeStore } from "./routes"
|
||||
import { screenStore } from "./screens"
|
||||
|
||||
export async function initialise() {
|
||||
await routeStore.actions.fetchRoutes()
|
||||
await screenStore.actions.fetchScreens()
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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.`
|
||||
// }
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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("")
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue