merge with develop
This commit is contained in:
commit
6f5567b4b6
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -12,7 +12,7 @@ const populateFromDB = async (userId, tenantId) => {
|
|||
const user = await getGlobalDB(tenantId).get(userId)
|
||||
user.budibaseAccess = true
|
||||
|
||||
if (!env.SELF_HOSTED) {
|
||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||
const account = await accounts.getAccount(user.email)
|
||||
if (account) {
|
||||
user.account = account
|
||||
|
|
|
@ -21,6 +21,7 @@ module.exports = {
|
|||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
|
||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
isTest,
|
||||
|
|
|
@ -19,6 +19,22 @@ const removeTenantFromInfoDB = async tenantId => {
|
|||
}
|
||||
}
|
||||
|
||||
exports.removeUserFromInfoDB = async dbUser => {
|
||||
const infoDb = getDB(PLATFORM_INFO_DB)
|
||||
const keys = [dbUser._id, dbUser.email]
|
||||
const userDocs = await infoDb.allDocs({
|
||||
keys,
|
||||
include_docs: true,
|
||||
})
|
||||
const toDelete = userDocs.rows.map(row => {
|
||||
return {
|
||||
...row.doc,
|
||||
_deleted: true,
|
||||
}
|
||||
})
|
||||
await infoDb.bulkDocs(toDelete)
|
||||
}
|
||||
|
||||
const removeUsersFromInfoDB = async tenantId => {
|
||||
try {
|
||||
const globalDb = getGlobalDB(tenantId)
|
||||
|
|
|
@ -73,7 +73,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => {
|
|||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
exports.getGlobalDB = (tenantId = null) => {
|
||||
exports.getGlobalDBName = (tenantId = null) => {
|
||||
// tenant ID can be set externally, for example user API where
|
||||
// new tenants are being created, this may be the case
|
||||
if (!tenantId) {
|
||||
|
@ -81,13 +81,16 @@ exports.getGlobalDB = (tenantId = null) => {
|
|||
}
|
||||
|
||||
let dbName
|
||||
|
||||
if (tenantId === DEFAULT_TENANT_ID) {
|
||||
dbName = StaticDatabases.GLOBAL.name
|
||||
} else {
|
||||
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
|
||||
}
|
||||
return dbName
|
||||
}
|
||||
|
||||
exports.getGlobalDB = (tenantId = null) => {
|
||||
const dbName = exports.getGlobalDBName(tenantId)
|
||||
return getDB(dbName)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"license": "AGPL-3.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -165,7 +165,7 @@ Cypress.Commands.add("getComponent", componentId => {
|
|||
.its("body")
|
||||
.should("not.be.null")
|
||||
.then(cy.wrap)
|
||||
.find(`[data-component-id=${componentId}]`)
|
||||
.find(`[data-id=${componentId}]`)
|
||||
})
|
||||
|
||||
Cypress.Commands.add("navigateToFrontend", () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.146-alpha.4",
|
||||
"@budibase/client": "^0.9.146-alpha.4",
|
||||
"@budibase/bbui": "^0.9.147-alpha.0",
|
||||
"@budibase/client": "^0.9.147-alpha.0",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.147-alpha.0",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import iframeTemplate from "./iframeTemplate"
|
||||
|
@ -7,6 +8,7 @@
|
|||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui"
|
||||
import ErrorSVG from "assets/error.svg?raw"
|
||||
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||
|
||||
let iframe
|
||||
let layout
|
||||
|
@ -102,7 +104,7 @@
|
|||
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
||||
})
|
||||
|
||||
// remove all iframe event listeners on component destroy
|
||||
// Remove all iframe event listeners on component destroy
|
||||
onDestroy(() => {
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
||||
|
@ -122,6 +124,26 @@
|
|||
// Wait for this event to show the client library if intelligent
|
||||
// loading is supported
|
||||
loading = false
|
||||
} else if (type === "move-component") {
|
||||
const { componentId, destinationComponentId } = data
|
||||
const rootComponent = get(currentAsset).props
|
||||
|
||||
// Get source and destination components
|
||||
const source = findComponent(rootComponent, componentId)
|
||||
const destination = findComponent(rootComponent, destinationComponentId)
|
||||
|
||||
// Stop if the target is a child of source
|
||||
const path = findComponentPath(source, destinationComponentId)
|
||||
const ids = path.map(component => component._id)
|
||||
if (ids.includes(data.destinationComponentId)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Cut and paste the component to the new destination
|
||||
if (source && destination) {
|
||||
store.actions.components.copy(source, true)
|
||||
store.actions.components.paste(destination, data.mode)
|
||||
}
|
||||
} else {
|
||||
console.warning(`Client sent unknown event type: ${type}`)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
import { onMount } from "svelte"
|
||||
|
||||
let loaded = false
|
||||
// don't react to these
|
||||
let cloud = $admin.cloud
|
||||
let shouldRedirect = !cloud || $admin.disableAccountPortal
|
||||
|
||||
$: multiTenancyEnabled = $admin.multiTenancy
|
||||
$: hasAdminUser = $admin?.checklist?.adminUser?.checked
|
||||
|
@ -39,30 +42,35 @@
|
|||
|
||||
$: {
|
||||
// We should never see the org or admin user creation screens in the cloud
|
||||
if (!cloud) {
|
||||
const apiReady = $admin.loaded && $auth.loaded
|
||||
// if tenant is not set go to it
|
||||
if (loaded && apiReady && multiTenancyEnabled && !tenantSet) {
|
||||
$redirect("./auth/org")
|
||||
}
|
||||
// Force creation of an admin user if one doesn't exist
|
||||
else if (loaded && apiReady && !hasAdminUser) {
|
||||
$redirect("./admin")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Redirect to log in at any time if the user isn't authenticated
|
||||
$: {
|
||||
const apiReady = $admin.loaded && $auth.loaded
|
||||
// if tenant is not set go to it
|
||||
if (
|
||||
loaded &&
|
||||
shouldRedirect &&
|
||||
apiReady &&
|
||||
multiTenancyEnabled &&
|
||||
!tenantSet
|
||||
) {
|
||||
$redirect("./auth/org")
|
||||
}
|
||||
// Force creation of an admin user if one doesn't exist
|
||||
else if (loaded && shouldRedirect && apiReady && !hasAdminUser) {
|
||||
$redirect("./admin")
|
||||
}
|
||||
// Redirect to log in at any time if the user isn't authenticated
|
||||
else if (
|
||||
loaded &&
|
||||
(hasAdminUser || cloud) &&
|
||||
!$auth.user &&
|
||||
!$isActive("./auth") &&
|
||||
!$isActive("./invite")
|
||||
!$isActive("./invite") &&
|
||||
!$isActive("./admin")
|
||||
) {
|
||||
const returnUrl = encodeURIComponent(window.location.pathname)
|
||||
$redirect("./auth?", { returnUrl })
|
||||
} else if ($auth?.user?.forceResetPassword) {
|
||||
}
|
||||
// check if password reset required for user
|
||||
else if ($auth.user?.forceResetPassword) {
|
||||
$redirect("./auth/reset")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
|
||||
import { post } from "builderStore/api"
|
||||
|
||||
let submitting = false
|
||||
|
||||
$: value = { file: null }
|
||||
|
||||
async function importApps() {
|
||||
submitting = true
|
||||
|
||||
try {
|
||||
// Create form data to create app
|
||||
let data = new FormData()
|
||||
data.append("importFile", value.file)
|
||||
|
||||
// Create App
|
||||
const importResp = await post("/api/cloud/import", data, {})
|
||||
const importJson = await importResp.json()
|
||||
if (!importResp.ok) {
|
||||
throw new Error(importJson.message)
|
||||
}
|
||||
// now reload to get to login
|
||||
window.location.reload()
|
||||
} catch (error) {
|
||||
notifications.error(error)
|
||||
submitting = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Import apps"
|
||||
confirmText="Import apps"
|
||||
onConfirm={importApps}
|
||||
disabled={!value.file}
|
||||
>
|
||||
<Body
|
||||
>Please upload the file that was exported from your Cloud environment to get
|
||||
started</Body
|
||||
>
|
||||
<Dropzone
|
||||
gallery={false}
|
||||
label="File to import"
|
||||
value={[value.file]}
|
||||
on:change={e => {
|
||||
value.file = e.detail?.[0]
|
||||
}}
|
||||
/>
|
||||
</ModalContent>
|
|
@ -7,18 +7,22 @@
|
|||
Input,
|
||||
Body,
|
||||
ActionButton,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import api from "builderStore/api"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
|
||||
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
|
||||
import Logo from "assets/bb-emblem.svg"
|
||||
|
||||
let adminUser = {}
|
||||
let error
|
||||
let modal
|
||||
|
||||
$: tenantId = $auth.tenantId
|
||||
$: multiTenancyEnabled = $admin.multiTenancy
|
||||
$: cloud = $admin.cloud
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
|
@ -38,6 +42,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} padding={false} width="600px">
|
||||
<ImportAppsModal />
|
||||
</Modal>
|
||||
<section>
|
||||
<div class="container">
|
||||
<Layout>
|
||||
|
@ -66,6 +73,15 @@
|
|||
>
|
||||
Change organisation
|
||||
</ActionButton>
|
||||
{:else if !cloud}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => {
|
||||
modal.show()
|
||||
}}
|
||||
>
|
||||
Import from cloud
|
||||
</ActionButton>
|
||||
{/if}
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
$redirect("../")
|
||||
}
|
||||
|
||||
// redirect to account portal for authentication in the cloud
|
||||
if (
|
||||
!$auth.user &&
|
||||
$admin.cloud &&
|
||||
!$admin.disableAccountPortal &&
|
||||
$admin.accountPortalUrl &&
|
||||
!$admin?.checklist?.sso?.checked
|
||||
) {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
let tenantId = get(auth).tenantSet ? get(auth).tenantId : ""
|
||||
$: multiTenancyEnabled = $admin.multiTenancy
|
||||
$: cloud = $admin.cloud
|
||||
$: disableAccountPortal = $admin.disableAccountPortal
|
||||
|
||||
async function setOrg() {
|
||||
if (tenantId == null || tenantId === "") {
|
||||
|
@ -26,7 +27,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
await auth.checkQueryString()
|
||||
if (!multiTenancyEnabled || cloud) {
|
||||
if (!multiTenancyEnabled || (cloud && !disableAccountPortal)) {
|
||||
$goto("../")
|
||||
} else {
|
||||
admin.unload()
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
auth.checkQueryString()
|
||||
|
||||
$: {
|
||||
if (!$auth.user) {
|
||||
$redirect(`./auth`)
|
||||
} else if ($auth.user.builder?.global) {
|
||||
if ($auth.user?.builder?.global) {
|
||||
$redirect(`./portal`)
|
||||
} else {
|
||||
} else if ($auth.user) {
|
||||
$redirect(`./apps`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
Page,
|
||||
notifications,
|
||||
Body,
|
||||
Search,
|
||||
} from "@budibase/bbui"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
|
@ -35,8 +36,13 @@
|
|||
let unpublishModal
|
||||
let creatingApp = false
|
||||
let loaded = false
|
||||
let searchTerm = ""
|
||||
let cloud = $admin.cloud
|
||||
|
||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||
$: filteredApps = enrichedApps.filter(app =>
|
||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
const enrichApps = (apps, user, sortBy) => {
|
||||
const enrichedApps = apps.map(app => ({
|
||||
|
@ -45,6 +51,7 @@
|
|||
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
|
||||
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
|
||||
}))
|
||||
|
||||
if (sortBy === "status") {
|
||||
return enrichedApps.sort((a, b) => {
|
||||
if (a.status === b.status) {
|
||||
|
@ -70,6 +77,15 @@
|
|||
creatingApp = true
|
||||
}
|
||||
|
||||
const initiateAppsExport = () => {
|
||||
try {
|
||||
download(`/api/cloud/export`)
|
||||
notifications.success("Apps exported successfully")
|
||||
} catch (err) {
|
||||
notifications.error(`Error exporting apps: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
const initiateAppImport = () => {
|
||||
template = { fromFile: true }
|
||||
creationModal.show()
|
||||
|
@ -190,6 +206,9 @@
|
|||
<div class="title">
|
||||
<Heading>Apps</Heading>
|
||||
<ButtonGroup>
|
||||
{#if cloud}
|
||||
<Button secondary on:click={initiateAppsExport}>Export apps</Button>
|
||||
{/if}
|
||||
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
||||
<Button cta on:click={initiateAppCreation}>Create app</Button>
|
||||
</ButtonGroup>
|
||||
|
@ -205,6 +224,7 @@
|
|||
{ label: "Sort by status", value: "status" },
|
||||
]}
|
||||
/>
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
</div>
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
|
@ -225,7 +245,7 @@
|
|||
class:appGrid={layout === "grid"}
|
||||
class:appTable={layout === "table"}
|
||||
>
|
||||
{#each enrichedApps as app (app.appId)}
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<svelte:component
|
||||
this={layout === "grid" ? AppCard : AppRow}
|
||||
{releaseLock}
|
||||
|
@ -301,7 +321,9 @@
|
|||
}
|
||||
|
||||
.select {
|
||||
width: 190px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
|
||||
.appGrid {
|
||||
|
|
|
@ -7,6 +7,7 @@ export function createAdminStore() {
|
|||
loaded: false,
|
||||
multiTenancy: false,
|
||||
cloud: false,
|
||||
disableAccountPortal: false,
|
||||
accountPortalUrl: "",
|
||||
onboardingProgress: 0,
|
||||
checklist: {
|
||||
|
@ -47,12 +48,14 @@ export function createAdminStore() {
|
|||
async function getEnvironment() {
|
||||
let multiTenancyEnabled = false
|
||||
let cloud = false
|
||||
let disableAccountPortal = false
|
||||
let accountPortalUrl = ""
|
||||
try {
|
||||
const response = await api.get(`/api/system/environment`)
|
||||
const json = await response.json()
|
||||
multiTenancyEnabled = json.multiTenancy
|
||||
cloud = json.cloud
|
||||
disableAccountPortal = json.disableAccountPortal
|
||||
accountPortalUrl = json.accountPortalUrl
|
||||
} catch (err) {
|
||||
// just let it stay disabled
|
||||
|
@ -60,6 +63,7 @@ export function createAdminStore() {
|
|||
admin.update(store => {
|
||||
store.multiTenancy = multiTenancyEnabled
|
||||
store.cloud = cloud
|
||||
store.disableAccountPortal = disableAccountPortal
|
||||
store.accountPortalUrl = accountPortalUrl
|
||||
return store
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.146-alpha.4",
|
||||
"@budibase/bbui": "^0.9.147-alpha.0",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.147-alpha.0",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||
import DNDHandler from "components/preview/DNDHandler.svelte"
|
||||
import ErrorSVG from "builder/assets/error.svg"
|
||||
|
||||
// Provide contexts
|
||||
|
@ -106,7 +107,10 @@
|
|||
<div id="app-root">
|
||||
<CustomThemeWrapper>
|
||||
{#key $screenStore.activeLayout._id}
|
||||
<Component instance={$screenStore.activeLayout.props} />
|
||||
<Component
|
||||
isLayout
|
||||
instance={$screenStore.activeLayout.props}
|
||||
/>
|
||||
{/key}
|
||||
|
||||
<!-- Layers on top of app -->
|
||||
|
@ -124,6 +128,7 @@
|
|||
{#if $builderStore.inBuilder}
|
||||
<SelectionIndicator />
|
||||
<HoverIndicator />
|
||||
<DNDHandler />
|
||||
{/if}
|
||||
</div>
|
||||
</StateBindingsProvider>
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
import Placeholder from "components/app/Placeholder.svelte"
|
||||
|
||||
export let instance = {}
|
||||
export let isLayout = false
|
||||
export let isScreen = false
|
||||
|
||||
// The enriched component settings
|
||||
let enrichedSettings
|
||||
|
@ -49,11 +51,11 @@
|
|||
$: children = instance._children || []
|
||||
$: id = instance._id
|
||||
$: name = instance._instanceName
|
||||
$: empty =
|
||||
!children.length &&
|
||||
definition?.hasChildren &&
|
||||
definition?.showEmptyState !== false &&
|
||||
$builderStore.inBuilder
|
||||
$: interactive =
|
||||
$builderStore.inBuilder &&
|
||||
($builderStore.previewType === "layout" || insideScreenslot)
|
||||
$: empty = interactive && !children.length && definition?.hasChildren
|
||||
$: emptyState = empty && definition?.showEmptyState !== false
|
||||
$: rawProps = getRawProps(instance)
|
||||
$: instanceKey = JSON.stringify(rawProps)
|
||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||
|
@ -61,16 +63,16 @@
|
|||
$builderStore.inBuilder &&
|
||||
$builderStore.selectedComponentId === instance._id
|
||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
|
||||
$: evaluateConditions(enrichedSettings?._conditions)
|
||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||
$: renderKey = `${propsHash}-${emptyState}`
|
||||
|
||||
// Update component context
|
||||
$: componentStore.set({
|
||||
id,
|
||||
children: children.length,
|
||||
styles: { ...instance._styles, id, empty, interactive },
|
||||
empty,
|
||||
styles: { ...instance._styles, id, empty: emptyState, interactive },
|
||||
empty: emptyState,
|
||||
selected,
|
||||
name,
|
||||
})
|
||||
|
@ -169,13 +171,22 @@
|
|||
conditionalSettings = result.settingUpdates
|
||||
visible = nextVisible
|
||||
}
|
||||
|
||||
// Drag and drop helper tags
|
||||
$: draggable = interactive && !isLayout && !isScreen
|
||||
$: droppable = interactive && !isLayout && !isScreen
|
||||
</script>
|
||||
|
||||
{#key propsHash}
|
||||
{#key renderKey}
|
||||
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||
<!-- and the performance matters for the selection indicators -->
|
||||
<div
|
||||
class={`component ${id}`}
|
||||
data-type={interactive ? "component" : ""}
|
||||
class:draggable
|
||||
class:droppable
|
||||
class:empty
|
||||
class:interactive
|
||||
data-id={id}
|
||||
data-name={name}
|
||||
>
|
||||
|
@ -184,7 +195,7 @@
|
|||
{#each children as child (child._id)}
|
||||
<svelte:self instance={child} />
|
||||
{/each}
|
||||
{:else if empty}
|
||||
{:else if emptyState}
|
||||
<Placeholder />
|
||||
{/if}
|
||||
</svelte:component>
|
||||
|
@ -196,4 +207,10 @@
|
|||
.component {
|
||||
display: contents;
|
||||
}
|
||||
.interactive :global(*:hover) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.draggable :global(*:hover) {
|
||||
cursor: grab;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -22,6 +22,6 @@
|
|||
<!-- Ensure to fully remount when screen changes -->
|
||||
{#key screenDefinition?._id}
|
||||
<Provider key="url" data={params}>
|
||||
<Component instance={screenDefinition} />
|
||||
<Component isScreen instance={screenDefinition} />
|
||||
</Provider>
|
||||
{/key}
|
||||
|
|
|
@ -31,4 +31,7 @@
|
|||
.spectrum-Button--overBackground:hover {
|
||||
color: #555;
|
||||
}
|
||||
.spectrum-Button::after {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
display: flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
.valid-container :global([data-type="component"] > *) {
|
||||
.valid-container :global(.component > *) {
|
||||
max-width: 100%;
|
||||
}
|
||||
.direction-row {
|
||||
|
@ -46,7 +46,7 @@
|
|||
|
||||
/* Grow containers inside a row need 0 width 0 so that they ignore content */
|
||||
/* The nested selector for data-type is the wrapper around all components */
|
||||
.direction-row :global(> [data-type="component"] > .size-grow) {
|
||||
.direction-row :global(> .component > .size-grow) {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
<script context="module">
|
||||
export const Sides = {
|
||||
Top: "Top",
|
||||
Right: "Right",
|
||||
Bottom: "Bottom",
|
||||
Left: "Left",
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
import IndicatorSet from "./IndicatorSet.svelte"
|
||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||
import { builderStore } from "stores"
|
||||
|
||||
let dragInfo
|
||||
let dropInfo
|
||||
|
||||
const getEdges = (bounds, mousePoint) => {
|
||||
const { width, height, top, left } = bounds
|
||||
return {
|
||||
[Sides.Top]: [mousePoint[0], top],
|
||||
[Sides.Right]: [left + width, mousePoint[1]],
|
||||
[Sides.Bottom]: [mousePoint[0], top + height],
|
||||
[Sides.Left]: [left, mousePoint[1]],
|
||||
}
|
||||
}
|
||||
|
||||
const calculatePointDelta = (point1, point2) => {
|
||||
const deltaX = Math.abs(point1[0] - point2[0])
|
||||
const deltaY = Math.abs(point1[1] - point2[1])
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||
}
|
||||
|
||||
const getDOMNodeForComponent = component => {
|
||||
const parent = component.closest(".component")
|
||||
const children = Array.from(parent.childNodes)
|
||||
return children?.find(node => node?.nodeType === 1)
|
||||
}
|
||||
|
||||
// Callback when initially starting a drag on a draggable component
|
||||
const onDragStart = e => {
|
||||
const parent = e.target.closest(".component")
|
||||
if (!parent?.classList.contains("draggable")) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update state
|
||||
dragInfo = {
|
||||
target: parent.dataset.id,
|
||||
parent: parent.dataset.parent,
|
||||
}
|
||||
builderStore.actions.selectComponent(dragInfo.target)
|
||||
builderStore.actions.setDragging(true)
|
||||
|
||||
// Highlight being dragged by setting opacity
|
||||
const child = getDOMNodeForComponent(e.target)
|
||||
if (child) {
|
||||
child.style.opacity = "0.5"
|
||||
}
|
||||
}
|
||||
|
||||
// Callback when drag stops (whether dropped or not)
|
||||
const onDragEnd = e => {
|
||||
// Reset opacity style
|
||||
if (dragInfo) {
|
||||
const child = getDOMNodeForComponent(e.target)
|
||||
if (child) {
|
||||
child.style.opacity = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Reset state and styles
|
||||
dragInfo = null
|
||||
dropInfo = null
|
||||
builderStore.actions.setDragging(false)
|
||||
}
|
||||
|
||||
// Callback when on top of a component
|
||||
const onDragOver = e => {
|
||||
// Skip if we aren't validly dragging currently
|
||||
if (!dragInfo || !dropInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
const { droppableInside, bounds } = dropInfo
|
||||
const { top, left, height, width } = bounds
|
||||
const mouseY = e.clientY
|
||||
const mouseX = e.clientX
|
||||
const snapFactor = droppableInside ? 0.33 : 0.5
|
||||
const snapLimitV = Math.min(40, height * snapFactor)
|
||||
const snapLimitH = Math.min(40, width * snapFactor)
|
||||
|
||||
// Determine all sies we are within snap range of
|
||||
let sides = []
|
||||
if (mouseY <= top + snapLimitV) {
|
||||
sides.push(Sides.Top)
|
||||
} else if (mouseY >= top + height - snapLimitV) {
|
||||
sides.push(Sides.Bottom)
|
||||
}
|
||||
if (mouseX < left + snapLimitH) {
|
||||
sides.push(Sides.Left)
|
||||
} else if (mouseX > left + width - snapLimitH) {
|
||||
sides.push(Sides.Right)
|
||||
}
|
||||
|
||||
// When no edges match, drop inside if possible
|
||||
if (!sides.length) {
|
||||
dropInfo.mode = droppableInside ? "inside" : null
|
||||
dropInfo.side = null
|
||||
return
|
||||
}
|
||||
|
||||
// When one edge matches, use that edge
|
||||
if (sides.length === 1) {
|
||||
dropInfo.side = sides[0]
|
||||
if ([Sides.Top, Sides.Left].includes(sides[0])) {
|
||||
dropInfo.mode = "above"
|
||||
} else {
|
||||
dropInfo.mode = "below"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// When 2 edges match, work out which is closer
|
||||
const mousePoint = [mouseX, mouseY]
|
||||
const edges = getEdges(bounds, mousePoint)
|
||||
const edge1 = edges[sides[0]]
|
||||
const delta1 = calculatePointDelta(mousePoint, edge1)
|
||||
const edge2 = edges[sides[1]]
|
||||
const delta2 = calculatePointDelta(mousePoint, edge2)
|
||||
const edge = delta1 < delta2 ? sides[0] : sides[1]
|
||||
dropInfo.side = edge
|
||||
if ([Sides.Top, Sides.Left].includes(edge)) {
|
||||
dropInfo.mode = "above"
|
||||
} else {
|
||||
dropInfo.mode = "below"
|
||||
}
|
||||
}
|
||||
|
||||
// Callback when entering a potential drop target
|
||||
const onDragEnter = e => {
|
||||
// Skip if we aren't validly dragging currently
|
||||
if (!dragInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
const element = e.target.closest(".component")
|
||||
if (
|
||||
element &&
|
||||
element.classList.contains("droppable") &&
|
||||
element.dataset.id !== dragInfo.target
|
||||
) {
|
||||
// Do nothing if this is the same target
|
||||
if (element.dataset.id === dropInfo?.target) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the dragging flag is always set.
|
||||
// There's a bit of a race condition between the app reinitialisation
|
||||
// after selecting the DND component and setting this the first time
|
||||
if (!get(builderStore).isDragging) {
|
||||
builderStore.actions.setDragging(true)
|
||||
}
|
||||
|
||||
// Store target ID
|
||||
const target = element.dataset.id
|
||||
|
||||
// Precompute and store some info to avoid recalculating everything in
|
||||
// dragOver
|
||||
const child = getDOMNodeForComponent(e.target)
|
||||
const bounds = child.getBoundingClientRect()
|
||||
dropInfo = {
|
||||
target,
|
||||
name: element.dataset.name,
|
||||
droppableInside: element.classList.contains("empty"),
|
||||
bounds,
|
||||
}
|
||||
} else {
|
||||
dropInfo = null
|
||||
}
|
||||
}
|
||||
|
||||
// Callback when leaving a potential drop target.
|
||||
// Since we don't style our targets, we don't need to unset anything.
|
||||
const onDragLeave = () => {}
|
||||
|
||||
// Callback when dropping a drag on top of some component
|
||||
const onDrop = e => {
|
||||
e.preventDefault()
|
||||
if (dropInfo?.mode) {
|
||||
builderStore.actions.moveComponent(
|
||||
dragInfo.target,
|
||||
dropInfo.target,
|
||||
dropInfo.mode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Events fired on the draggable target
|
||||
document.addEventListener("dragstart", onDragStart, false)
|
||||
document.addEventListener("dragend", onDragEnd, false)
|
||||
|
||||
// Events fired on the drop targets
|
||||
document.addEventListener("dragover", onDragOver, false)
|
||||
document.addEventListener("dragenter", onDragEnter, false)
|
||||
document.addEventListener("dragleave", onDragLeave, false)
|
||||
document.addEventListener("drop", onDrop, false)
|
||||
|
||||
return () => {
|
||||
// Events fired on the draggable target
|
||||
document.removeEventListener("dragstart", onDragStart, false)
|
||||
document.removeEventListener("dragend", onDragEnd, false)
|
||||
|
||||
// Events fired on the drop targets
|
||||
document.removeEventListener("dragover", onDragOver, false)
|
||||
document.removeEventListener("dragenter", onDragEnter, false)
|
||||
document.removeEventListener("dragleave", onDragLeave, false)
|
||||
document.removeEventListener("drop", onDrop, false)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<IndicatorSet
|
||||
componentId={dropInfo?.mode === "inside" ? dropInfo.target : null}
|
||||
color="var(--spectrum-global-color-static-green-500)"
|
||||
zIndex="930"
|
||||
transition
|
||||
prefix="Inside"
|
||||
/>
|
||||
|
||||
<DNDPositionIndicator
|
||||
{dropInfo}
|
||||
color="var(--spectrum-global-color-static-green-500)"
|
||||
zIndex="940"
|
||||
transition
|
||||
/>
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
import Indicator from "./Indicator.svelte"
|
||||
import { Sides } from "./DNDHandler.svelte"
|
||||
|
||||
export let dropInfo
|
||||
export let zIndex
|
||||
export let color
|
||||
export let transition
|
||||
|
||||
$: dimensions = getDimensions(dropInfo)
|
||||
$: prefix = dropInfo?.mode === "above" ? "Before" : "After"
|
||||
$: text = `${prefix} ${dropInfo?.name}`
|
||||
$: renderKey = `${dropInfo?.target}-${dropInfo?.side}`
|
||||
|
||||
const getDimensions = info => {
|
||||
const { bounds, side } = info ?? {}
|
||||
if (!bounds || !side) {
|
||||
return null
|
||||
}
|
||||
const { left, top, width, height } = bounds
|
||||
if (side === Sides.Top || side === Sides.Bottom) {
|
||||
return {
|
||||
top: side === Sides.Top ? top - 4 : top + height,
|
||||
left: left - 2,
|
||||
width: width + 4,
|
||||
height: 0,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
top: top - 2,
|
||||
left: side === Sides.Left ? left - 4 : left + width,
|
||||
width: 0,
|
||||
height: height + 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key renderKey}
|
||||
{#if dimensions && dropInfo?.mode !== "inside"}
|
||||
<Indicator
|
||||
left={Math.round(dimensions.left)}
|
||||
top={Math.round(dimensions.top)}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
{text}
|
||||
{zIndex}
|
||||
{color}
|
||||
{transition}
|
||||
alignRight={dropInfo?.side === Sides.Right}
|
||||
line
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
|
@ -7,7 +7,7 @@
|
|||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||
|
||||
const onMouseOver = e => {
|
||||
const element = e.target.closest("[data-type='component']")
|
||||
const element = e.target.closest(".interactive.component")
|
||||
const newId = element?.dataset?.id
|
||||
if (newId !== componentId) {
|
||||
componentId = newId
|
||||
|
@ -30,7 +30,7 @@
|
|||
</script>
|
||||
|
||||
<IndicatorSet
|
||||
{componentId}
|
||||
componentId={$builderStore.isDragging ? null : componentId}
|
||||
color="var(--spectrum-global-color-static-blue-200)"
|
||||
transition
|
||||
{zIndex}
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
export let color
|
||||
export let zIndex
|
||||
export let transition = false
|
||||
export let line = false
|
||||
export let alignRight = false
|
||||
|
||||
$: flipped = top < 20
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -18,11 +22,12 @@
|
|||
}}
|
||||
out:fade={{ duration: transition ? 130 : 0 }}
|
||||
class="indicator"
|
||||
class:flipped={top < 20}
|
||||
class:flipped
|
||||
class:line
|
||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
||||
>
|
||||
{#if text}
|
||||
<div class="text" class:flipped={top < 20}>
|
||||
<div class="text" class:flipped class:line class:right={alignRight}>
|
||||
{text}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -30,6 +35,7 @@
|
|||
|
||||
<style>
|
||||
.indicator {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
z-index: var(--zIndex);
|
||||
border: 2px solid var(--color);
|
||||
|
@ -42,6 +48,9 @@
|
|||
.indicator.flipped {
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
.indicator.line {
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
.text {
|
||||
background-color: var(--color);
|
||||
color: white;
|
||||
|
@ -61,9 +70,18 @@
|
|||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
.text.line {
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.text.flipped {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
transform: translateY(0%);
|
||||
top: -2px;
|
||||
}
|
||||
.text.right {
|
||||
right: -2px;
|
||||
left: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
export let color
|
||||
export let transition
|
||||
export let zIndex
|
||||
export let prefix = null
|
||||
|
||||
let indicators = []
|
||||
let interval
|
||||
|
@ -51,6 +52,9 @@
|
|||
const parents = document.getElementsByClassName(componentId)
|
||||
if (parents.length) {
|
||||
text = parents[0].dataset.name
|
||||
if (prefix) {
|
||||
text = `${prefix} ${text}`
|
||||
}
|
||||
}
|
||||
|
||||
// Batch reads to minimize reflow
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
let measured = false
|
||||
|
||||
$: definition = $builderStore.selectedComponentDefinition
|
||||
$: showBar = definition?.showSettingsBar
|
||||
$: showBar = definition?.showSettingsBar && !$builderStore.isDragging
|
||||
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
||||
|
||||
const updatePosition = () => {
|
||||
|
|
|
@ -24,6 +24,7 @@ const createBuilderStore = () => {
|
|||
theme: null,
|
||||
customTheme: null,
|
||||
previewDevice: "desktop",
|
||||
isDragging: false,
|
||||
}
|
||||
const writableStore = writable(initialState)
|
||||
const derivedStore = derived(writableStore, $state => {
|
||||
|
@ -68,13 +69,24 @@ const createBuilderStore = () => {
|
|||
analytics.pingEndUser()
|
||||
},
|
||||
setSelectedPath: path => {
|
||||
console.log("set to ")
|
||||
console.log(path)
|
||||
writableStore.update(state => {
|
||||
state.selectedPath = path
|
||||
return state
|
||||
})
|
||||
},
|
||||
moveComponent: (componentId, destinationComponentId, mode) => {
|
||||
dispatchEvent("move-component", {
|
||||
componentId,
|
||||
destinationComponentId,
|
||||
mode,
|
||||
})
|
||||
},
|
||||
setDragging: dragging => {
|
||||
writableStore.update(state => {
|
||||
state.isDragging = dragging
|
||||
return state
|
||||
})
|
||||
},
|
||||
}
|
||||
return {
|
||||
...writableStore,
|
||||
|
|
|
@ -23,10 +23,14 @@ export const styleable = (node, styles = {}) => {
|
|||
let applyHoverStyles
|
||||
let selectComponent
|
||||
|
||||
// Allow dragging if required
|
||||
const parent = node.closest(".component")
|
||||
if (parent && parent.classList.contains("draggable")) {
|
||||
node.setAttribute("draggable", true)
|
||||
}
|
||||
|
||||
// Creates event listeners and applies initial styles
|
||||
const setupStyles = (newStyles = {}) => {
|
||||
// Use empty state styles as base styles if required, but let them, get
|
||||
// overridden by any user specified styles
|
||||
let baseStyles = {}
|
||||
if (newStyles.empty) {
|
||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
||||
|
@ -45,7 +49,6 @@ export const styleable = (node, styles = {}) => {
|
|||
// Applies a style string to a DOM node
|
||||
const applyStyles = styleString => {
|
||||
node.style = styleString
|
||||
node.dataset.componentId = componentId
|
||||
}
|
||||
|
||||
// Applies the "normal" style definition
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -64,9 +64,9 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^0.9.146-alpha.4",
|
||||
"@budibase/client": "^0.9.146-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.4",
|
||||
"@budibase/auth": "^0.9.147-alpha.0",
|
||||
"@budibase/client": "^0.9.147-alpha.0",
|
||||
"@budibase/string-templates": "^0.9.147-alpha.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
"@koa/router": "8.0.0",
|
||||
"@sendgrid/mail": "7.1.1",
|
||||
|
@ -96,6 +96,7 @@
|
|||
"koa-session": "5.12.0",
|
||||
"koa-static": "5.0.0",
|
||||
"lodash": "4.17.21",
|
||||
"memorystream": "^0.3.1",
|
||||
"mongodb": "3.6.3",
|
||||
"mssql": "6.2.3",
|
||||
"mysql": "2.18.1",
|
||||
|
|
|
@ -37,7 +37,7 @@ async function init() {
|
|||
const envFileJson = {
|
||||
PORT: 4001,
|
||||
MINIO_URL: "http://localhost:10000/",
|
||||
COUCH_DB_URL: "http://@localhost:10000/db/",
|
||||
COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
|
||||
REDIS_URL: "localhost:6379",
|
||||
WORKER_URL: "http://localhost:4002",
|
||||
INTERNAL_API_KEY: "budibase",
|
||||
|
@ -48,6 +48,7 @@ async function init() {
|
|||
COUCH_DB_PASSWORD: "budibase",
|
||||
COUCH_DB_USER: "budibase",
|
||||
SELF_HOSTED: 1,
|
||||
DISABLE_ACCOUNT_PORTAL: "",
|
||||
MULTI_TENANCY: "",
|
||||
}
|
||||
let envFile = ""
|
||||
|
|
|
@ -31,7 +31,7 @@ const {
|
|||
getDeployedApps,
|
||||
removeAppFromUserRoles,
|
||||
} = require("../../utilities/workerRequests")
|
||||
const { clientLibraryPath } = require("../../utilities")
|
||||
const { clientLibraryPath, stringToReadStream } = require("../../utilities")
|
||||
const { getAllLocks } = require("../../utilities/redis")
|
||||
const {
|
||||
updateClientLibrary,
|
||||
|
@ -114,8 +114,13 @@ async function createInstance(template) {
|
|||
|
||||
// replicate the template data to the instance DB
|
||||
// this is currently very hard to test, downloading and importing template files
|
||||
/* istanbul ignore next */
|
||||
if (template && template.useTemplate === "true") {
|
||||
if (template && template.templateString) {
|
||||
const { ok } = await db.load(stringToReadStream(template.templateString))
|
||||
if (!ok) {
|
||||
throw "Error loading database dump from memory."
|
||||
}
|
||||
} else if (template && template.useTemplate === "true") {
|
||||
/* istanbul ignore next */
|
||||
const { ok } = await db.load(await getTemplateStream(template))
|
||||
if (!ok) {
|
||||
throw "Error loading database dump from template."
|
||||
|
@ -191,10 +196,11 @@ exports.fetchAppPackage = async function (ctx) {
|
|||
}
|
||||
|
||||
exports.create = async function (ctx) {
|
||||
const { useTemplate, templateKey } = ctx.request.body
|
||||
const { useTemplate, templateKey, templateString } = ctx.request.body
|
||||
const instanceConfig = {
|
||||
useTemplate,
|
||||
key: templateKey,
|
||||
templateString,
|
||||
}
|
||||
if (ctx.request.files && ctx.request.files.templateFile) {
|
||||
instanceConfig.file = ctx.request.files.templateFile
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
const env = require("../../environment")
|
||||
const { getAllApps } = require("@budibase/auth/db")
|
||||
const CouchDB = require("../../db")
|
||||
const {
|
||||
exportDB,
|
||||
sendTempFile,
|
||||
readFileSync,
|
||||
} = require("../../utilities/fileSystem")
|
||||
const { stringToReadStream } = require("../../utilities")
|
||||
const { getGlobalDBName, getGlobalDB } = require("@budibase/auth/tenancy")
|
||||
const { create } = require("./application")
|
||||
const { getDocParams, DocumentTypes, isDevAppID } = require("../../db/utils")
|
||||
|
||||
async function createApp(appName, appImport) {
|
||||
const ctx = {
|
||||
request: {
|
||||
body: {
|
||||
templateString: appImport,
|
||||
name: appName,
|
||||
},
|
||||
},
|
||||
}
|
||||
return create(ctx)
|
||||
}
|
||||
|
||||
exports.exportApps = async ctx => {
|
||||
if (env.SELF_HOSTED || !env.MULTI_TENANCY) {
|
||||
ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.")
|
||||
}
|
||||
const apps = await getAllApps(CouchDB, { all: true })
|
||||
const globalDBString = await exportDB(getGlobalDBName())
|
||||
let allDBs = {
|
||||
global: globalDBString,
|
||||
}
|
||||
for (let app of apps) {
|
||||
// only export the dev apps as they will be the latest, the user can republish the apps
|
||||
// in their self hosted environment
|
||||
if (isDevAppID(app._id)) {
|
||||
allDBs[app.name] = await exportDB(app._id)
|
||||
}
|
||||
}
|
||||
const filename = `cloud-export-${new Date().getTime()}.txt`
|
||||
ctx.attachment(filename)
|
||||
ctx.body = sendTempFile(JSON.stringify(allDBs))
|
||||
}
|
||||
|
||||
async function getAllDocType(db, docType) {
|
||||
const response = await db.allDocs(
|
||||
getDocParams(docType, null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
return response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.importApps = async ctx => {
|
||||
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
|
||||
ctx.throw(400, "Importing only allowed in self hosted environments.")
|
||||
}
|
||||
const apps = await getAllApps(CouchDB, { all: true })
|
||||
if (
|
||||
apps.length !== 0 ||
|
||||
!ctx.request.files ||
|
||||
!ctx.request.files.importFile
|
||||
) {
|
||||
ctx.throw(
|
||||
400,
|
||||
"Import file is required and environment must be fresh to import apps."
|
||||
)
|
||||
}
|
||||
const importFile = ctx.request.files.importFile
|
||||
const importString = readFileSync(importFile.path)
|
||||
const dbs = JSON.parse(importString)
|
||||
const globalDbImport = dbs.global
|
||||
// remove from the list of apps
|
||||
delete dbs.global
|
||||
const globalDb = getGlobalDB()
|
||||
// load the global db first
|
||||
await globalDb.load(stringToReadStream(globalDbImport))
|
||||
for (let [appName, appImport] of Object.entries(dbs)) {
|
||||
await createApp(appName, appImport)
|
||||
}
|
||||
// once apps are created clean up the global db
|
||||
let users = await getAllDocType(globalDb, DocumentTypes.USER)
|
||||
for (let user of users) {
|
||||
delete user.tenantId
|
||||
}
|
||||
await globalDb.bulkDocs(users)
|
||||
ctx.body = {
|
||||
message: "Apps successfully imported.",
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ const {
|
|||
generateRowID,
|
||||
DocumentTypes,
|
||||
InternalTables,
|
||||
generateMemoryViewID,
|
||||
} = require("../../../db/utils")
|
||||
const userController = require("../user")
|
||||
const {
|
||||
|
@ -20,7 +19,12 @@ const { fullSearch, paginatedSearch } = require("./internalSearch")
|
|||
const { getGlobalUsersFromMetadata } = require("../../../utilities/global")
|
||||
const inMemoryViews = require("../../../db/inMemoryView")
|
||||
const env = require("../../../environment")
|
||||
const { migrateToInMemoryView } = require("../view/utils")
|
||||
const {
|
||||
migrateToInMemoryView,
|
||||
migrateToDesignView,
|
||||
getFromDesignDoc,
|
||||
getFromMemoryDoc,
|
||||
} = require("../view/utils")
|
||||
|
||||
const CALCULATION_TYPES = {
|
||||
SUM: "sum",
|
||||
|
@ -74,33 +78,24 @@ async function getRawTableData(ctx, db, tableId) {
|
|||
}
|
||||
|
||||
async function getView(db, viewName) {
|
||||
let viewInfo
|
||||
async function getFromDesignDoc() {
|
||||
const designDoc = await db.get("_design/database")
|
||||
viewInfo = designDoc.views[viewName]
|
||||
return viewInfo
|
||||
}
|
||||
let migrate = false
|
||||
if (env.SELF_HOSTED) {
|
||||
viewInfo = await getFromDesignDoc()
|
||||
} else {
|
||||
try {
|
||||
viewInfo = await db.get(generateMemoryViewID(viewName))
|
||||
if (viewInfo) {
|
||||
viewInfo = viewInfo.view
|
||||
}
|
||||
} catch (err) {
|
||||
// check if it can be retrieved from design doc (needs migrated)
|
||||
if (err.status !== 404) {
|
||||
viewInfo = null
|
||||
} else {
|
||||
viewInfo = await getFromDesignDoc()
|
||||
migrate = !!viewInfo
|
||||
}
|
||||
let mainGetter = env.SELF_HOSTED ? getFromDesignDoc : getFromMemoryDoc
|
||||
let secondaryGetter = env.SELF_HOSTED ? getFromMemoryDoc : getFromDesignDoc
|
||||
let migration = env.SELF_HOSTED ? migrateToDesignView : migrateToInMemoryView
|
||||
let viewInfo,
|
||||
migrate = false
|
||||
try {
|
||||
viewInfo = await mainGetter(db, viewName)
|
||||
} catch (err) {
|
||||
// check if it can be retrieved from design doc (needs migrated)
|
||||
if (err.status !== 404) {
|
||||
viewInfo = null
|
||||
} else {
|
||||
viewInfo = await secondaryGetter(db, viewName)
|
||||
migrate = !!viewInfo
|
||||
}
|
||||
}
|
||||
if (migrate) {
|
||||
await migrateToInMemoryView(db, viewName)
|
||||
await migration(db, viewName)
|
||||
}
|
||||
if (!viewInfo) {
|
||||
throw "View does not exist."
|
||||
|
|
|
@ -107,3 +107,30 @@ exports.migrateToInMemoryView = async (db, viewName) => {
|
|||
await db.put(designDoc)
|
||||
await exports.saveView(db, null, viewName, view)
|
||||
}
|
||||
|
||||
exports.migrateToDesignView = async (db, viewName) => {
|
||||
let view = await db.get(generateMemoryViewID(viewName))
|
||||
const designDoc = await db.get("_design/database")
|
||||
designDoc.views[viewName] = view.view
|
||||
await db.put(designDoc)
|
||||
await db.remove(view._id, view._rev)
|
||||
}
|
||||
|
||||
exports.getFromDesignDoc = async (db, viewName) => {
|
||||
const designDoc = await db.get("_design/database")
|
||||
let view = designDoc.views[viewName]
|
||||
if (view == null) {
|
||||
throw { status: 404, message: "Unable to get view" }
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
exports.getFromMemoryDoc = async (db, viewName) => {
|
||||
let view = await db.get(generateMemoryViewID(viewName))
|
||||
if (view) {
|
||||
view = view.view
|
||||
} else {
|
||||
throw { status: 404, message: "Unable to get view" }
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/cloud")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("@budibase/auth/permissions")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/cloud/export", authorized(BUILDER), controller.exportApps)
|
||||
// has to be public, only run if apps don't exist
|
||||
.post("/api/cloud/import", controller.importApps)
|
||||
|
||||
module.exports = router
|
|
@ -24,6 +24,7 @@ const hostingRoutes = require("./hosting")
|
|||
const backupRoutes = require("./backup")
|
||||
const metadataRoutes = require("./metadata")
|
||||
const devRoutes = require("./dev")
|
||||
const cloudRoutes = require("./cloud")
|
||||
|
||||
exports.mainRoutes = [
|
||||
authRoutes,
|
||||
|
@ -49,6 +50,7 @@ exports.mainRoutes = [
|
|||
backupRoutes,
|
||||
metadataRoutes,
|
||||
devRoutes,
|
||||
cloudRoutes,
|
||||
// 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,
|
||||
|
|
|
@ -317,7 +317,7 @@ describe("/rows", () => {
|
|||
await request
|
||||
.get(`/api/views/derp`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect(400)
|
||||
.expect(404)
|
||||
})
|
||||
|
||||
it("should be able to run on a view", async () => {
|
||||
|
@ -394,4 +394,4 @@ describe("/rows", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -110,6 +110,8 @@ function getDocParams(docType, docId = null, otherProps = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
exports.getDocParams = getDocParams
|
||||
|
||||
/**
|
||||
* Gets parameters for retrieving tables, this is a utility function for the getDocParams function.
|
||||
*/
|
||||
|
|
|
@ -44,6 +44,7 @@ module.exports = {
|
|||
NODE_ENV: process.env.NODE_ENV,
|
||||
JEST_WORKER_ID: process.env.JEST_WORKER_ID,
|
||||
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||
// minor
|
||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
LOGGER: process.env.LOGGER,
|
||||
|
|
|
@ -85,10 +85,10 @@ module MongoDBModule {
|
|||
// which method we want to call on the collection
|
||||
switch (query.extra.actionTypes) {
|
||||
case "insertOne": {
|
||||
return collection.insertOne(query.json)
|
||||
return await collection.insertOne(query.json)
|
||||
}
|
||||
case "insertMany": {
|
||||
return collection.insertOne(query.json).toArray()
|
||||
return await collection.insertOne(query.json).toArray()
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
|
@ -112,19 +112,19 @@ module MongoDBModule {
|
|||
|
||||
switch (query.extra.actionTypes) {
|
||||
case "find": {
|
||||
return collection.find(query.json).toArray()
|
||||
return await collection.find(query.json).toArray()
|
||||
}
|
||||
case "findOne": {
|
||||
return collection.findOne(query.json)
|
||||
return await collection.findOne(query.json)
|
||||
}
|
||||
case "findOneAndUpdate": {
|
||||
return collection.findOneAndUpdate(query.json)
|
||||
return await collection.findOneAndUpdate(query.json)
|
||||
}
|
||||
case "count": {
|
||||
return collection.countDocuments(query.json)
|
||||
return await collection.countDocuments(query.json)
|
||||
}
|
||||
case "distinct": {
|
||||
return collection.distinct(query.json)
|
||||
return await collection.distinct(query.json)
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
|
@ -148,10 +148,10 @@ module MongoDBModule {
|
|||
|
||||
switch (query.extra.actionTypes) {
|
||||
case "updateOne": {
|
||||
return collection.updateOne(query.json)
|
||||
return await collection.updateOne(query.json)
|
||||
}
|
||||
case "updateMany": {
|
||||
return collection.updateMany(query.json).toArray()
|
||||
return await collection.updateMany(query.json).toArray()
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
|
@ -175,10 +175,10 @@ module MongoDBModule {
|
|||
|
||||
switch (query.extra.actionTypes) {
|
||||
case "deleteOne": {
|
||||
return collection.deleteOne(query.json)
|
||||
return await collection.deleteOne(query.json)
|
||||
}
|
||||
case "deleteMany": {
|
||||
return collection.deleteMany(query.json).toArray()
|
||||
return await collection.deleteMany(query.json).toArray()
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
|
|
|
@ -19,6 +19,7 @@ const {
|
|||
USER_METDATA_PREFIX,
|
||||
LINK_USER_METADATA_PREFIX,
|
||||
} = require("../../db/utils")
|
||||
const MemoryStream = require("memorystream")
|
||||
|
||||
const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..")
|
||||
const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules")
|
||||
|
@ -111,29 +112,88 @@ exports.apiFileReturn = contents => {
|
|||
* to the temporary backup file (to return via API if required).
|
||||
*/
|
||||
exports.performBackup = async (appId, backupName) => {
|
||||
const path = join(budibaseTempDir(), backupName)
|
||||
const writeStream = fs.createWriteStream(path)
|
||||
// perform couch dump
|
||||
const instanceDb = new CouchDB(appId)
|
||||
await instanceDb.dump(writeStream, {
|
||||
// filter out anything that has a user metadata structure in its ID
|
||||
return exports.exportDB(appId, {
|
||||
exportName: backupName,
|
||||
filter: doc =>
|
||||
!(
|
||||
doc._id.includes(USER_METDATA_PREFIX) ||
|
||||
doc.includes(LINK_USER_METADATA_PREFIX)
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* exports a DB to either file or a variable (memory).
|
||||
* @param {string} dbName the DB which is to be exported.
|
||||
* @param {string} exportName optional - the file name to export to, if not in memory.
|
||||
* @param {function} filter optional - a filter function to clear out any un-wanted docs.
|
||||
* @return Either the file stream or the variable (if no export name provided).
|
||||
*/
|
||||
exports.exportDB = async (
|
||||
dbName,
|
||||
{ exportName, filter } = { exportName: undefined, filter: undefined }
|
||||
) => {
|
||||
let stream,
|
||||
appString = "",
|
||||
path = null
|
||||
if (exportName) {
|
||||
path = join(budibaseTempDir(), exportName)
|
||||
stream = fs.createWriteStream(path)
|
||||
} else {
|
||||
stream = new MemoryStream()
|
||||
stream.on("data", chunk => {
|
||||
appString += chunk.toString()
|
||||
})
|
||||
}
|
||||
// perform couch dump
|
||||
const instanceDb = new CouchDB(dbName)
|
||||
await instanceDb.dump(stream, {
|
||||
filter,
|
||||
})
|
||||
// just in memory, return the final string
|
||||
if (!exportName) {
|
||||
return appString
|
||||
}
|
||||
// write the file to the object store
|
||||
if (env.SELF_HOSTED) {
|
||||
await streamUpload(
|
||||
ObjectStoreBuckets.BACKUPS,
|
||||
join(appId, backupName),
|
||||
join(dbName, exportName),
|
||||
fs.createReadStream(path)
|
||||
)
|
||||
}
|
||||
return fs.createReadStream(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the provided contents to a temporary file, which can be used briefly.
|
||||
* @param {string} fileContents contents which will be written to a temp file.
|
||||
* @return {string} the path to the temp file.
|
||||
*/
|
||||
exports.storeTempFile = fileContents => {
|
||||
const path = join(budibaseTempDir(), uuid())
|
||||
fs.writeFileSync(path, fileContents)
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for getting a file read stream - a simple in memory buffered read
|
||||
* stream doesn't work for pouchdb.
|
||||
*/
|
||||
exports.stringToFileStream = contents => {
|
||||
const path = exports.storeTempFile(contents)
|
||||
return fs.createReadStream(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temp file and returns it from the API.
|
||||
* @param {string} fileContents the contents to be returned in file.
|
||||
*/
|
||||
exports.sendTempFile = fileContents => {
|
||||
const path = exports.storeTempFile(fileContents)
|
||||
return fs.createReadStream(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the latest client library to the object store.
|
||||
* @param {string} appId The ID of the app which is being created.
|
||||
|
|
|
@ -3,6 +3,7 @@ const { OBJ_STORE_DIRECTORY } = require("../constants")
|
|||
const { sanitizeKey } = require("@budibase/auth/src/objectStore")
|
||||
const CouchDB = require("../db")
|
||||
const { generateMetadataID } = require("../db/utils")
|
||||
const Readable = require("stream").Readable
|
||||
|
||||
const BB_CDN = "https://cdn.budi.live"
|
||||
|
||||
|
@ -124,3 +125,12 @@ exports.escapeDangerousCharacters = string => {
|
|||
.replace(/[\r]/g, "\\r")
|
||||
.replace(/[\t]/g, "\\t")
|
||||
}
|
||||
|
||||
exports.stringToReadStream = string => {
|
||||
return new Readable({
|
||||
read() {
|
||||
this.push(string)
|
||||
this.push(null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -943,10 +943,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/auth@^0.9.146-alpha.4":
|
||||
version "0.9.146"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.146.tgz#920fe02a78ca17903b72ccde307ca3e82b4176ad"
|
||||
integrity sha512-T7DhI3WIolD0CjO2pRCEZfJBpJce4cmZWTFRIZ8lBnKe/6dxkK9fNrkZDYRhRkMwQbDQXoARADZM1hAfgUsSMg==
|
||||
"@budibase/auth@^0.9.147-alpha.0":
|
||||
version "0.9.147"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-0.9.147.tgz#8b959f5dae586ac4e210c7b8d1d3212859a664bc"
|
||||
integrity sha512-DL5kXc+fU6pteTWiaJG2/MYEra/gwuT3ThTHfaUIinNta1VVAFfwLWHOg5fcKUaaXvQO9GWMJPbHT9b6hJnqFA==
|
||||
dependencies:
|
||||
"@techpass/passport-openidconnect" "^0.3.0"
|
||||
aws-sdk "^2.901.0"
|
||||
|
@ -1015,10 +1015,10 @@
|
|||
svelte-flatpickr "^3.1.0"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/bbui@^0.9.146":
|
||||
version "0.9.146"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.146.tgz#7689b2c0f148321e62969181e3f6549f03dd3e78"
|
||||
integrity sha512-Mq0oMyaN18Dg5e0IPtPXSGmu/TS4B74gW+l2ypJDNTzSRm934DOAPghDgkb53rFNZhsovCYjixJZmesUcv2o3g==
|
||||
"@budibase/bbui@^0.9.147":
|
||||
version "0.9.147"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-0.9.147.tgz#b2442a4d2259afdcbf14db6223153e4e561a249e"
|
||||
integrity sha512-7GL45a9VMaxmHdbXh0xSBM+Mzw6YJCVRsAtoi0oMkd34U80M7xD58CZykb+0+4JZ8CMZqQnjBvv7QrgeWcWRaA==
|
||||
dependencies:
|
||||
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
||||
"@spectrum-css/actionbutton" "^1.0.1"
|
||||
|
@ -1064,14 +1064,14 @@
|
|||
svelte-flatpickr "^3.1.0"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/client@^0.9.146-alpha.4":
|
||||
version "0.9.146"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.146.tgz#d3b1bbd67245ab5a3870ccb580b9fc76f0344fd6"
|
||||
integrity sha512-vd/bMmiQVghFH3Pa9jrGXjYAAKo+lGrwWyfUSdXAb4XP6gCSnMK5BXf8NliNrQzQVmruYT+2rGMsnc+9q4lW1g==
|
||||
"@budibase/client@^0.9.147-alpha.0":
|
||||
version "0.9.147"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.9.147.tgz#c9171a52d15ce99df433e977c7ea716e8aad062e"
|
||||
integrity sha512-v9AnWJIs+1wesW65vbFab/fPmaWtiyIJKiEoS2ff0pANbCOVOBL3c1PWYM+BwK6r8vH9J54h/ReMCCGcaXzyGQ==
|
||||
dependencies:
|
||||
"@budibase/bbui" "^0.9.146"
|
||||
"@budibase/bbui" "^0.9.147"
|
||||
"@budibase/standard-components" "^0.9.139"
|
||||
"@budibase/string-templates" "^0.9.146"
|
||||
"@budibase/string-templates" "^0.9.147"
|
||||
regexparam "^1.3.0"
|
||||
shortid "^2.2.15"
|
||||
svelte-spa-router "^3.0.5"
|
||||
|
@ -1122,10 +1122,10 @@
|
|||
svelte-apexcharts "^1.0.2"
|
||||
svelte-flatpickr "^3.1.0"
|
||||
|
||||
"@budibase/string-templates@^0.9.146", "@budibase/string-templates@^0.9.146-alpha.4":
|
||||
version "0.9.146"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.146.tgz#85249c7a8777a5f0c280af6f6d0e3d3ff0bf20b5"
|
||||
integrity sha512-4f91SVUaTKseB+j7ycWbP54XiqiFZ6bZvcKgzsg1mLF+VVJ1/ALUsLvCRaj6SlcSHrhhALiGVR1z18KOyBWoKw==
|
||||
"@budibase/string-templates@^0.9.147", "@budibase/string-templates@^0.9.147-alpha.0":
|
||||
version "0.9.147"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-0.9.147.tgz#48d0815c15bf3b0905f54463e309d47c0274595a"
|
||||
integrity sha512-wuj20uMRXvpw5P4ScHen9n0kDfoVO0F9yi9HEPZpHF/pcAeTfL2+ohBr9irb/H4W9nHpMS0X5G/fGPEc5zqsUw==
|
||||
dependencies:
|
||||
"@budibase/handlebars-helpers" "^0.11.4"
|
||||
dayjs "^1.10.4"
|
||||
|
@ -8216,6 +8216,11 @@ memory-pager@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5"
|
||||
integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==
|
||||
|
||||
memorystream@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
|
||||
integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI=
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "0.9.146-alpha.4",
|
||||
"version": "0.9.147-alpha.0",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -25,8 +25,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^0.9.146-alpha.4",
|
||||
"@budibase/string-templates": "^0.9.146-alpha.4",
|
||||
"@budibase/auth": "^0.9.147-alpha.0",
|
||||
"@budibase/string-templates": "^0.9.147-alpha.0",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
"aws-sdk": "^2.811.0",
|
||||
|
|
|
@ -21,6 +21,7 @@ async function init() {
|
|||
COUCH_DB_PASSWORD: "budibase",
|
||||
// empty string is false
|
||||
MULTI_TENANCY: "",
|
||||
DISABLE_ACCOUNT_PORTAL: "",
|
||||
ACCOUNT_PORTAL_URL: "http://localhost:10001",
|
||||
}
|
||||
let envFile = ""
|
||||
|
|
|
@ -19,6 +19,7 @@ const {
|
|||
tryAddTenant,
|
||||
updateTenantId,
|
||||
} = require("@budibase/auth/tenancy")
|
||||
const { removeUserFromInfoDB } = require("@budibase/auth/deprovision")
|
||||
const env = require("../../../environment")
|
||||
|
||||
const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name
|
||||
|
@ -65,7 +66,7 @@ async function saveUser(
|
|||
}
|
||||
|
||||
// check root account users in account portal
|
||||
if (!env.SELF_HOSTED) {
|
||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||
const account = await accounts.getAccount(email)
|
||||
if (account && account.verified && account.tenantId !== tenantId) {
|
||||
throw `Email address ${email} already in use.`
|
||||
|
@ -132,7 +133,7 @@ exports.save = async ctx => {
|
|||
}
|
||||
|
||||
const parseBooleanParam = param => {
|
||||
if (param && param == "false") {
|
||||
if (param && param === "false") {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
|
@ -160,6 +161,17 @@ exports.adminUser = async ctx => {
|
|||
|
||||
// write usage quotas for cloud
|
||||
if (!env.SELF_HOSTED) {
|
||||
// could be a scenario where it exists, make sure its clean
|
||||
try {
|
||||
const usageQuota = await db.get(
|
||||
StaticDatabases.PLATFORM_INFO.docs.usageQuota
|
||||
)
|
||||
if (usageQuota) {
|
||||
await db.remove(usageQuota._id, usageQuota._rev)
|
||||
}
|
||||
} catch (err) {
|
||||
// don't worry about errors
|
||||
}
|
||||
await db.post(generateNewUsageQuotaDoc())
|
||||
}
|
||||
|
||||
|
@ -193,6 +205,7 @@ exports.adminUser = async ctx => {
|
|||
exports.destroy = async ctx => {
|
||||
const db = getGlobalDB()
|
||||
const dbUser = await db.get(ctx.params.id)
|
||||
await removeUserFromInfoDB(dbUser)
|
||||
await db.remove(dbUser._id, dbUser._rev)
|
||||
await userCache.invalidateUser(dbUser._id)
|
||||
await invalidateSessions(dbUser._id)
|
||||
|
|
|
@ -5,5 +5,6 @@ exports.fetch = async ctx => {
|
|||
multiTenancy: !!env.MULTI_TENANCY,
|
||||
cloud: !env.SELF_HOSTED,
|
||||
accountPortalUrl: env.ACCOUNT_PORTAL_URL,
|
||||
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ module.exports = {
|
|||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
|
||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
|
||||
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
|
||||
SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED,
|
||||
SMTP_USER: process.env.SMTP_USER,
|
||||
|
|
Loading…
Reference in New Issue